实现项目真机预览与待审版本扫码验收
需求:接管客户端未提交 WIP,保留真机预览,移除旧登录原型并维持 2.0.0。 实现:由 Main 核对项目与精确 Release,签发短时 Owner preview;补充一键提交映射能力及 Windows ZIP 预检兼容。
This commit is contained in:
589
tests/unit/device-preview-routes.test.ts
Normal file
589
tests/unit/device-preview-routes.test.ts
Normal file
@@ -0,0 +1,589 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { handleDevicePreviewRoutes } from '@electron/api/routes/device-preview';
|
||||
import {
|
||||
getRendererCapability,
|
||||
RENDERER_CAPABILITY_HEADER,
|
||||
rotateRendererCapability,
|
||||
} from '@electron/api/renderer-capability';
|
||||
|
||||
const getValidWorksSquareAccessTokenMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@electron/services/works-square-session', () => ({
|
||||
getValidWorksSquareAccessToken: (...args: unknown[]) => getValidWorksSquareAccessTokenMock(...args),
|
||||
}));
|
||||
|
||||
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 Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
|
||||
function createRequest(withRendererCapability = true): IncomingMessage {
|
||||
const req = new EventEmitter();
|
||||
Object.assign(req, {
|
||||
method: 'GET',
|
||||
headers: withRendererCapability
|
||||
? { [RENDERER_CAPABILITY_HEADER]: getRendererCapability() }
|
||||
: {},
|
||||
});
|
||||
return req as IncomingMessage;
|
||||
}
|
||||
|
||||
function createContext(deployment: Record<string, unknown> | null, activeProjectId = 'project-1') {
|
||||
return {
|
||||
opencodeProjectStore: {
|
||||
getActiveProject: vi.fn(async () => ({
|
||||
id: activeProjectId,
|
||||
name: '星球小游戏',
|
||||
path: 'D:/projects/planet-game',
|
||||
})),
|
||||
},
|
||||
worksCloudDeployment: {
|
||||
get: vi.fn(async () => deployment),
|
||||
},
|
||||
} as never;
|
||||
}
|
||||
|
||||
const submittedDeployment = {
|
||||
schema_version: 1,
|
||||
project_id: 'project-1',
|
||||
status: 'submitted',
|
||||
requested_at: '2026-08-03T01:00:00.000Z',
|
||||
updated_at: '2026-08-03T01:05:00.000Z',
|
||||
app_id: 'planet-game',
|
||||
version_id: 'version-7',
|
||||
version_name: 'v0.7.0',
|
||||
review_status: 'building',
|
||||
};
|
||||
|
||||
function matchingRemotePayload(overrides: {
|
||||
project?: Record<string, unknown>;
|
||||
latestVersion?: Record<string, unknown>;
|
||||
} = {}) {
|
||||
return {
|
||||
project: {
|
||||
app_id: 'planet-game',
|
||||
playable: true,
|
||||
runtime_url: '/apps/planet-game/',
|
||||
version_name: 'v0.7.0',
|
||||
...overrides.project,
|
||||
},
|
||||
latest_version: {
|
||||
id: 'version-7',
|
||||
version_name: 'v0.7.0',
|
||||
release_id: 'release-7',
|
||||
review_status: 'approved',
|
||||
...overrides.latestVersion,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('device preview Host API route', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
rotateRendererCapability();
|
||||
getValidWorksSquareAccessTokenMock.mockReset();
|
||||
getValidWorksSquareAccessTokenMock.mockResolvedValue('managed-secret-token');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('requires the Main-injected renderer capability', async () => {
|
||||
const response = createResponse();
|
||||
|
||||
const handled = await handleDevicePreviewRoutes(
|
||||
createRequest(false),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(null),
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(response.statusCode).toBe(403);
|
||||
expect(response.json()).toEqual({ success: false, error: 'Renderer capability required' });
|
||||
});
|
||||
|
||||
it('returns a project-scoped not-deployed snapshot without contacting Works Square', async () => {
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(null),
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toEqual({
|
||||
success: true,
|
||||
preview: {
|
||||
state: 'not_deployed',
|
||||
projectId: 'project-1',
|
||||
message: '当前项目还没有与本机绑定的服务端预览版本。',
|
||||
},
|
||||
});
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns only a trusted absolute HTTPS runtime URL for the submitted version', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValueOnce(new Response(
|
||||
JSON.stringify(matchingRemotePayload()),
|
||||
{ status: 200 },
|
||||
));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(submittedDeployment),
|
||||
);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://square.nianxx.cn/api/projects/mine/planet-game/status',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
headers: { Authorization: 'Bearer managed-secret-token' },
|
||||
signal: expect.any(AbortSignal),
|
||||
}),
|
||||
);
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toEqual({
|
||||
success: true,
|
||||
preview: {
|
||||
state: 'ready',
|
||||
projectId: 'project-1',
|
||||
appId: 'planet-game',
|
||||
versionId: 'version-7',
|
||||
versionName: 'v0.7.0',
|
||||
reviewStatus: 'approved',
|
||||
launchUrl: 'https://square.nianxx.cn/apps/planet-game/',
|
||||
updatedAt: '2026-08-03T01:05:00.000Z',
|
||||
message: '真机预览已就绪。',
|
||||
},
|
||||
});
|
||||
expect(JSON.stringify(response.json())).not.toContain('managed-secret-token');
|
||||
});
|
||||
|
||||
it('creates an exact short-lived owner preview for a pending-review release', async () => {
|
||||
const fetchMock = vi.fn()
|
||||
.mockResolvedValueOnce(new Response(
|
||||
JSON.stringify(matchingRemotePayload({
|
||||
project: { playable: false, runtime_url: null },
|
||||
latestVersion: { review_status: 'pending_review' },
|
||||
})),
|
||||
{ status: 200 },
|
||||
))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({
|
||||
url: '/previews/release-7/signed-ticket/',
|
||||
expires_in_seconds: 300,
|
||||
}), { status: 200 }));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(submittedDeployment),
|
||||
);
|
||||
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'https://square.nianxx.cn/api/projects/planet-game/releases/release-7/preview-url',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { Authorization: 'Bearer managed-secret-token' },
|
||||
signal: expect.any(AbortSignal),
|
||||
}),
|
||||
);
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
success: true,
|
||||
preview: {
|
||||
state: 'ready',
|
||||
reviewStatus: 'pending_review',
|
||||
launchUrl: 'https://square.nianxx.cn/previews/release-7/signed-ticket/',
|
||||
message: '审核前真机预览已就绪。',
|
||||
},
|
||||
});
|
||||
expect(JSON.stringify(response.json())).not.toContain('managed-secret-token');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['a foreign origin', 'https://evil.example/previews/release-7/ticket/'],
|
||||
['another release', '/previews/release-8/ticket/'],
|
||||
])('rejects a pending-review preview for %s', async (_case, previewUrl) => {
|
||||
vi.stubGlobal('fetch', vi.fn()
|
||||
.mockResolvedValueOnce(new Response(
|
||||
JSON.stringify(matchingRemotePayload({
|
||||
project: { playable: false, runtime_url: null },
|
||||
latestVersion: { review_status: 'pending_review' },
|
||||
})),
|
||||
{ status: 200 },
|
||||
))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ url: previewUrl }), { status: 200 })));
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(submittedDeployment),
|
||||
);
|
||||
|
||||
expect(response.json()).toMatchObject({
|
||||
success: true,
|
||||
preview: { state: 'unavailable' },
|
||||
});
|
||||
expect(response.json()).not.toHaveProperty('preview.launchUrl');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['an untrusted origin', 'https://evil.example/steal'],
|
||||
['an overlong URL', `https://square.nianxx.cn/apps/${'x'.repeat(1_100)}`],
|
||||
])('refuses a runtime URL from %s', async (_case, runtimeUrl) => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(new Response(
|
||||
JSON.stringify(matchingRemotePayload({ project: { runtime_url: runtimeUrl } })),
|
||||
{ status: 200 },
|
||||
)));
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(submittedDeployment),
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
success: true,
|
||||
preview: {
|
||||
state: 'unavailable',
|
||||
message: '服务端返回的预览地址未通过安全校验。',
|
||||
},
|
||||
});
|
||||
expect(response.json()).not.toHaveProperty('preview.launchUrl');
|
||||
});
|
||||
|
||||
it('does not expose an older published runtime while the requested version is building', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(new Response(
|
||||
JSON.stringify(matchingRemotePayload({
|
||||
project: { version_name: 'v0.6.0' },
|
||||
latestVersion: { review_status: 'building' },
|
||||
})),
|
||||
{ status: 200 },
|
||||
)));
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(submittedDeployment),
|
||||
);
|
||||
|
||||
expect(response.json()).toMatchObject({
|
||||
success: true,
|
||||
preview: {
|
||||
state: 'building',
|
||||
message: '本次预览版本仍在构建或审核,暂不展示旧版本。',
|
||||
},
|
||||
});
|
||||
expect(response.json()).not.toHaveProperty('preview.launchUrl');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['latest version id is missing', { latestVersion: { id: undefined } }, 'unavailable'],
|
||||
['latest version name is missing', { latestVersion: { version_name: undefined } }, 'unavailable'],
|
||||
['runtime version name is missing', { project: { version_name: undefined } }, 'building'],
|
||||
['latest version id differs', { latestVersion: { id: 'version-6' } }, 'unavailable'],
|
||||
['latest version name differs', { latestVersion: { version_name: 'v0.6.0' } }, 'unavailable'],
|
||||
])('does not expose a runtime when %s', async (_case, overrides, expectedState) => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(new Response(
|
||||
JSON.stringify(matchingRemotePayload(overrides)),
|
||||
{ status: 200 },
|
||||
)));
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(submittedDeployment),
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
success: true,
|
||||
preview: { state: expectedState },
|
||||
});
|
||||
expect(response.json()).not.toHaveProperty('preview.launchUrl');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['building', 'building'],
|
||||
['rejected', 'unavailable'],
|
||||
])('does not expose a runtime with a %s review', async (reviewStatus, expectedState) => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(new Response(
|
||||
JSON.stringify(matchingRemotePayload({
|
||||
latestVersion: { review_status: reviewStatus },
|
||||
})),
|
||||
{ status: 200 },
|
||||
)));
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(submittedDeployment),
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
success: true,
|
||||
preview: { state: expectedState },
|
||||
});
|
||||
expect(response.json()).not.toHaveProperty('preview.launchUrl');
|
||||
});
|
||||
|
||||
it('does not attribute a replacement version rejection to the locally bound version', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(new Response(
|
||||
JSON.stringify(matchingRemotePayload({
|
||||
latestVersion: {
|
||||
id: 'version-8',
|
||||
version_name: 'v0.8.0',
|
||||
review_status: 'rejected',
|
||||
},
|
||||
})),
|
||||
{ status: 200 },
|
||||
)));
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(submittedDeployment),
|
||||
);
|
||||
|
||||
expect(response.json()).toMatchObject({
|
||||
success: true,
|
||||
preview: {
|
||||
state: 'unavailable',
|
||||
message: '服务端最新版本已与本地绑定版本不一致,请重新同步项目状态。',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses a deployment record owned by another local project without contacting Works Square', async () => {
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext({ ...submittedDeployment, project_id: 'project-2' }),
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
success: true,
|
||||
preview: { state: 'unavailable', projectId: 'project-1' },
|
||||
});
|
||||
expect(response.json()).not.toHaveProperty('preview.launchUrl');
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refuses a remote project with a different app id', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(new Response(
|
||||
JSON.stringify(matchingRemotePayload({ project: { app_id: 'other-game' } })),
|
||||
{ status: 200 },
|
||||
)));
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(submittedDeployment),
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
success: true,
|
||||
preview: { state: 'unavailable' },
|
||||
});
|
||||
expect(response.json()).not.toHaveProperty('preview.launchUrl');
|
||||
});
|
||||
|
||||
it('does not treat preview_url as an executable runtime URL', async () => {
|
||||
const previewUrl = 'https://square.nianxx.cn/apps/planet-game/preview';
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(new Response(
|
||||
JSON.stringify(matchingRemotePayload({
|
||||
project: { runtime_url: undefined, preview_url: previewUrl },
|
||||
})),
|
||||
{ status: 200 },
|
||||
)));
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(submittedDeployment),
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
success: true,
|
||||
preview: { state: 'unavailable' },
|
||||
});
|
||||
expect(response.json()).not.toHaveProperty('preview.launchUrl');
|
||||
expect(JSON.stringify(response.json())).not.toContain(previewUrl);
|
||||
});
|
||||
|
||||
it('requires a managed owner session before querying a submitted preview', async () => {
|
||||
getValidWorksSquareAccessTokenMock.mockResolvedValue(null);
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(submittedDeployment),
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(401);
|
||||
expect(response.json()).toEqual({
|
||||
success: false,
|
||||
error: '请先登录,再核对当前项目的真机预览版本',
|
||||
});
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps polling a submitted version when the owner endpoint has not indexed it yet', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValueOnce(new Response(null, { status: 404 }));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(submittedDeployment),
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
success: true,
|
||||
preview: { state: 'building' },
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('aborts a hanging owner status request instead of retaining a stale preview forever', async () => {
|
||||
vi.useFakeTimers();
|
||||
const fetchMock = vi.fn((_input: unknown, init?: RequestInit) => new Promise<Response>((_resolve, reject) => {
|
||||
init?.signal?.addEventListener('abort', () => {
|
||||
reject(new DOMException('Aborted', 'AbortError'));
|
||||
}, { once: true });
|
||||
}));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const response = createResponse();
|
||||
|
||||
const handled = handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(submittedDeployment),
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
await handled;
|
||||
|
||||
expect(response.statusCode).toBe(502);
|
||||
expect(response.json()).toEqual({
|
||||
success: false,
|
||||
error: '服务端预览状态读取超时,请稍后刷新',
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['an unauthorized owner response', 401, JSON.stringify({ detail: 'Unauthorized' }), 401],
|
||||
['an owner service failure', 500, JSON.stringify({ detail: 'Failed' }), 502],
|
||||
['a malformed owner payload', 200, '<html>not json</html>', 502],
|
||||
])('never falls back to the public project endpoint after %s', async (
|
||||
_case,
|
||||
ownerStatus,
|
||||
ownerBody,
|
||||
expectedStatus,
|
||||
) => {
|
||||
const fetchMock = vi.fn().mockResolvedValueOnce(new Response(ownerBody, { status: ownerStatus }));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(submittedDeployment),
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(expectedStatus);
|
||||
expect(response.json()).toMatchObject({ success: false });
|
||||
expect(response.json()).not.toHaveProperty('preview.launchUrl');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://square.nianxx.cn/api/projects/mine/planet-game/status',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
headers: { Authorization: 'Bearer managed-secret-token' },
|
||||
signal: expect.any(AbortSignal),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a preview request for a project that is not active', async () => {
|
||||
const response = createResponse();
|
||||
|
||||
await handleDevicePreviewRoutes(
|
||||
createRequest(),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
|
||||
createContext(null, 'project-2'),
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(409);
|
||||
expect(response.json()).toEqual({
|
||||
success: false,
|
||||
error: 'Select this project before opening device preview',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user