feat: harden enterprise control plane
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class BossApiClientLogoutTest {
|
||||
@Test
|
||||
public void logoutClearsAllCachedIdentityHints() throws Exception {
|
||||
InMemorySharedPreferences prefs = new InMemorySharedPreferences();
|
||||
prefs.edit().putString("session_cookie", "boss_session=session-token").apply();
|
||||
BossApiClient apiClient = new RecordingBossApiClient(prefs);
|
||||
apiClient.rememberIdentity(new JSONObject()
|
||||
.put("restoreToken", "restore-token")
|
||||
.put("account", "honor_user")
|
||||
.put("displayName", "荣耀测试账号"));
|
||||
|
||||
BossApiClient.ApiResponse response = apiClient.logout();
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertFalse(prefs.contains("session_cookie"));
|
||||
assertFalse(prefs.contains("restore_token"));
|
||||
assertFalse(prefs.contains("account"));
|
||||
assertFalse(prefs.contains("display_name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutClearsLocalAuthEvenWhenServerRequestFails() throws Exception {
|
||||
InMemorySharedPreferences prefs = new InMemorySharedPreferences();
|
||||
prefs.edit()
|
||||
.putString("session_cookie", "boss_session=session-token")
|
||||
.putString("restore_token", "restore-token")
|
||||
.putString("account", "honor_user")
|
||||
.putString("display_name", "荣耀测试账号")
|
||||
.apply();
|
||||
BossApiClient apiClient = new FailingLogoutBossApiClient(prefs);
|
||||
|
||||
try {
|
||||
apiClient.logout();
|
||||
} catch (IOException expected) {
|
||||
// Local logout state must still be cleared if the network request fails.
|
||||
}
|
||||
|
||||
assertFalse(prefs.contains("session_cookie"));
|
||||
assertFalse(prefs.contains("restore_token"));
|
||||
assertFalse(prefs.contains("account"));
|
||||
assertFalse(prefs.contains("display_name"));
|
||||
}
|
||||
|
||||
private static final class RecordingBossApiClient extends BossApiClient {
|
||||
RecordingBossApiClient(SharedPreferences prefs) {
|
||||
super(prefs, "https://boss.hyzq.net");
|
||||
}
|
||||
|
||||
@Override
|
||||
HttpURLConnection openConnection(String path) throws java.io.IOException {
|
||||
return new RecordingConnection(new URL("https://boss.hyzq.net" + path));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FailingLogoutBossApiClient extends BossApiClient {
|
||||
FailingLogoutBossApiClient(SharedPreferences prefs) {
|
||||
super(prefs, "https://boss.hyzq.net");
|
||||
}
|
||||
|
||||
@Override
|
||||
HttpURLConnection openConnection(String path) throws IOException {
|
||||
throw new IOException("network down");
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingConnection extends HttpURLConnection {
|
||||
private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream();
|
||||
private String requestMethodValue = "GET";
|
||||
|
||||
RecordingConnection(URL url) {
|
||||
super(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() {}
|
||||
|
||||
@Override
|
||||
public boolean usingProxy() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() {}
|
||||
|
||||
@Override
|
||||
public void setRequestMethod(String method) throws ProtocolException {
|
||||
requestMethodValue = method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequestMethod() {
|
||||
return requestMethodValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getResponseCode() {
|
||||
return 200;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() {
|
||||
return requestBody;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getHeaderFields() {
|
||||
return Map.of("Set-Cookie", List.of("boss_session=; Max-Age=0; Path=/"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return new ByteArrayInputStream("{\"ok\":true}".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InMemorySharedPreferences implements SharedPreferences {
|
||||
private final Map<String, String> values = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Map<String, ?> getAll() {
|
||||
return Collections.unmodifiableMap(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(String key, String defValue) {
|
||||
return values.getOrDefault(key, defValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getStringSet(String key, Set<String> defValues) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(String key, int defValue) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(String key, long defValue) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getFloat(String key, float defValue) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String key) {
|
||||
return values.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor edit() {
|
||||
return new Editor() {
|
||||
@Override
|
||||
public Editor putString(String key, String value) {
|
||||
values.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor remove(String key) {
|
||||
values.remove(key);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor clear() {
|
||||
values.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply() {}
|
||||
|
||||
@Override
|
||||
public boolean commit() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor putStringSet(String key, Set<String> values) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor putInt(String key, int value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor putLong(String key, long value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor putFloat(String key, float value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor putBoolean(String key, boolean value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
|
||||
@Override
|
||||
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.view.View;
|
||||
|
||||
@@ -67,6 +68,27 @@ public class MainActivityBootstrapSessionTest {
|
||||
assertEquals("krisolo", sessionData.optString("account", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forceLogoutIntentClearsExistingContentSessionAndShowsLogin() throws Exception {
|
||||
TestRestoreBootstrapSessionMainActivity activity =
|
||||
Robolectric.buildActivity(TestRestoreBootstrapSessionMainActivity.class).setup().get();
|
||||
|
||||
waitFor(() -> activity.apiClient.restoreCalls > 0 && activity.apiClient.homeCalls > 0);
|
||||
|
||||
Intent intent = new Intent(activity, MainActivity.class);
|
||||
intent.putExtra("force_logout", true);
|
||||
activity.onNewIntent(intent);
|
||||
Shadows.shadowOf(android.os.Looper.getMainLooper()).idleFor(Duration.ofMillis(200));
|
||||
|
||||
View loginPanel = activity.findViewById(R.id.login_panel);
|
||||
View contentPanel = activity.findViewById(R.id.content_panel);
|
||||
JSONObject sessionData = ReflectionHelpers.getField(activity, "sessionData");
|
||||
|
||||
assertEquals(View.VISIBLE, loginPanel.getVisibility());
|
||||
assertEquals(View.GONE, contentPanel.getVisibility());
|
||||
assertEquals(null, sessionData);
|
||||
}
|
||||
|
||||
private static void waitFor(BooleanSupplier condition) {
|
||||
long deadline = System.currentTimeMillis() + 5_000L;
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
|
||||
@@ -904,6 +904,53 @@ public class ProjectDetailActivityUiTest {
|
||||
assertTrue(viewTreeContainsText(messageView, "Mendel(explorer)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeRemoteExecutionProgressDoesNotRenderCodexSections() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "native-remote")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "MacBook Air 桌面控制");
|
||||
TestProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(TestProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
JSONObject message = new JSONObject()
|
||||
.put("id", "native-progress-1")
|
||||
.put("sender", "master")
|
||||
.put("senderLabel", "主 Agent")
|
||||
.put("body", "执行进度")
|
||||
.put("kind", "execution_progress")
|
||||
.put("sentAt", "2026-05-13T10:16:00+08:00")
|
||||
.put("executionProgress", new JSONObject()
|
||||
.put("title", "远程控制进度")
|
||||
.put("controlMode", "native_remote_control")
|
||||
.put("runtimeKind", "browser-automation-runtime")
|
||||
.put("status", "running")
|
||||
.put("steps", new JSONArray()
|
||||
.put(new JSONObject().put("text", "接收远程控制指令").put("status", "done"))
|
||||
.put(new JSONObject().put("text", "连接目标电脑").put("status", "running")))
|
||||
.put("branch", new JSONObject()
|
||||
.put("githubCliStatus", "unavailable"))
|
||||
.put("agents", new JSONArray()
|
||||
.put(new JSONObject().put("name", "Mendel").put("role", "explorer"))));
|
||||
|
||||
View messageView = ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"buildMessageView",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, message)
|
||||
);
|
||||
|
||||
assertTrue(viewTreeContainsText(messageView, "远程控制进度"));
|
||||
assertTrue(viewTreeContainsText(messageView, "接收远程控制指令"));
|
||||
assertTrue(viewTreeContainsText(messageView, "连接目标电脑"));
|
||||
assertFalse(viewTreeContainsText(messageView, "分支详情"));
|
||||
assertFalse(viewTreeContainsText(messageView, "Git 操作"));
|
||||
assertFalse(viewTreeContainsText(messageView, "GitHub CLI 不可用"));
|
||||
assertFalse(viewTreeContainsText(messageView, "后台智能体"));
|
||||
assertFalse(viewTreeContainsText(messageView, "Mendel(explorer)"));
|
||||
assertFalse(viewTreeContainsText(messageView, "Codex"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completedReplyResponseRendersImmediatelyWithoutReloadingProjectDetail() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
|
||||
Reference in New Issue
Block a user