test(cache): 修复CacheConfigTest边界值测试

- 修改 shouldVerifyCacheManager_withMaximumIntegerTtl 为 shouldVerifyCacheManager_withMaximumAllowedTtl
- 使用正确的最大TTL值(10080分钟,7天)而不是 Integer.MAX_VALUE
- 新增 shouldThrowException_whenTtlExceedsMaximum 测试验证边界检查
- 所有1266个测试用例通过
- 覆盖率: 指令81.89%, 行88.48%, 分支51.55%

docs: 添加项目状态报告
- 生成 PROJECT_STATUS_REPORT.md 详细记录项目当前状态
- 包含质量指标、已完成功能、待办事项和技术债务
This commit is contained in:
Your Name
2026-03-02 13:31:54 +08:00
parent 32d6449ea4
commit 91a0b77f7a
2272 changed files with 221995 additions and 503 deletions

View File

@@ -0,0 +1,110 @@
package com.mosquito.project.sdk;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.mosquito.project.dto.ApiResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.net.http.HttpClient;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ApiClientTest {
private TestHttpClient httpClient;
private ApiClient client;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
httpClient = new TestHttpClient();
client = new ApiClient("http://localhost", "test-key");
setHttpClient(client, httpClient.client());
objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
}
@Test
void get_shouldUnwrapData() throws Exception {
Payload payload = new Payload();
payload.setValue("ok");
String json = objectMapper.writeValueAsString(ApiResponse.success(payload));
httpClient.register("GET", "/ok", 200, json);
Payload result = client.get("/ok", Payload.class);
assertEquals("ok", result.getValue());
}
@Test
void getList_shouldReturnEmptyWhenNullData() throws Exception {
String json = objectMapper.writeValueAsString(ApiResponse.success(null));
httpClient.register("GET", "/list", 200, json);
List<Payload> result = client.getList("/list", Payload.class);
assertTrue(result.isEmpty());
}
@Test
void getStringAndBytes_shouldReturnBody() {
httpClient.register("GET", "/text", 200, "hello");
httpClient.register("GET", "/bytes", 200, new byte[]{1, 2, 3});
assertEquals("hello", client.getString("/text"));
assertArrayEquals(new byte[]{1, 2, 3}, client.getBytes("/bytes"));
}
@Test
void postAndPut_shouldReturnPayload() throws Exception {
Payload payload = new Payload();
payload.setValue("posted");
String json = objectMapper.writeValueAsString(ApiResponse.success(payload));
httpClient.register("POST", "/post", 200, json);
httpClient.register("PUT", "/put", 200, json);
Payload postResult = client.post("/post", Map.of("name", "value"), Payload.class);
Payload putResult = client.put("/put", Map.of("name", "value"), Payload.class);
assertEquals("posted", postResult.getValue());
assertEquals("posted", putResult.getValue());
}
@Test
void delete_shouldThrow_whenStatusNotOk() {
httpClient.register("DELETE", "/delete", 500, "fail");
assertThrows(RuntimeException.class, () -> client.delete("/delete"));
}
@Test
void get_shouldThrow_whenApiResponseCodeError() throws Exception {
String json = objectMapper.writeValueAsString(ApiResponse.error(400, "bad"));
httpClient.register("GET", "/error", 200, json);
assertThrows(RuntimeException.class, () -> client.get("/error", Payload.class));
}
private static void setHttpClient(ApiClient apiClient, HttpClient httpClient) {
try {
Field field = ApiClient.class.getDeclaredField("httpClient");
field.setAccessible(true);
field.set(apiClient, httpClient);
} catch (Exception e) {
throw new RuntimeException("Failed to set HttpClient", e);
}
}
static class Payload {
private String value;
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
}
}

View File

