647 lines
21 KiB
TypeScript
647 lines
21 KiB
TypeScript
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',
|
|
})),
|
|
},
|
|
worksSubmissionBinding: {
|
|
get: vi.fn(async () => deployment),
|
|
},
|
|
} as never;
|
|
}
|
|
|
|
const submittedDeployment = {
|
|
schema_version: 2,
|
|
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,
|
|
play_url: '/apps/planet-game/',
|
|
runtime_url: '/apps/legacy-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('explains that a legacy automatic deployment must be resubmitted', 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({
|
|
schema_version: 2,
|
|
project_id: 'project-1',
|
|
status: 'legacy_retired',
|
|
requested_at: '2026-08-01T00:00:00.000Z',
|
|
updated_at: '2026-08-10T00:00:00.000Z',
|
|
error_code: 'LEGACY_AUTO_DEPLOY_RETIRED',
|
|
message: '旧版自动部署任务已停用,请在项目配置中点击“提交审核”重新提交。',
|
|
}),
|
|
);
|
|
|
|
expect(response.json()).toEqual({
|
|
success: true,
|
|
preview: {
|
|
state: 'unavailable',
|
|
projectId: 'project-1',
|
|
updatedAt: '2026-08-10T00:00:00.000Z',
|
|
message: '旧版自动部署任务已停用,请在项目配置中点击“提交审核”重新提交。',
|
|
},
|
|
});
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('prefers the trusted static play URL over the legacy runtime alias', 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('keeps the legacy runtime alias as a one-release fallback', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(new Response(
|
|
JSON.stringify(matchingRemotePayload({
|
|
project: { play_url: null, runtime_url: '/apps/runtime-fallback/' },
|
|
})),
|
|
{ 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({
|
|
preview: {
|
|
state: 'ready',
|
|
launchUrl: 'https://square.nianxx.cn/apps/runtime-fallback/',
|
|
},
|
|
});
|
|
});
|
|
|
|
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, play_url: null, 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, play_url: null, 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 play URL from %s', async (_case, playUrl) => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(new Response(
|
|
JSON.stringify(matchingRemotePayload({ project: { play_url: playUrl } })),
|
|
{ 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: { play_url: undefined, 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',
|
|
});
|
|
});
|
|
});
|