mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
88 lines
2.9 KiB
TypeScript
88 lines
2.9 KiB
TypeScript
import { Buffer } from "node:buffer";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { getActivitySyncWatermark, uploadSessionAnalytics } from "../src/core/activity-sync/api.ts";
|
|
import type { PiDevApiError } from "../src/core/pi-dev/http.ts";
|
|
|
|
function jsonResponse(body: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
describe("activity sync api", () => {
|
|
it("reads watermarks", async () => {
|
|
vi.stubEnv("PI_DEV_URL", "https://example.test");
|
|
const urls: string[] = [];
|
|
const fetchMock: typeof fetch = async (input, init) => {
|
|
const request = new Request(input, init);
|
|
urls.push(request.url);
|
|
expect(request.headers.get("Authorization")).toBe("Bearer access-2");
|
|
return jsonResponse({ ok: true, watermark: "2026-01-01T00:00:00.000Z" });
|
|
};
|
|
|
|
const watermark = await getActivitySyncWatermark("access-2", "device-1", {
|
|
fetch: fetchMock,
|
|
});
|
|
|
|
expect(watermark.watermark).toBe("2026-01-01T00:00:00.000Z");
|
|
expect(urls).toEqual(["https://example.test/analytics/activity/device-1"]);
|
|
});
|
|
|
|
it("uploads compressed NDJSON with sync headers and surfaces API errors", async () => {
|
|
vi.stubEnv("PI_DEV_URL", "https://example.test");
|
|
let request: Request | undefined;
|
|
const fetchMock: typeof fetch = async (input, init) => {
|
|
request = new Request(input, init);
|
|
return jsonResponse(
|
|
{
|
|
ok: true,
|
|
accepted: true,
|
|
received_bytes: 10,
|
|
watermark: "2026-01-02T00:00:00.000Z",
|
|
},
|
|
202,
|
|
);
|
|
};
|
|
|
|
const response = await uploadSessionAnalytics({
|
|
fetch: fetchMock,
|
|
accessToken: "access-1",
|
|
deviceId: "device-1",
|
|
watermark: "2026-01-02T00:00:00.000Z",
|
|
idempotencyKey: "retry-key",
|
|
body: Buffer.from("payload"),
|
|
contentEncoding: "zstd",
|
|
});
|
|
|
|
expect(response.accepted).toBe(true);
|
|
expect(request?.headers.get("Authorization")).toBe("Bearer access-1");
|
|
expect(request?.headers.get("Content-Type")).toBe("application/x-ndjson");
|
|
expect(request?.headers.get("Content-Encoding")).toBe("zstd");
|
|
expect(request?.headers.get("Pi-Sync-Watermark")).toBe("2026-01-02T00:00:00.000Z");
|
|
expect(request?.headers.get("Idempotency-Key")).toBe("retry-key");
|
|
|
|
const failingFetch: typeof fetch = async () =>
|
|
jsonResponse({ ok: false, error: "invalid_payload", description: "bad line" }, 400);
|
|
await expect(
|
|
uploadSessionAnalytics({
|
|
fetch: failingFetch,
|
|
accessToken: "access-1",
|
|
deviceId: "device-1",
|
|
watermark: "2026-01-02T00:00:00.000Z",
|
|
idempotencyKey: "retry-key",
|
|
body: Buffer.from("payload"),
|
|
contentEncoding: "zstd",
|
|
}),
|
|
).rejects.toMatchObject({
|
|
name: "PiDevApiError",
|
|
status: 400,
|
|
errorCode: "invalid_payload",
|
|
description: "bad line",
|
|
operation: "POST /analytics/activity/:deviceId",
|
|
message: "POST /analytics/activity/:deviceId failed: invalid_payload: bad line",
|
|
} satisfies Partial<PiDevApiError>);
|
|
});
|
|
});
|