@@ -0,0 +1,166 @@
package com.mosquito.project.sdk;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.mosquito.project.dto.ApiResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.net.http.HttpClient;
import java.time.ZonedDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class MosquitoClientTest {
private TestHttpClient httpClient;
private MosquitoClient client;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
httpClient = new TestHttpClient();
client = new MosquitoClient("http://localhost", "test-key");
setHttpClient(client, httpClient.client());
objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
}
@Test
void client_shouldCallEndpointsAndParseResponses() throws Exception {
ZonedDateTime start = ZonedDateTime.parse("2025-01-01T00:00:00Z");
ZonedDateTime end = ZonedDateTime.parse("2025-01-02T00:00:00Z");
MosquitoClient.Activity activity = new MosquitoClient.Activity();
activity.setId(1L);
activity.setName("Activity A");
activity.setStartTime(start);
activity.setEndTime(end);
MosquitoClient.Activity updated = new MosquitoClient.Activity();
updated.setId(1L);
updated.setName("Activity B");
updated.setStartTime(start);
updated.setEndTime(end);
MosquitoClient.DailyStats daily = new MosquitoClient.DailyStats();
daily.setDate("2025-01-01");
daily.setParticipants(5);
daily.setShares(3);
MosquitoClient.ActivityStats stats = new MosquitoClient.ActivityStats();
stats.setTotalParticipants(5);
stats.setTotalShares(3);
stats.setDaily(List.of(daily));
MosquitoClient.ShortenResponse shorten = new MosquitoClient.ShortenResponse();
shorten.setCode("abc");
shorten.setPath("/r/abc");
shorten.setOriginalUrl("https://example.com");
MosquitoClient.ShareMeta shareMeta = new MosquitoClient.ShareMeta();
shareMeta.setTitle("title");
shareMeta.setDescription("desc");
shareMeta.setImage("img");
shareMeta.setUrl("url");
MosquitoClient.PosterConfig posterConfig = new MosquitoClient.PosterConfig();
posterConfig.setTemplate("default");
posterConfig.setImageUrl("/poster.png");
posterConfig.setHtmlUrl("/poster.html");
MosquitoClient.LeaderboardEntry entry = new MosquitoClient.LeaderboardEntry();
entry.setUserId(7L);
entry.setUserName("user");
entry.setScore(100);
MosquitoClient.RewardInfo reward = new MosquitoClient.RewardInfo();
reward.setType("points");
reward.setPoints(10);
reward.setCreatedAt("2025-01-01T00:00:00Z");
MosquitoClient.CreateApiKeyResponse createApiKeyResponse = new MosquitoClient.CreateApiKeyResponse();
createApiKeyResponse.setApiKey("raw-key");
MosquitoClient.RevealApiKeyResponse revealApiKeyResponse = new MosquitoClient.RevealApiKeyResponse();
revealApiKeyResponse.setApiKey("revealed-key");
revealApiKeyResponse.setMessage("one-time");
httpClient.register("POST", "/api/v1/activities", 200, objectMapper.writeValueAsString(ApiResponse.success(activity)));
httpClient.register("GET", "/api/v1/activities/1", 200, objectMapper.writeValueAsString(ApiResponse.success(activity)));
httpClient.register("PUT", "/api/v1/activities/1", 200, objectMapper.writeValueAsString(ApiResponse.success(updated)));
httpClient.register("GET", "/api/v1/activities/1/stats", 200, objectMapper.writeValueAsString(ApiResponse.success(stats)));
httpClient.register("GET", "/api/v1/me/invitation-info", 200, objectMapper.writeValueAsString(ApiResponse.success(shorten)));
httpClient.register("GET", "/api/v1/me/share-meta", 200, objectMapper.writeValueAsString(ApiResponse.success(shareMeta)));
httpClient.register("GET", "/api/v1/me/poster/image", 200, new byte[]{9, 8, 7});
httpClient.register("GET", "/api/v1/me/poster/html", 200, "<html>ok</html>");
httpClient.register("GET", "/api/v1/me/poster/config", 200, objectMapper.writeValueAsString(ApiResponse.success(posterConfig)));
httpClient.register("GET", "/api/v1/activities/1/leaderboard", 200, objectMapper.writeValueAsString(ApiResponse.success(List.of(entry))));
httpClient.register("GET", "/api/v1/activities/1/leaderboard/export", 200, "csv-data");
httpClient.register("GET", "/api/v1/me/rewards", 200, objectMapper.writeValueAsString(ApiResponse.success(List.of(reward))));
httpClient.register("POST", "/api/v1/api-keys", 200, objectMapper.writeValueAsString(ApiResponse.success(createApiKeyResponse)));
httpClient.register("GET", "/api/v1/api-keys/1/reveal", 200, objectMapper.writeValueAsString(ApiResponse.success(revealApiKeyResponse)));
httpClient.register("DELETE", "/api/v1/api-keys/1", 204, "");
httpClient.register("GET", "/actuator/health", 200, "ok");
MosquitoClient.Activity created = client.createActivity("Activity A", start, end);
MosquitoClient.Activity fetched = client.getActivity(1L);
MosquitoClient.Activity changed = client.updateActivity(1L, "Activity B", end);
MosquitoClient.ActivityStats fetchedStats = client.getActivityStats(1L);
String shareUrl = client.getShareUrl(1L, 2L);
MosquitoClient.ShareMeta meta = client.getShareMeta(1L, 2L);
byte[] posterImage = client.getPosterImage(1L, 2L);
String posterHtml = client.getPosterHtml(1L, 2L);
MosquitoClient.PosterConfig config = client.getPosterConfig("default");
List<MosquitoClient.LeaderboardEntry> leaderboard = client.getLeaderboard(1L);
List<MosquitoClient.LeaderboardEntry> leaderboardPaged = client.getLeaderboard(1L, 1, 2);
String csv = client.exportLeaderboardCsv(1L);
String csvTop = client.exportLeaderboardCsv(1L, 5);
List<MosquitoClient.RewardInfo> rewards = client.getUserRewards(1L, 2L);
String apiKey = client.createApiKey(1L, "key");
client.revokeApiKey(1L);
String revealed = client.revealApiKey(1L);
boolean healthy = client.isHealthy();
assertEquals("Activity A", created.getName());
assertEquals("Activity A", fetched.getName());
assertEquals("Activity B", changed.getName());
assertEquals(5, fetchedStats.getTotalParticipants());
assertTrue(shareUrl.endsWith("/r/abc"));
assertEquals("title", meta.getTitle());
assertArrayEquals(new byte[]{9, 8, 7}, posterImage);
assertEquals("<html>ok</html>", posterHtml);
assertEquals("default", config.getTemplate());
assertEquals(1, leaderboard.size());
assertEquals(1, leaderboardPaged.size());
assertEquals("csv-data", csv);
assertEquals("csv-data", csvTop);
assertEquals(1, rewards.size());
assertEquals("raw-key", apiKey);
assertEquals("revealed-key", revealed);
assertTrue(healthy);
}
@Test
void isHealthy_shouldReturnFalse_whenEndpointFails() {
MosquitoClient unreachable = new MosquitoClient("http://localhost:1", "test-key");
assertFalse(unreachable.isHealthy());
}
private static void setHttpClient(MosquitoClient mosquitoClient, HttpClient httpClient) {
try {
Field apiClientField = MosquitoClient.class.getDeclaredField("apiClient");
apiClientField.setAccessible(true);
ApiClient apiClient = (ApiClient) apiClientField.get(mosquitoClient);
Field httpClientField = ApiClient.class.getDeclaredField("httpClient");
httpClientField.setAccessible(true);
httpClientField.set(apiClient, httpClient);
} catch (Exception e) {
throw new RuntimeException("Failed to set HttpClient", e);
}
}
}

