Files
makelore/tests/unit/data-service-client.test.ts

323 lines
12 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import {
createDataServiceOperations,
DataServiceCloudClient,
} from '@electron/services/data-service-client';
const projectId = '11111111-1111-4111-8111-111111111111';
function jsonResponse(value: unknown, init: ResponseInit = {}): Response {
const headers = new Headers(init.headers);
headers.set('content-type', 'application/json');
return new Response(JSON.stringify(value), { ...init, headers });
}
function instance(project = projectId) {
return {
instance_id: '22222222-2222-4222-8222-222222222222',
project_id: project,
collections: [],
usage: { document_count: 0, total_bytes: 0 },
limits: {
max_collections: 20,
max_documents: 5000,
max_total_bytes: 20971520,
max_document_bytes: 65536,
list_default_limit: 50,
list_max_limit: 100,
list_max_data_bytes: 1048576,
mutations_per_minute: 120,
},
created_at: '2026-08-26T08:00:00Z',
updated_at: '2026-08-26T08:00:00Z',
};
}
function document() {
return {
id: 'todo-1',
data: { title: 'Ship P0', done: false },
revision: 7,
created_at: '2026-08-26T08:00:00Z',
updated_at: '2026-08-26T08:00:00Z',
};
}
describe('DataServiceCloudClient', () => {
it('projects the control-plane route and direct DTO', async () => {
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
jsonResponse(instance(), { status: 200 }),
);
const client = new DataServiceCloudClient({
fetchImpl,
getAccessToken: vi.fn().mockResolvedValue('access-token'),
apiBaseUrl: 'https://square.example/',
});
const result = await client.configure(projectId, ['todos']);
expect(result).toMatchObject({ success: true, status: 200, data: instance() });
expect(fetchImpl).toHaveBeenCalledWith(
`https://square.example/api/data-service/v1/projects/${projectId}`,
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({ collections: ['todos'] }),
headers: expect.objectContaining({
Accept: 'application/json',
Authorization: 'Bearer access-token',
'Content-Type': 'application/json',
}),
}),
);
});
it('refreshes and replays exactly once after an authoritative 401', async () => {
const fetchImpl = vi.fn<typeof fetch>()
.mockResolvedValueOnce(new Response(null, { status: 401 }))
.mockResolvedValueOnce(jsonResponse(instance(), { status: 200 }));
const getAccessToken = vi.fn(async (options?: { forceRefresh?: boolean }) => (
options?.forceRefresh ? 'refreshed-token' : 'stale-token'
));
const client = new DataServiceCloudClient({ fetchImpl, getAccessToken });
const result = await client.inspect(projectId);
expect(result.success).toBe(true);
expect(fetchImpl).toHaveBeenCalledTimes(2);
expect(getAccessToken).toHaveBeenNthCalledWith(1, { fetchImpl });
expect(getAccessToken).toHaveBeenNthCalledWith(2, { fetchImpl, forceRefresh: true });
expect((fetchImpl.mock.calls[1][1] as RequestInit).headers).toEqual(
expect.objectContaining({ Authorization: 'Bearer refreshed-token' }),
);
});
it('does not replay an ambiguous transport failure', async () => {
const fetchImpl = vi.fn<typeof fetch>().mockRejectedValue(new Error('socket closed'));
const getAccessToken = vi.fn().mockResolvedValue('access-token');
const client = new DataServiceCloudClient({ fetchImpl, getAccessToken });
const result = await client.inspect(projectId);
expect(result).toMatchObject({
success: false,
status: 503,
code: 'data_service_unavailable',
retryable: true,
data: null,
});
expect(fetchImpl).toHaveBeenCalledTimes(1);
expect(getAccessToken).toHaveBeenCalledTimes(1);
});
it('keeps error fields safe while preserving accepted code and context', async () => {
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
jsonResponse({
detail: {
code: 'quota_exceeded',
message: 'owner secret must not cross the Host boundary',
retryable: false,
context: {
resource: 'bytes',
limit: 20971520,
current: 20900000,
attempted: 80000,
owner_user_id: 'private',
},
},
}, { status: 409 }),
);
const client = new DataServiceCloudClient({
fetchImpl,
getAccessToken: vi.fn().mockResolvedValue('access-token'),
});
const result = await client.putDocument(projectId, {
collection: 'todos',
document_id: 'todo-1',
data: { done: false },
});
expect(result).toEqual({
success: false,
status: 409,
code: 'quota_exceeded',
error: 'Data Service quota exceeded',
retryable: false,
context: { resource: 'bytes', limit: 20971520, current: 20900000, attempted: 80000 },
data: null,
});
});
it('normalizes an invalid success DTO', async () => {
const client = new DataServiceCloudClient({
fetchImpl: vi.fn<typeof fetch>().mockResolvedValue(jsonResponse({ items: [] })),
getAccessToken: vi.fn().mockResolvedValue('access-token'),
});
const result = await client.listProjects();
expect(result).toEqual({
success: false,
status: 502,
code: 'upstream_invalid_response',
error: 'Data Service returned an invalid response',
retryable: false,
data: null,
});
});
it('rejects a control-plane response for a different project', async () => {
const client = new DataServiceCloudClient({
fetchImpl: vi.fn<typeof fetch>().mockResolvedValue(
jsonResponse(instance('33333333-3333-4333-8333-333333333333'), { status: 200 }),
),
getAccessToken: vi.fn().mockResolvedValue('access-token'),
});
const result = await client.inspect(projectId);
expect(result).toMatchObject({
success: false,
status: 502,
code: 'upstream_invalid_response',
data: null,
});
});
it('rejects malformed known error fields', async () => {
const client = new DataServiceCloudClient({
fetchImpl: vi.fn<typeof fetch>().mockResolvedValue(
jsonResponse({ detail: { code: 'quota_exceeded', retryable: 'no' } }, { status: 409 }),
),
getAccessToken: vi.fn().mockResolvedValue('access-token'),
});
const result = await client.inspect(projectId);
expect(result).toMatchObject({
success: false,
status: 502,
code: 'upstream_invalid_response',
data: null,
});
});
it('rejects a known error code paired with the wrong HTTP status', async () => {
const client = new DataServiceCloudClient({
fetchImpl: vi.fn<typeof fetch>().mockResolvedValue(
jsonResponse({ detail: { code: 'quota_exceeded' } }, { status: 400 }),
),
getAccessToken: vi.fn().mockResolvedValue('access-token'),
});
const result = await client.inspect(projectId);
expect(result).toMatchObject({
success: false,
status: 502,
code: 'upstream_invalid_response',
data: null,
});
});
it('normalizes malformed client errors and all upstream 5xx responses safely', async () => {
const fetchImpl = vi.fn<typeof fetch>()
.mockResolvedValueOnce(new Response(null, { status: 404 }))
.mockResolvedValueOnce(new Response('private upstream exception', { status: 500 }));
const client = new DataServiceCloudClient({
fetchImpl,
getAccessToken: vi.fn().mockResolvedValue('access-token'),
});
const malformed = await client.inspect(projectId);
const unavailable = await client.inspect(projectId);
expect(malformed).toMatchObject({ success: false, status: 502, code: 'upstream_invalid_response', data: null });
expect(unavailable).toMatchObject({
success: false,
status: 503,
code: 'data_service_unavailable',
retryable: true,
data: null,
});
});
it('projects data-plane paths, query parameters, and strong revision headers exactly', async () => {
const fetchImpl = vi.fn<typeof fetch>()
.mockResolvedValueOnce(jsonResponse(document(), { status: 200 }))
.mockResolvedValueOnce(jsonResponse({ items: [document()], next_cursor: null, limit: 50 }, { status: 200 }))
.mockResolvedValueOnce(jsonResponse(document(), { status: 200 }))
.mockResolvedValueOnce(new Response(null, { status: 204 }));
const client = new DataServiceCloudClient({
fetchImpl,
getAccessToken: vi.fn().mockResolvedValue('access-token'),
apiBaseUrl: 'https://square.example',
});
await client.getDocument(projectId, 'todos', 'todo-1');
await client.listDocuments(projectId, 'todos', 50, 'cursor/next');
await client.putDocument(projectId, {
collection: 'todos',
document_id: 'todo-1',
data: document().data,
if_revision: 7,
});
await client.deleteDocument(projectId, { collection: 'todos', document_id: 'todo-1', if_revision: 7 });
expect(fetchImpl.mock.calls.map(([url]) => url)).toEqual([
`https://square.example/api/data/v1/projects/${projectId}/collections/todos/documents/todo-1`,
`https://square.example/api/data/v1/projects/${projectId}/collections/todos/documents?limit=50&cursor=cursor%2Fnext`,
`https://square.example/api/data/v1/projects/${projectId}/collections/todos/documents/todo-1`,
`https://square.example/api/data/v1/projects/${projectId}/collections/todos/documents/todo-1`,
]);
expect((fetchImpl.mock.calls[2][1] as RequestInit).headers).toEqual(
expect.objectContaining({ 'If-Match': '"7"' }),
);
expect((fetchImpl.mock.calls[3][1] as RequestInit).headers).toEqual(
expect.objectContaining({ 'If-Match': '"7"' }),
);
});
it('preserves opaque cursor text when projecting the query', async () => {
const listDocuments = vi.fn<typeof fetch>().mockResolvedValue(
jsonResponse({ items: [], next_cursor: null, limit: 50 }, { status: 200 }),
);
const client = new DataServiceCloudClient({
fetchImpl: listDocuments,
getAccessToken: vi.fn().mockResolvedValue('access-token'),
});
const result = await client.listDocuments(projectId, 'todos', 50, ' cursor ');
expect(result).toMatchObject({ success: true, status: 200 });
expect(listDocuments.mock.calls[0][0]).toContain('cursor=+cursor+');
});
});
describe('DataServiceOperations', () => {
it('derives active project identity for every operation except owner-wide listing', async () => {
const client = {
listProjects: vi.fn().mockResolvedValue({ success: true, status: 200, code: null, error: null, retryable: false, data: { items: [], total: 0, instance_limit: 20 } }),
inspect: vi.fn().mockResolvedValue({ success: true, status: 200, code: null, error: null, retryable: false, data: instance() }),
} as unknown as DataServiceCloudClient;
const requireActiveRealProjectWithIdentity = vi.fn().mockResolvedValue({
project: {},
path: 'C:\\projects\\active',
projectId,
});
const operations = createDataServiceOperations({
projects: { requireActiveRealProjectWithIdentity },
client,
});
await operations.listProjects();
expect(requireActiveRealProjectWithIdentity).not.toHaveBeenCalled();
const unconfirmed = await operations.removeProject({ confirmed: false as true });
expect(unconfirmed).toMatchObject({ success: false, status: 400, code: 'confirmation_required', data: null });
expect(requireActiveRealProjectWithIdentity).not.toHaveBeenCalled();
await operations.inspect();
expect(requireActiveRealProjectWithIdentity).toHaveBeenCalledTimes(1);
expect(client.inspect).toHaveBeenCalledWith(projectId);
});
});