gpt-image-playground deploy
Deploy 17eadcc to Docker Space
96bdf6c
Raw
History Blame Contribute Delete
9.49 kB
import { GET, POST } from './route';
import { createAccessToken } from '@/lib/server-runtime';
import { getWebuiImageRetentionStore, resetWebuiImageRetentionStoresForTests } from '@/lib/webui-image-retention-store';
import { NextRequest } from 'next/server';
import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, it } from 'node:test';
const PAGE_PASSWORD_FIXTURE = ['customer', 'access', 'code'].join('-');
const validFilename = '1781567999000-aaaaaaaaaaaaaaaa-0.png';
const missingFilename = '1781567999001-bbbbbbbbbbbbbbbb-1.webp';
const symlinkFilename = '1781567999002-cccccccccccccccc-2.png';
let originalEnv: NodeJS.ProcessEnv;
let originalCwd = '';
let tempDir = '';
beforeEach(async () => {
originalEnv = { ...process.env };
originalCwd = process.cwd();
tempDir = await mkdtemp(path.join(os.tmpdir(), 'image-retention-route-'));
process.chdir(tempDir);
delete process.env.APP_PASSWORD;
resetWebuiImageRetentionStoresForTests();
});
afterEach(async () => {
resetWebuiImageRetentionStoresForTests();
process.chdir(originalCwd);
await rm(tempDir, { recursive: true, force: true });
restoreProcessEnv(originalEnv);
});
describe('GET and POST /api/image-retention', { concurrency: false }, () => {
it('preserves valid top-level files in one batch and reports invalid files', async () => {
await writeOutputFile(validFilename);
const response = await POST(
jsonRequest({
action: 'preserve',
filenames: [validFilename, '../outside.png', missingFilename]
})
);
const body = (await response.json()) as {
results: Array<{ filename: string; success: boolean; error?: string }>;
};
assert.equal(response.status, 207);
assert.deepEqual(body.results, [
{ filename: validFilename, success: true },
{ filename: '../outside.png', success: false, error: '文件名格式无效。' },
{ filename: missingFilename, success: false, error: '文件不存在。' }
]);
const store = await getWebuiImageRetentionStore();
assert.deepEqual(await store.listPermanentFilenames(), [validFilename]);
const listResponse = await GET(new NextRequest('http://localhost/api/image-retention'));
assert.equal(listResponse.status, 200);
assert.deepEqual(await listResponse.json(), { filenames: [validFilename] });
});
it('rejects preserve requests for symbolic links', async () => {
const outputDir = await outputDirectory();
const targetPath = path.join(tempDir, 'outside.png');
await writeFile(targetPath, 'outside');
await symlink(targetPath, path.join(outputDir, symlinkFilename));
const response = await POST(
jsonRequest({
action: 'preserve',
filenames: [symlinkFilename]
})
);
const body = (await response.json()) as {
results: Array<{ filename: string; success: boolean; error?: string }>;
};
assert.equal(response.status, 207);
assert.deepEqual(body.results, [{ filename: symlinkFilename, success: false, error: '文件必须是常规文件。' }]);
});
it('releases stale markers without requiring the source file to exist', async () => {
const store = await getWebuiImageRetentionStore();
await store.preserve([missingFilename]);
const response = await POST(
jsonRequest({
action: 'release',
filenames: [missingFilename]
})
);
const body = (await response.json()) as {
results: Array<{ filename: string; success: boolean; error?: string }>;
};
assert.equal(response.status, 200);
assert.deepEqual(body.results, [{ filename: missingFilename, success: true }]);
assert.deepEqual(await store.listPermanentFilenames(), []);
});
it('rejects malformed or oversized retention batches before writing state', async () => {
const malformed = await POST(jsonRequest({ action: 'preserve', filenames: [1] }));
assert.equal(malformed.status, 400);
const invalidAction = await POST(jsonRequest({ action: 'delete', filenames: [validFilename] }));
assert.equal(invalidAction.status, 400);
const oversized = await POST(
jsonRequest({
action: 'preserve',
filenames: Array.from(
{ length: 101 },
(_, index) => `1781567999${String(index).padStart(3, '0')}-aaaaaaaaaaaaaaaa-0.png`
)
})
);
assert.equal(oversized.status, 400);
const store = await getWebuiImageRetentionStore();
assert.deepEqual(await store.listPermanentFilenames(), []);
});
it('deduplicates repeated filenames before preserving them', async () => {
await writeOutputFile(validFilename);
const response = await POST(
jsonRequest({
action: 'preserve',
filenames: [validFilename, validFilename]
})
);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), {
results: [{ filename: validFilename, success: true }]
});
assert.deepEqual(await (await getWebuiImageRetentionStore()).listPermanentFilenames(), [validFilename]);
});
it('serializes a release behind an in-flight preserve for the same filename', { timeout: 5_000 }, async () => {
await writeOutputFile(validFilename);
const store = await getWebuiImageRetentionStore();
const originalPreserve = store.preserve.bind(store);
let releasePreserve: (() => void) | undefined;
const preserveGate = new Promise<void>((resolve) => {
releasePreserve = resolve;
});
let markPreserveEntered: (() => void) | undefined;
const preserveEntered = new Promise<void>((resolve) => {
markPreserveEntered = resolve;
});
store.preserve = async (filenames, now) => {
markPreserveEntered?.();
await preserveGate;
await originalPreserve(filenames, now);
};
try {
const preserve = POST(jsonRequest({ action: 'preserve', filenames: [validFilename] }));
await preserveEntered;
const release = POST(jsonRequest({ action: 'release', filenames: [validFilename] }));
releasePreserve?.();
assert.equal((await preserve).status, 200);
assert.equal((await release).status, 200);
assert.deepEqual(await store.listPermanentFilenames(), []);
} finally {
releasePreserve?.();
store.preserve = originalPreserve;
}
});
it('requires a valid password hash for POST and an access cookie for GET', async () => {
await writeOutputFile(validFilename);
process.env.APP_PASSWORD = PAGE_PASSWORD_FIXTURE;
const missingHash = await POST(
jsonRequest({
action: 'preserve',
filenames: [validFilename]
})
);
assert.equal(missingHash.status, 401);
const invalidHash = await POST(
jsonRequest({
action: 'preserve',
filenames: [validFilename],
passwordHash: '0'.repeat(64)
})
);
assert.equal(invalidHash.status, 401);
const authorizedPost = await POST(
jsonRequest({
action: 'preserve',
filenames: [validFilename],
passwordHash: sha256(PAGE_PASSWORD_FIXTURE)
})
);
assert.equal(authorizedPost.status, 200);
const missingCookie = await GET(new NextRequest('http://localhost/api/image-retention'));
assert.equal(missingCookie.status, 401);
const accessCookie = createAccessToken(PAGE_PASSWORD_FIXTURE);
const authorizedGet = await GET(
new NextRequest('http://localhost/api/image-retention', {
headers: { Cookie: `gptImageAccess=${accessCookie}` }
})
);
assert.equal(authorizedGet.status, 200);
});
});
function jsonRequest(body: unknown): NextRequest {
return new NextRequest('http://localhost/api/image-retention', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
}
async function outputDirectory(): Promise<string> {
const directory = path.join(tempDir, 'generated-images');
await mkdir(directory, { recursive: true });
return directory;
}
async function writeOutputFile(filename: string): Promise<void> {
await writeFile(path.join(await outputDirectory(), filename), 'image');
}
function restoreProcessEnv(snapshot: NodeJS.ProcessEnv): void {
for (const key of Object.keys(process.env)) {
if (!(key in snapshot)) delete process.env[key];
}
for (const [key, value] of Object.entries(snapshot)) {
process.env[key] = value;
}
}
function sha256(value: string): string {
return crypto.createHash('sha256').update(value).digest('hex');
}