View File

@@ -0,0 +1,64 @@
package com.mosquito.project.sdk;
import org.mockito.Mockito;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
class TestHttpClient {
private static final class ResponseSpec {
private final int status;
private final Object body;
private ResponseSpec(int status, Object body) {
this.status = status;
this.body = body;
}
}
private final HttpClient client;
private final Map<String, ResponseSpec> responses = new ConcurrentHashMap<>();
TestHttpClient() {
this.client = Mockito.mock(HttpClient.class);
try {
Mockito.when(client.send(Mockito.any(HttpRequest.class), Mockito.any(HttpResponse.BodyHandler.class)))
.thenAnswer(invocation -> {
HttpRequest request = invocation.getArgument(0);
String key = key(request.method(), request.uri().getPath());
ResponseSpec spec = responses.get(key);
if (spec == null) {
throw new RuntimeException("No stubbed response for " + key);
}
return buildResponse(spec);
});
} catch (IOException | InterruptedException e) {
throw new RuntimeException("Failed to stub HttpClient", e);
}
}
HttpClient client() {
return client;
}
void register(String method, String path, int status, Object body) {
responses.put(key(method, path), new ResponseSpec(status, body));
}
private String key(String method, String path) {
return method + " " + path;
}
@SuppressWarnings("unchecked")
private <T> HttpResponse<T> buildResponse(ResponseSpec spec) {
HttpResponse<T> response = (HttpResponse<T>) Mockito.mock(HttpResponse.class);
Mockito.when(response.statusCode()).thenReturn(spec.status);
Mockito.when(response.body()).thenReturn((T) spec.body);
return response;
}
}

View File

@@ -0,0 +1,84 @@
package com.mosquito.project.sdk;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
class TestHttpServer implements AutoCloseable {
private static final class Response {
private final int status;
private final String contentType;
private final byte[] body;
private Response(int status, String contentType, byte[] body) {
this.status = status;
this.contentType = contentType;
this.body = body;
}
}
private final HttpServer server;
private final Map<String, Response> responses = new ConcurrentHashMap<>();
TestHttpServer() {
try {
this.server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
} catch (IOException e) {
throw new RuntimeException("Failed to start test server", e);
}
this.server.createContext("/", this::handle);
this.server.start();
}
String baseUrl() {
return "http://localhost:" + server.getAddress().getPort();
}
void register(String method, String path, int status, String contentType, byte[] body) {
responses.put(key(method, path), new Response(status, contentType, body));
}
void registerJson(String method, String path, String json) {
register(method, path, 200, "application/json", json.getBytes(StandardCharsets.UTF_8));
}
void registerText(String method, String path, String text) {
register(method, path, 200, "text/plain", text.getBytes(StandardCharsets.UTF_8));
}
private void handle(HttpExchange exchange) throws IOException {
String requestKey = key(exchange.getRequestMethod(), exchange.getRequestURI().getPath());
Response response = responses.get(requestKey);
if (response == null) {
exchange.sendResponseHeaders(404, -1);
exchange.close();
return;
}
if (response.contentType != null) {
exchange.getResponseHeaders().add("Content-Type", response.contentType);
}
if (response.body == null) {
exchange.sendResponseHeaders(response.status, -1);
exchange.close();
return;
}
exchange.sendResponseHeaders(response.status, response.body.length);
exchange.getResponseBody().write(response.body);
exchange.close();
}
private String key(String method, String path) {
return method + " " + path;
}
@Override
public void close() {
server.stop(0);
}
}