From d6ca79152c9ead50265149d324d223cd598e85c2 Mon Sep 17 00:00:00 2001 From: Oleg Proskurin Date: Mon, 10 Nov 2025 00:57:06 +0700 Subject: [PATCH] fix: service and tests --- apps/api-service/src/app.ts | 3 +- .../src/services/ImageGenService.ts | 1 + .../src/services/core/GenerationService.ts | 2 +- apps/api-service/src/types/api.ts | 1 + tests/api/01-basic.ts | 62 +++++++++--------- tests/api/02-flows.ts | 52 +++++++-------- tests/api/03-aliases.ts | 60 ++++++++--------- tests/api/config.ts | 2 +- .../api/fixture/{image.png => test-image.png} | Bin tests/api/utils.ts | 10 +-- 10 files changed, 98 insertions(+), 95 deletions(-) rename tests/api/fixture/{image.png => test-image.png} (100%) diff --git a/apps/api-service/src/app.ts b/apps/api-service/src/app.ts index 3d9b371..a5fd4cc 100644 --- a/apps/api-service/src/app.ts +++ b/apps/api-service/src/app.ts @@ -1,6 +1,7 @@ import express, { Application } from 'express'; import cors from 'cors'; import { config } from 'dotenv'; +import { randomUUID } from 'crypto'; import { Config } from './types/api'; import { textToImageRouter } from './routes/textToImage'; import { imagesRouter } from './routes/images'; @@ -43,7 +44,7 @@ export const createApp = (): Application => { // Request ID middleware for logging app.use((req, res, next) => { - req.requestId = Math.random().toString(36).substr(2, 9); + req.requestId = randomUUID(); res.setHeader('X-Request-ID', req.requestId); next(); }); diff --git a/apps/api-service/src/services/ImageGenService.ts b/apps/api-service/src/services/ImageGenService.ts index 3c73a91..7c2f3aa 100644 --- a/apps/api-service/src/services/ImageGenService.ts +++ b/apps/api-service/src/services/ImageGenService.ts @@ -78,6 +78,7 @@ export class ImageGenService { filename: uploadResult.filename, filepath: uploadResult.path, url: uploadResult.url, + size: uploadResult.size, model: this.primaryModel, geminiParams, ...(generatedData.description && { diff --git a/apps/api-service/src/services/core/GenerationService.ts b/apps/api-service/src/services/core/GenerationService.ts index 0ef30ff..e23964b 100644 --- a/apps/api-service/src/services/core/GenerationService.ts +++ b/apps/api-service/src/services/core/GenerationService.ts @@ -122,7 +122,7 @@ export class GenerationService { storageKey, storageUrl: genResult.url!, mimeType: 'image/jpeg', - fileSize: 0, // TODO: Get actual file size from storage + fileSize: genResult.size || 0, fileHash, source: 'generated', alias: params.assignAlias || null, diff --git a/apps/api-service/src/types/api.ts b/apps/api-service/src/types/api.ts index 6b4983f..3f48b0c 100644 --- a/apps/api-service/src/types/api.ts +++ b/apps/api-service/src/types/api.ts @@ -94,6 +94,7 @@ export interface ImageGenerationResult { filename?: string; filepath?: string; url?: string; // API URL for accessing the image + size?: number; // File size in bytes description?: string; model: string; geminiParams?: GeminiParams; // Gemini SDK parameters used for generation diff --git a/tests/api/01-basic.ts b/tests/api/01-basic.ts index 6023a09..4538c72 100644 --- a/tests/api/01-basic.ts +++ b/tests/api/01-basic.ts @@ -22,39 +22,39 @@ async function main() { description: 'Test logo image', }); - if (!response.image || !response.image.id) { + if (!response || !response.id) { throw new Error('No image returned'); } - testContext.uploadedImageId = response.image.id; - log.detail('Image ID', response.image.id); - log.detail('Storage Key', response.image.storageKey); - log.detail('Alias', response.image.alias); + testContext.uploadedImageId = response.id; + log.detail('Image ID', response.id); + log.detail('Storage Key', response.storageKey); + log.detail('Alias', response.alias); }); // Test 2: List images await runTest('List images', async () => { const result = await api(endpoints.images); - if (!result.data.images || !Array.isArray(result.data.images)) { + if (!result.data.data || !Array.isArray(result.data.data)) { throw new Error('No images array returned'); } - log.detail('Total images', result.data.images.length); - log.detail('Has uploaded', result.data.images.some((img: any) => img.source === 'uploaded')); + log.detail('Total images', result.data.data.length); + log.detail('Has uploaded', result.data.data.some((img: any) => img.source === 'uploaded')); }); // Test 3: Get image by ID await runTest('Get image by ID', async () => { const result = await api(`${endpoints.images}/${testContext.uploadedImageId}`); - if (!result.data.image) { + if (!result.data.data) { throw new Error('Image not found'); } - log.detail('Image ID', result.data.image.id); - log.detail('Source', result.data.image.source); - log.detail('File size', `${result.data.image.fileSize} bytes`); + log.detail('Image ID', result.data.data.id); + log.detail('Source', result.data.data.source); + log.detail('File size', `${result.data.data.fileSize} bytes`); }); // Test 4: Generate image without references @@ -68,14 +68,14 @@ async function main() { }), }); - if (!result.data.generation) { + if (!result.data.data) { throw new Error('No generation returned'); } - testContext.generationId = result.data.generation.id; - log.detail('Generation ID', result.data.generation.id); - log.detail('Status', result.data.generation.status); - log.detail('Prompt', result.data.generation.originalPrompt); + testContext.generationId = result.data.data.id; + log.detail('Generation ID', result.data.data.id); + log.detail('Status', result.data.data.status); + log.detail('Prompt', result.data.data.originalPrompt); // Wait for completion log.info('Waiting for generation to complete...'); @@ -93,7 +93,7 @@ async function main() { const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`); // Download image - const imageUrl = imageResult.data.image.storageUrl; + const imageUrl = imageResult.data.data.storageUrl; const imageResponse = await fetch(imageUrl); const imageBuffer = await imageResponse.arrayBuffer(); @@ -114,16 +114,16 @@ async function main() { }), }); - if (!result.data.generation) { + if (!result.data.data) { throw new Error('No generation returned'); } - log.detail('Generation ID', result.data.generation.id); - log.detail('Referenced images', result.data.generation.referencedImages?.length || 0); + log.detail('Generation ID', result.data.data.id); + log.detail('Referenced images', result.data.data.referencedImages?.length || 0); // Wait for completion log.info('Waiting for generation to complete...'); - const generation = await waitForGeneration(result.data.generation.id); + const generation = await waitForGeneration(result.data.data.id); if (generation.status !== 'success') { throw new Error(`Generation failed: ${generation.errorMessage}`); @@ -132,7 +132,7 @@ async function main() { // Save generated image if (generation.outputImageId) { const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`); - const imageUrl = imageResult.data.image.storageUrl; + const imageUrl = imageResult.data.data.storageUrl; const imageResponse = await fetch(imageUrl); const imageBuffer = await imageResponse.arrayBuffer(); @@ -144,12 +144,12 @@ async function main() { await runTest('List generations', async () => { const result = await api(endpoints.generations); - if (!result.data.generations || !Array.isArray(result.data.generations)) { + if (!result.data.data || !Array.isArray(result.data.data)) { throw new Error('No generations array returned'); } - log.detail('Total generations', result.data.generations.length); - log.detail('Successful', result.data.generations.filter((g: any) => g.status === 'success').length); + log.detail('Total generations', result.data.data.length); + log.detail('Successful', result.data.data.filter((g: any) => g.status === 'success').length); log.detail('Has pagination', !!result.data.pagination); }); @@ -157,14 +157,14 @@ async function main() { await runTest('Get generation details', async () => { const result = await api(`${endpoints.generations}/${testContext.generationId}`); - if (!result.data.generation) { + if (!result.data.data) { throw new Error('Generation not found'); } - log.detail('Generation ID', result.data.generation.id); - log.detail('Status', result.data.generation.status); - log.detail('Has image', !!result.data.image); - log.detail('Referenced images', result.data.referencedImages?.length || 0); + log.detail('Generation ID', result.data.data.id); + log.detail('Status', result.data.data.status); + log.detail('Has output image', !!result.data.data.outputImage); + log.detail('Referenced images', result.data.data.referencedImages?.length || 0); }); log.section('BASIC TESTS COMPLETED'); diff --git a/tests/api/02-flows.ts b/tests/api/02-flows.ts index c091bc9..ee08783 100644 --- a/tests/api/02-flows.ts +++ b/tests/api/02-flows.ts @@ -22,24 +22,24 @@ async function main() { }), }); - if (!result.data.flow || !result.data.flow.id) { + if (!result.data.data || !result.data.data.id) { throw new Error('No flow returned'); } - testContext.flowId = result.data.flow.id; - log.detail('Flow ID', result.data.flow.id); - log.detail('Aliases', JSON.stringify(result.data.flow.aliases)); + testContext.flowId = result.data.data.id; + log.detail('Flow ID', result.data.data.id); + log.detail('Aliases', JSON.stringify(result.data.data.aliases)); }); // Test 2: List flows await runTest('List flows', async () => { const result = await api(endpoints.flows); - if (!result.data.flows || !Array.isArray(result.data.flows)) { + if (!result.data.data || !Array.isArray(result.data.data)) { throw new Error('No flows array returned'); } - log.detail('Total flows', result.data.flows.length); + log.detail('Total flows', result.data.data.length); log.detail('Has pagination', !!result.data.pagination); }); @@ -55,16 +55,16 @@ async function main() { }), }); - if (!result.data.generation) { + if (!result.data.data) { throw new Error('No generation returned'); } - log.detail('Generation ID', result.data.generation.id); - log.detail('Flow ID', result.data.generation.flowId); + log.detail('Generation ID', result.data.data.id); + log.detail('Flow ID', result.data.data.flowId); // Wait for completion log.info('Waiting for generation to complete...'); - const generation = await waitForGeneration(result.data.generation.id); + const generation = await waitForGeneration(result.data.data.id); if (generation.status !== 'success') { throw new Error(`Generation failed: ${generation.errorMessage}`); @@ -96,16 +96,16 @@ async function main() { }), }); - if (!result.data.generation) { + if (!result.data.data) { throw new Error('No generation returned'); } - log.detail('Generation ID', result.data.generation.id); - log.detail('Referenced @last', result.data.generation.referencedImages?.some((r: any) => r.alias === '@last')); + log.detail('Generation ID', result.data.data.id); + log.detail('Referenced @last', result.data.data.referencedImages?.some((r: any) => r.alias === '@last')); // Wait for completion log.info('Waiting for generation to complete...'); - const generation = await waitForGeneration(result.data.generation.id); + const generation = await waitForGeneration(result.data.data.id); if (generation.status !== 'success') { throw new Error(`Generation failed: ${generation.errorMessage}`); @@ -126,13 +126,13 @@ async function main() { await runTest('Get flow details', async () => { const result = await api(`${endpoints.flows}/${testContext.flowId}`); - if (!result.data.flow) { + if (!result.data.data) { throw new Error('Flow not found'); } - log.detail('Flow ID', result.data.flow.id); - log.detail('Generations count', result.data.generations?.length || 0); - log.detail('Images count', result.data.images?.length || 0); + log.detail('Flow ID', result.data.data.id); + log.detail('Generations count', result.data.datas?.length || 0); + log.detail('Images count', result.data.data?.length || 0); log.detail('Resolved aliases', Object.keys(result.data.resolvedAliases || {}).length); }); @@ -157,11 +157,11 @@ async function main() { }), }); - if (!result.data.flow) { + if (!result.data.data) { throw new Error('Flow not returned'); } - log.detail('Updated aliases', JSON.stringify(result.data.flow.aliases)); + log.detail('Updated aliases', JSON.stringify(result.data.data.aliases)); }); // Test 7: Generate with flow-scoped alias @@ -177,16 +177,16 @@ async function main() { }), }); - if (!result.data.generation) { + if (!result.data.data) { throw new Error('No generation returned'); } - log.detail('Generation ID', result.data.generation.id); - log.detail('Referenced @hero', result.data.generation.referencedImages?.some((r: any) => r.alias === '@hero')); + log.detail('Generation ID', result.data.data.id); + log.detail('Referenced @hero', result.data.data.referencedImages?.some((r: any) => r.alias === '@hero')); // Wait for completion log.info('Waiting for generation to complete...'); - const generation = await waitForGeneration(result.data.generation.id); + const generation = await waitForGeneration(result.data.data.id); if (generation.status !== 'success') { throw new Error(`Generation failed: ${generation.errorMessage}`); @@ -211,13 +211,13 @@ async function main() { // Verify it's deleted const result = await api(`${endpoints.flows}/${testContext.flowId}`); - const hasFeatureAlias = '@featured' in result.data.flow.aliases; + const hasFeatureAlias = '@featured' in result.data.data.aliases; if (hasFeatureAlias) { throw new Error('Alias was not deleted'); } - log.detail('Remaining aliases', JSON.stringify(result.data.flow.aliases)); + log.detail('Remaining aliases', JSON.stringify(result.data.data.aliases)); }); log.section('FLOW TESTS COMPLETED'); diff --git a/tests/api/03-aliases.ts b/tests/api/03-aliases.ts index 81a4030..415c13b 100644 --- a/tests/api/03-aliases.ts +++ b/tests/api/03-aliases.ts @@ -78,50 +78,50 @@ async function main() { await runTest('Resolve project alias', async () => { const result = await api(`${endpoints.images}/resolve/@brand-logo`); - if (!result.data.image) { + if (!result.data.data) { throw new Error('Image not resolved'); } - if (result.data.scope !== 'project') { - throw new Error(`Wrong scope: ${result.data.scope}`); + if (result.data.data.scope !== 'project') { + throw new Error(`Wrong scope: ${result.data.data.scope}`); } - log.detail('Image ID', result.data.image.id); - log.detail('Scope', result.data.scope); - log.detail('Alias', result.data.image.alias); + log.detail('Image ID', result.data.data.id); + log.detail('Scope', result.data.data.scope); + log.detail('Alias', result.data.data.alias); }); // Test 5: Resolve flow-scoped alias await runTest('Resolve flow alias', async () => { const result = await api(`${endpoints.images}/resolve/@temp-logo?flowId=${testContext.flowId}`); - if (!result.data.image) { + if (!result.data.data) { throw new Error('Image not resolved'); } - if (result.data.scope !== 'flow') { - throw new Error(`Wrong scope: ${result.data.scope}`); + if (result.data.data.scope !== 'flow') { + throw new Error(`Wrong scope: ${result.data.data.scope}`); } - log.detail('Image ID', result.data.image.id); - log.detail('Scope', result.data.scope); - log.detail('Flow ID', result.data.flow?.id); + log.detail('Image ID', result.data.data.id); + log.detail('Scope', result.data.data.scope); + log.detail('Flow ID', result.data.data?.id); }); // Test 6: Resolve @last technical alias await runTest('Resolve @last technical alias', async () => { const result = await api(`${endpoints.images}/resolve/@last?flowId=${testContext.flowId}`); - if (!result.data.image) { + if (!result.data.data) { throw new Error('Image not resolved'); } - if (result.data.scope !== 'technical') { - throw new Error(`Wrong scope: ${result.data.scope}`); + if (result.data.data.scope !== 'technical') { + throw new Error(`Wrong scope: ${result.data.data.scope}`); } - log.detail('Image ID', result.data.image.id); - log.detail('Scope', result.data.scope); + log.detail('Image ID', result.data.data.id); + log.detail('Scope', result.data.data.scope); log.detail('Technical alias', '@last'); }); @@ -129,33 +129,33 @@ async function main() { await runTest('Resolve @first technical alias', async () => { const result = await api(`${endpoints.images}/resolve/@first?flowId=${testContext.flowId}`); - if (!result.data.image) { + if (!result.data.data) { throw new Error('Image not resolved'); } - if (result.data.scope !== 'technical') { - throw new Error(`Wrong scope: ${result.data.scope}`); + if (result.data.data.scope !== 'technical') { + throw new Error(`Wrong scope: ${result.data.data.scope}`); } - log.detail('Image ID', result.data.image.id); - log.detail('Scope', result.data.scope); + log.detail('Image ID', result.data.data.id); + log.detail('Scope', result.data.data.scope); }); // Test 8: Resolve @upload technical alias await runTest('Resolve @upload technical alias', async () => { const result = await api(`${endpoints.images}/resolve/@upload?flowId=${testContext.flowId}`); - if (!result.data.image) { + if (!result.data.data) { throw new Error('Image not resolved'); } - if (result.data.scope !== 'technical') { - throw new Error(`Wrong scope: ${result.data.scope}`); + if (result.data.data.scope !== 'technical') { + throw new Error(`Wrong scope: ${result.data.data.scope}`); } - log.detail('Image ID', result.data.image.id); - log.detail('Scope', result.data.scope); - log.detail('Source', result.data.image.source); + log.detail('Image ID', result.data.data.id); + log.detail('Scope', result.data.data.scope); + log.detail('Source', result.data.data.source); }); // Test 9: Generate with assignAlias (project-scoped) @@ -171,7 +171,7 @@ async function main() { }); log.info('Waiting for generation to complete...'); - const generation = await waitForGeneration(result.data.generation.id); + const generation = await waitForGeneration(result.data.data.id); if (generation.status !== 'success') { throw new Error(`Generation failed: ${generation.errorMessage}`); @@ -208,7 +208,7 @@ async function main() { }); log.info('Waiting for generation to complete...'); - const generation = await waitForGeneration(result.data.generation.id); + const generation = await waitForGeneration(result.data.data.id); if (generation.status !== 'success') { throw new Error(`Generation failed: ${generation.errorMessage}`); diff --git a/tests/api/config.ts b/tests/api/config.ts index 46d1dd1..61d2640 100644 --- a/tests/api/config.ts +++ b/tests/api/config.ts @@ -7,7 +7,7 @@ export const config = { // Paths resultsDir: '../../results', - fixturesDir: './fixtures', + fixturesDir: './fixture', // Timeouts requestTimeout: 30000, diff --git a/tests/api/fixture/image.png b/tests/api/fixture/test-image.png similarity index 100% rename from tests/api/fixture/image.png rename to tests/api/fixture/test-image.png diff --git a/tests/api/utils.ts b/tests/api/utils.ts index 30dbf29..230958c 100644 --- a/tests/api/utils.ts +++ b/tests/api/utils.ts @@ -2,7 +2,7 @@ import { writeFile, mkdir } from 'fs/promises'; import { join } from 'path'; -import { config } from './config'; +import { config, endpoints } from './config'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; @@ -150,7 +150,7 @@ export async function uploadFile( formData.append(key, value); } - const result = await api(config.endpoints.images + '/upload', { + const result = await api(endpoints.images + '/upload', { method: 'POST', body: formData, headers: { @@ -158,7 +158,7 @@ export async function uploadFile( }, }); - return result.data; + return result.data.data; } // Wait helper @@ -172,8 +172,8 @@ export async function waitForGeneration( maxAttempts = 20 ): Promise { for (let i = 0; i < maxAttempts; i++) { - const result = await api(`${config.endpoints.generations}/${generationId}`); - const generation = result.data.generation; + const result = await api(`${endpoints.generations}/${generationId}`); + const generation = result.data.data; if (generation.status === 'success' || generation.status === 'failed') { return generation;