Files

83 lines
4.5 KiB
JavaScript

import {BackendClient} from '../../../src/lib/sccn/api';
const response = (data, status = 200) => ({ok: status < 400, status, json: async () => data});
describe('ScCN backend adapter', () => {
let client;
beforeEach(() => {
client = new BackendClient();
global.fetch = jest.fn();
});
afterEach(() => { delete global.fetch; });
test('uploads even clean builtin assets once per server session', async () => {
fetch.mockResolvedValue(response({status: 'ok'}));
const asset = {assetId: 'abc', dataFormat: 'svg', data: new Uint8Array([1, 2]), clean: true};
await client.upload(asset);
await client.upload(asset);
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch.mock.calls[0][1]).toMatchObject({credentials: 'include', method: 'POST', body: asset.data});
});
test('failed uploads remain retryable', async () => {
fetch.mockResolvedValueOnce(response({code: 40101}, 401)).mockResolvedValue(response({status: 'ok'}));
const asset = {assetId: 'abc', dataFormat: 'svg', data: new Uint8Array([1])};
await expect(client.upload(asset)).rejects.toThrow('登录');
await client.upload(asset);
expect(fetch).toHaveBeenCalledTimes(2);
});
test('creates a project with raw JSON and updates with the loaded revision', async () => {
fetch.mockResolvedValueOnce(response({'content-name': '12', id: 12, revision: 1}))
.mockResolvedValueOnce(response({id: 12, revision: 2}));
await client.save(null, '{"targets":[]}', {title: '标题'});
await client.save('12', '{"targets":[]}', {title: '新标题'});
expect(fetch.mock.calls[0][0]).toContain('/editor/projects/?title=');
expect(fetch.mock.calls[0][1].body).toBe('{"targets":[]}');
expect(fetch.mock.calls[1][0]).toContain('revision=1');
expect(client.revisions.get('12')).toBe(2);
});
test('does not overwrite the loaded revision when refreshing permissions', async () => {
fetch.mockResolvedValueOnce(response({data: {revision: 4, canSave: true}}))
.mockResolvedValueOnce(response({data: {revision: 9, canSave: true}}))
.mockResolvedValueOnce(response({code: 40902}, 409));
await client.project('12');
await client.permissions('12');
expect(client.revisions.get('12')).toBe(4);
await expect(client.save('12', '{}')).rejects.toThrow('其他窗口');
expect(client.revisions.get('12')).toBe(4);
expect(fetch.mock.calls[2][0]).toContain('revision=4');
});
test('copy and remix create new IDs without a revision precondition', async () => {
fetch.mockResolvedValue(response({id: 13, revision: 1}));
await client.save(null, '{}', {originalId: 12, isCopy: 1});
expect(fetch.mock.calls[0][0]).toContain('original_id=12&is_copy=1');
expect(fetch.mock.calls[0][0]).not.toContain('revision=');
await client.save(null, '{}', {originalId: 12, isRemix: 1});
expect(fetch.mock.calls[1][0]).toContain('original_id=12&is_remix=1');
});
test('refuses updates without a loaded version', async () => {
await expect(client.save('12', '{}')).rejects.toThrow('版本');
expect(fetch).not.toHaveBeenCalled();
});
test('switching accounts clears cached ownership', async () => {
client.state = {...client.state, user: {id: 1}, projects: {12: {canSave: true}}};
fetch.mockResolvedValue(response({data: {user: {id: 2}}}));
await client.session();
expect(client.state.projects).toEqual({});
expect(client.state.user.id).toBe(2);
});
test('logout clears ownership and uses the HttpOnly cookie', async () => {
client.state = {...client.state, user: {id: 1}, projects: {12: {canSave: true}}};
fetch.mockResolvedValue(response({code: 0}));
await client.logout();
expect(client.state.user).toBe(null);
expect(client.state.projects).toEqual({});
expect(fetch.mock.calls[0][1]).toMatchObject({credentials: 'include', method: 'POST'});
});
test('handles network and non-JSON responses without claiming a successful save', async () => {
fetch.mockRejectedValueOnce(new Error('network'));
await expect(client.save(null, '{}')).rejects.toThrow('无法连接服务器');
fetch.mockResolvedValueOnce({ok: true, status: 200, json: async () => { throw new Error('html'); }});
await expect(client.save(null, '{}')).rejects.toThrow('无效响应');
});
});