fix(deploy): pin distinct image tags per compose (booklib/{api,web}:{dev,prod}); same project name shared default tags so dev up without --build ran the prod image
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { api, apiRaw, formatPageUrl, getToken, HttpError, setToken } from "../src/api/client";
|
||||
|
||||
const jres = (status: number, body: unknown) =>
|
||||
new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
||||
|
||||
beforeEach(() => setToken(null));
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it("login 成功存 token,请求体带凭据", async () => {
|
||||
const f = vi.fn(async (_url: any, _init: any) => jres(200, { token: "T" }));
|
||||
vi.stubGlobal("fetch", f);
|
||||
await expect(api.login("a", "b")).resolves.toEqual({ token: "T" });
|
||||
expect(f.mock.calls[0][0]).toBe("/api/auth/login");
|
||||
expect(String(f.mock.calls[0][1].body)).toContain('"username":"a"');
|
||||
});
|
||||
|
||||
it("401 清 token 并抛 HttpError(parsed code)", async () => {
|
||||
setToken("T");
|
||||
vi.stubGlobal("fetch", async (_url: any, init: any) => {
|
||||
expect(init.headers.Authorization).toBe("Bearer T");
|
||||
return jres(401, { error: { code: "unauthorized", message: "bad credentials" } });
|
||||
});
|
||||
const e = await api.login("a", "b").catch((e) => e);
|
||||
expect(e).toBeInstanceOf(HttpError);
|
||||
expect((e as HttpError).code).toBe("unauthorized");
|
||||
expect(getToken()).toBeNull();
|
||||
});
|
||||
|
||||
it("非 JSON 错误体回退 http_<status>", async () => {
|
||||
vi.stubGlobal("fetch", async () => new Response("oops", { status: 500 }));
|
||||
const e = await api.listUsers().catch((e) => e);
|
||||
expect((e as HttpError).code).toBe("http_500");
|
||||
});
|
||||
|
||||
it("204 无体 resolve undefined", async () => {
|
||||
vi.stubGlobal("fetch", async () => new Response(null, { status: 204 }));
|
||||
await expect(api.deleteBook(1)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("相对路径补 /api 前缀,绝对 /api 原样;query 拼装", async () => {
|
||||
const seen: string[] = [];
|
||||
vi.stubGlobal("fetch", async (url: any) => {
|
||||
seen.push(String(url));
|
||||
return jres(200, []);
|
||||
});
|
||||
await api.listBooks({ library: 3, q: "x" });
|
||||
await apiRaw("/api/books/1/cover?v=h");
|
||||
expect(seen).toEqual(["/api/books?library=3&q=x", "/api/books/1/cover?v=h"]);
|
||||
});
|
||||
|
||||
it("page_url_fmt %d 替换", () => {
|
||||
expect(formatPageUrl("/api/books/9/pages/%d?v=h", 12)).toBe("/api/books/9/pages/12?v=h");
|
||||
});
|
||||
|
||||
it("putProgress keepalive 透传 + JSON body", async () => {
|
||||
const f = vi.fn(async (_url: any, _init: any) => new Response(null, { status: 204 }));
|
||||
vi.stubGlobal("fetch", f);
|
||||
await api.putProgress(1, { page: 2 }, 0.5, true);
|
||||
expect(f.mock.calls[0][1].keepalive).toBe(true);
|
||||
expect(String(f.mock.calls[0][1].body)).toContain('"percent":0.5');
|
||||
});
|
||||
Reference in New Issue
Block a user