240 lines
7.2 KiB
TypeScript
240 lines
7.2 KiB
TypeScript
import { EventEmitter } from 'node:events';
|
|
import type { IncomingMessage, ServerResponse } from 'http';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { handleUserSyncRoutes } from '@electron/api/routes/user-sync';
|
|
|
|
const fetchMock = vi.hoisted(() => vi.fn());
|
|
const providerServiceMock = vi.hoisted(() => ({
|
|
upsertSyncedAccountMetadata: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@electron/utils/proxy-fetch', () => ({
|
|
proxyAwareFetch: (...args: unknown[]) => fetchMock(...args),
|
|
}));
|
|
|
|
vi.mock('@electron/services/providers/provider-service', () => ({
|
|
getProviderService: () => providerServiceMock,
|
|
}));
|
|
|
|
function createResponse() {
|
|
const chunks: string[] = [];
|
|
const res = {
|
|
statusCode: 0,
|
|
setHeader: vi.fn(),
|
|
end: vi.fn((chunk?: string) => {
|
|
if (chunk) chunks.push(chunk);
|
|
}),
|
|
} as unknown as ServerResponse;
|
|
|
|
return {
|
|
res,
|
|
get statusCode() {
|
|
return res.statusCode;
|
|
},
|
|
json: () => JSON.parse(chunks.join('')) as unknown,
|
|
};
|
|
}
|
|
|
|
function createRequest(
|
|
method: string,
|
|
body?: unknown,
|
|
headers: Record<string, string> = {},
|
|
): IncomingMessage {
|
|
const req = new EventEmitter();
|
|
Object.assign(req, {
|
|
method,
|
|
headers: body === undefined
|
|
? headers
|
|
: { ...headers, 'content-type': 'application/json' },
|
|
[Symbol.asyncIterator]: async function* () {
|
|
if (body !== undefined) {
|
|
yield Buffer.from(JSON.stringify(body));
|
|
}
|
|
},
|
|
});
|
|
return req as IncomingMessage;
|
|
}
|
|
|
|
describe('user sync host api routes', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.restoreAllMocks();
|
|
providerServiceMock.upsertSyncedAccountMetadata.mockResolvedValue(undefined);
|
|
});
|
|
|
|
it('requires a current user access token', async () => {
|
|
const response = createResponse();
|
|
|
|
const handled = await handleUserSyncRoutes(
|
|
createRequest('GET'),
|
|
response.res,
|
|
new URL('http://127.0.0.1/api/user-sync/bootstrap'),
|
|
{} as never,
|
|
);
|
|
|
|
expect(handled).toBe(true);
|
|
expect(response.statusCode).toBe(400);
|
|
expect(response.json()).toEqual({
|
|
success: false,
|
|
error: 'Missing x-niancode-access-token',
|
|
});
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('fetches bootstrap, applies provider metadata, and strips secrets from the response', async () => {
|
|
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({
|
|
preferences: {
|
|
theme: 'dark',
|
|
sidebar_collapsed: true,
|
|
},
|
|
provider_accounts: [{
|
|
id: 'openai-account-1',
|
|
vendor_id: 'openai',
|
|
label: 'OpenAI',
|
|
auth_mode: 'api_key',
|
|
model: 'gpt-4.1-mini',
|
|
fallback_models: ['gpt-4o-mini'],
|
|
enabled: true,
|
|
is_default: true,
|
|
api_key: 'sk-should-not-return',
|
|
refresh_token: 'refresh-should-not-return',
|
|
headers: { Authorization: 'Bearer secret' },
|
|
updated_at: '2026-07-04T00:00:00.000Z',
|
|
}],
|
|
partners: [],
|
|
usage_aggregates: [],
|
|
}), { status: 200 }));
|
|
const response = createResponse();
|
|
|
|
const handled = await handleUserSyncRoutes(
|
|
createRequest('GET', undefined, { 'x-niancode-access-token': 'access-token' }),
|
|
response.res,
|
|
new URL('http://127.0.0.1/api/user-sync/bootstrap'),
|
|
{} as never,
|
|
);
|
|
|
|
expect(handled).toBe(true);
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.json()).toEqual({
|
|
success: true,
|
|
bootstrap: {
|
|
preferences: {
|
|
theme: 'dark',
|
|
sidebarCollapsed: true,
|
|
},
|
|
providerAccounts: [{
|
|
id: 'openai-account-1',
|
|
vendorId: 'openai',
|
|
label: 'OpenAI',
|
|
authMode: 'api_key',
|
|
model: 'gpt-4.1-mini',
|
|
fallbackModels: ['gpt-4o-mini'],
|
|
enabled: true,
|
|
isDefault: true,
|
|
metadata: {},
|
|
updatedAt: '2026-07-04T00:00:00.000Z',
|
|
}],
|
|
partners: [],
|
|
usageAggregates: [],
|
|
},
|
|
applied: { providerAccounts: 1 },
|
|
});
|
|
expect(JSON.stringify(response.json())).not.toContain('sk-should-not-return');
|
|
expect(JSON.stringify(response.json())).not.toContain('refresh-should-not-return');
|
|
expect(JSON.stringify(response.json())).not.toContain('Authorization');
|
|
expect(fetchMock).toHaveBeenCalledWith('https://square.nianxx.cn/api/me/bootstrap', {
|
|
method: 'GET',
|
|
headers: { Authorization: 'Bearer access-token' },
|
|
});
|
|
expect(providerServiceMock.upsertSyncedAccountMetadata).toHaveBeenCalledWith({
|
|
id: 'openai-account-1',
|
|
vendorId: 'openai',
|
|
label: 'OpenAI',
|
|
authMode: 'api_key',
|
|
model: 'gpt-4.1-mini',
|
|
fallbackModels: ['gpt-4o-mini'],
|
|
enabled: true,
|
|
isDefault: true,
|
|
metadata: {},
|
|
updatedAt: '2026-07-04T00:00:00.000Z',
|
|
});
|
|
});
|
|
|
|
it('proxies preferences as sanitized server payloads', async () => {
|
|
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({
|
|
preferences: { theme: 'dark' },
|
|
provider_accounts: [],
|
|
partners: [],
|
|
usage_aggregates: [],
|
|
}), { status: 200 }));
|
|
const response = createResponse();
|
|
|
|
const handled = await handleUserSyncRoutes(
|
|
createRequest('PUT', {
|
|
theme: 'dark',
|
|
language: 'zh-CN',
|
|
launchAtStartup: true,
|
|
activeProjectId: 'prj_1',
|
|
}, { 'x-niancode-access-token': 'access-token' }),
|
|
response.res,
|
|
new URL('http://127.0.0.1/api/user-sync/preferences'),
|
|
{} as never,
|
|
);
|
|
|
|
expect(handled).toBe(true);
|
|
expect(response.statusCode).toBe(200);
|
|
expect(fetchMock).toHaveBeenCalledWith('https://square.nianxx.cn/api/me/preferences', {
|
|
method: 'PUT',
|
|
headers: {
|
|
Authorization: 'Bearer access-token',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ theme: 'dark', language: 'zh-CN' }),
|
|
});
|
|
});
|
|
|
|
it('proxies partners as sanitized server payloads', async () => {
|
|
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({
|
|
preferences: {},
|
|
provider_accounts: [],
|
|
partners: [],
|
|
usage_aggregates: [],
|
|
}), { status: 200 }));
|
|
const response = createResponse();
|
|
|
|
const handled = await handleUserSyncRoutes(
|
|
createRequest('POST', {
|
|
id: 'makelore-pm-test',
|
|
displayName: 'PM',
|
|
templateRoleId: 'custom',
|
|
skillIds: ['pm-project-plan'],
|
|
filePath: 'D:/repo/.opencode/agent/makelore-pm-test.md',
|
|
projectRuntimeConfigsByProjectId: { prj_1: { filePath: 'D:/repo/file.md' } },
|
|
createdAt: '2026-07-04T00:00:00.000Z',
|
|
updatedAt: '2026-07-04T00:00:00.000Z',
|
|
}, { 'x-niancode-access-token': 'access-token' }),
|
|
response.res,
|
|
new URL('http://127.0.0.1/api/user-sync/partners'),
|
|
{} as never,
|
|
);
|
|
|
|
expect(handled).toBe(true);
|
|
expect(response.statusCode).toBe(200);
|
|
expect(fetchMock).toHaveBeenCalledWith('https://square.nianxx.cn/api/me/partners', {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: 'Bearer access-token',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
id: 'makelore-pm-test',
|
|
display_name: 'PM',
|
|
template_role_id: 'custom',
|
|
skill_ids: ['pm-project-plan'],
|
|
created_at: '2026-07-04T00:00:00.000Z',
|
|
updated_at: '2026-07-04T00:00:00.000Z',
|
|
}),
|
|
});
|
|
});
|
|
});
|