From 200fc18210d2d6eab1e56e313f4cbb84778a1dcc Mon Sep 17 00:00:00 2001 From: kris Date: Sat, 28 Mar 2026 08:33:08 +0800 Subject: [PATCH] test: cover native forward request boundary --- .../java/com/hyzq/boss/BossApiClient.java | 18 +- .../boss/BossApiClientForwardingTest.java | 227 ++++++++++++++++++ 2 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 android/app/src/test/java/com/hyzq/boss/BossApiClientForwardingTest.java diff --git a/android/app/src/main/java/com/hyzq/boss/BossApiClient.java b/android/app/src/main/java/com/hyzq/boss/BossApiClient.java index 84dba9b..b2baf38 100644 --- a/android/app/src/main/java/com/hyzq/boss/BossApiClient.java +++ b/android/app/src/main/java/com/hyzq/boss/BossApiClient.java @@ -33,8 +33,12 @@ public class BossApiClient { private final String baseUrl; public BossApiClient(Context context) { - this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - this.baseUrl = BuildConfig.BOSS_API_BASE_URL; + this(context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE), BuildConfig.BOSS_API_BASE_URL); + } + + BossApiClient(SharedPreferences prefs, String baseUrl) { + this.prefs = prefs; + this.baseUrl = baseUrl; } public boolean hasSessionHints() { @@ -264,7 +268,7 @@ public class BossApiClient { } private ApiResponse requestRaw(String method, String path, @Nullable String body, boolean expectProtected) throws IOException, JSONException { - HttpURLConnection connection = (HttpURLConnection) new URL(baseUrl + path).openConnection(); + HttpURLConnection connection = openConnection(path); connection.setRequestMethod(method); connection.setConnectTimeout(12000); connection.setReadTimeout(12000); @@ -301,6 +305,10 @@ public class BossApiClient { return new ApiResponse(statusCode, json == null ? new JSONObject() : json); } + HttpURLConnection openConnection(String path) throws IOException { + return (HttpURLConnection) new URL(baseUrl + path).openConnection(); + } + private JSONObject readJson(InputStream stream) throws IOException, JSONException { if (stream == null) { return new JSONObject(); @@ -339,7 +347,7 @@ public class BossApiClient { } } - private void rememberIdentity(JSONObject json) { + void rememberIdentity(JSONObject json) { if (json == null) return; JSONObject session = json.optJSONObject("session"); JSONObject source = session != null ? session : json; @@ -370,7 +378,7 @@ public class BossApiClient { .apply(); } - private String encode(String value) { + String encode(String value) { return Uri.encode(value); } diff --git a/android/app/src/test/java/com/hyzq/boss/BossApiClientForwardingTest.java b/android/app/src/test/java/com/hyzq/boss/BossApiClientForwardingTest.java new file mode 100644 index 0000000..632a802 --- /dev/null +++ b/android/app/src/test/java/com/hyzq/boss/BossApiClientForwardingTest.java @@ -0,0 +1,227 @@ +package com.hyzq.boss; + +import static org.junit.Assert.assertEquals; +import android.content.SharedPreferences; + +import org.json.JSONObject; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +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.Map; +import java.util.Set; + +public class BossApiClientForwardingTest { + @Test + public void forwardProjectMessageWritesStructuredJsonBody() throws Exception { + RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/source/forwards")); + RecordingBossApiClient apiClient = new RecordingBossApiClient(connection); + JSONObject payload = ForwardPayloads.build("single", "m1", java.util.List.of()); + + BossApiClient.ApiResponse response = apiClient.forwardProjectMessage("source", "target", payload); + + assertEquals(200, response.statusCode); + assertEquals("/api/v1/projects/source/forwards", apiClient.lastPath); + assertEquals("POST", connection.requestMethodValue); + assertEquals( + "{\"targetProjectId\":\"target\",\"mode\":\"single\",\"sourceMessageId\":\"m1\"}", + connection.requestBody() + ); + } + + private static final class RecordingBossApiClient extends BossApiClient { + private final RecordingConnection connection; + private String lastPath = ""; + + RecordingBossApiClient(RecordingConnection connection) { + super(new InMemorySharedPreferences(), "https://boss.hyzq.net"); + this.connection = connection; + } + + @Override + HttpURLConnection openConnection(String path) { + lastPath = path; + return connection; + } + + @Override + String encode(String value) { + return value; + } + + @Override + void rememberIdentity(JSONObject json) { + // JVM 单测只关心 request body,不需要走 Android org.json 的身份恢复副作用。 + } + } + + private static final class RecordingConnection extends HttpURLConnection { + private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream(); + private final Map requestHeaders = new HashMap<>(); + 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 void setRequestProperty(String key, String value) { + requestHeaders.put(key, value); + } + + @Override + public String getRequestProperty(String key) { + return requestHeaders.get(key); + } + + @Override + public OutputStream getOutputStream() { + return requestBody; + } + + @Override + public int getResponseCode() { + return 200; + } + + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream("{\"ok\":true}".getBytes(StandardCharsets.UTF_8)); + } + + String requestBody() { + return requestBody.toString(StandardCharsets.UTF_8); + } + } + + private static final class InMemorySharedPreferences implements SharedPreferences { + private final Map values = new HashMap<>(); + + @Override + public Map getAll() { + return Collections.unmodifiableMap(values); + } + + @Override + public String getString(String key, String defValue) { + return values.getOrDefault(key, defValue); + } + + @Override + public Set getStringSet(String key, Set 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 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) {} + } +}