refactor(*): 稳定期治理 - 收敛核心架构、错误语义并修复跨项目数据竞态
This commit is contained in:
1 parent
bacd502f5c
commit
b5f3413ded
298 files changed
+12681
-26428
No files matched your search
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import axios from "axios";
|
||||
|
||||
import { apiClient } from "@/services/api/request";
|
||||
import { ResourceUploadError, uploadResourceFile } from "@/services/api/resources";
|
||||
|
||||
type AdapterResult = { status: number; body: unknown };
|
||||
|
||||
// 直传失败的分类是这条边界的全部价值:permanent 决定调用方是当场报错还是退回本机等待重传。
|
||||
async function uploadWith(result: AdapterResult | Error) {
|
||||
const previous = apiClient.defaults.adapter;
|
||||
apiClient.defaults.adapter = async (config) => {
|
||||
if (result instanceof Error) throw result;
|
||||
if (result.status >= 400) {
|
||||
throw new axios.AxiosError(`Request failed with status code ${result.status}`, "ERR_BAD_REQUEST", config, undefined, {
|
||||
data: result.body,
|
||||
status: result.status,
|
||||
statusText: "Error",
|
||||
headers: {},
|
||||
config,
|
||||
});
|
||||
}
|
||||
return { data: result.body, status: result.status, statusText: "OK", headers: {}, config };
|
||||
};
|
||||
try {
|
||||
return await uploadResourceFile(new Blob(["x"], { type: "image/png" }), "image", { fileName: "x.png" });
|
||||
} finally {
|
||||
apiClient.defaults.adapter = previous;
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFailure(result: AdapterResult | Error) {
|
||||
try {
|
||||
await uploadWith(result);
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
throw new Error("上传本应失败");
|
||||
}
|
||||
|
||||
describe("资源直传失败分类", () => {
|
||||
test("鉴权、越权与体积超限属于重试不会自愈的永久失败", async () => {
|
||||
for (const status of [400, 401, 403, 404, 413, 415]) {
|
||||
const error = await uploadFailure({ status, body: { code: status, data: null, msg: "" } });
|
||||
expect(error).toBeInstanceOf(ResourceUploadError);
|
||||
expect((error as ResourceUploadError).permanent).toBe(true);
|
||||
expect((error as ResourceUploadError).status).toBe(status);
|
||||
}
|
||||
});
|
||||
|
||||
test("限流与服务端故障属于瞬时失败,允许退回本机后重传", async () => {
|
||||
for (const status of [429, 500, 502, 503]) {
|
||||
const error = await uploadFailure({ status, body: { code: status, data: null, msg: "" } });
|
||||
expect(error).toBeInstanceOf(ResourceUploadError);
|
||||
expect((error as ResourceUploadError).permanent).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("断网等非 HTTP 失败按瞬时处理", async () => {
|
||||
const error = await uploadFailure(new Error("Network Error"));
|
||||
expect(error).toBeInstanceOf(ResourceUploadError);
|
||||
expect((error as ResourceUploadError).permanent).toBe(false);
|
||||
});
|
||||
|
||||
test("保留后端可读文案,并把 multipart 超限翻译成中文", async () => {
|
||||
const quota = await uploadFailure({ status: 403, body: { code: 403, data: null, msg: "存储配额不足" } });
|
||||
expect((quota as ResourceUploadError).message).toBe("存储配额不足");
|
||||
|
||||
const oversize = await uploadFailure({ status: 400, body: { code: 400, data: null, msg: "http: request body too large" } });
|
||||
expect((oversize as ResourceUploadError).message).toContain("文件过大");
|
||||
expect((oversize as ResourceUploadError).permanent).toBe(true);
|
||||
});
|
||||
|
||||
test("成功响应仍返回后端资源本身", async () => {
|
||||
const resource = await uploadWith({
|
||||
status: 200,
|
||||
body: { code: 0, msg: "", data: { resource: { id: "res-1", kind: "image", status: "ready", publicUrl: "", size: 1 } } },
|
||||
});
|
||||
expect(resource.id).toBe("res-1");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user