// tests/api/02-basic.ts // Image Upload and CRUD Operations import { join } from 'path'; import { api, log, runTest, saveImage, uploadFile, waitForGeneration, testContext, verifyImageAccessible, resolveAlias } from './utils'; import { config, endpoints } from './config'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); async function main() { log.section('IMAGE UPLOAD & CRUD TESTS'); // Test 1: Upload image with project-scoped alias await runTest('Upload image with project alias', async () => { const fixturePath = join(__dirname, config.fixturesDir, 'test-image.png'); const response = await uploadFile(fixturePath, { alias: '@test-logo', description: 'Test logo image for CRUD tests', }); if (!response || !response.id) { throw new Error('No image returned'); } if (response.alias !== '@test-logo') { throw new Error('Alias not set correctly'); } if (response.source !== 'uploaded') { throw new Error('Source should be "uploaded"'); } testContext.uploadedImageId = response.id; log.detail('Image ID', response.id); log.detail('Storage Key', response.storageKey); log.detail('Alias', response.alias); log.detail('Source', response.source); }); // Test 2: Upload image without alias await runTest('Upload image without alias', async () => { const fixturePath = join(__dirname, config.fixturesDir, 'test-image.png'); const response = await uploadFile(fixturePath, { description: 'Image without alias', }); if (!response || !response.id) { throw new Error('No image returned'); } if (response.alias !== null) { throw new Error('Alias should be null'); } log.detail('Image ID', response.id); log.detail('Alias', 'null (as expected)'); }); // Test 3: List all images await runTest('List all images', async () => { const result = await api(endpoints.images); if (!result.data.data || !Array.isArray(result.data.data)) { throw new Error('No images array returned'); } log.detail('Total images', result.data.data.length); // Find our uploaded image const found = result.data.data.find((img: any) => img.id === testContext.uploadedImageId); if (!found) { throw new Error('Uploaded image not in list'); } log.detail('Found our image', '✓'); }); // Test 4: List images with source filter await runTest('List images - filter by source=uploaded', async () => { const result = await api(`${endpoints.images}?source=uploaded`); if (!result.data.data || !Array.isArray(result.data.data)) { throw new Error('No images array returned'); } // All should be uploaded const allUploaded = result.data.data.every((img: any) => img.source === 'uploaded'); if (!allUploaded) { throw new Error('Source filter not working'); } log.detail('Uploaded images', result.data.data.length); }); // Test 5: List images with pagination await runTest('List images with pagination', async () => { const result = await api(`${endpoints.images}?limit=3&offset=0`); if (!result.data.pagination) { throw new Error('No pagination data'); } log.detail('Limit', result.data.pagination.limit); log.detail('Offset', result.data.pagination.offset); log.detail('Total', result.data.pagination.total); log.detail('Has more', result.data.pagination.hasMore); }); // Test 6: Get image by ID await runTest('Get image by ID', async () => { const result = await api(`${endpoints.images}/${testContext.uploadedImageId}`); if (!result.data.data) { throw new Error('Image not found'); } const image = result.data.data; // Verify fields if (!image.id) throw new Error('Missing id'); if (!image.storageKey) throw new Error('Missing storageKey'); if (!image.storageUrl) throw new Error('Missing storageUrl'); if (!image.source) throw new Error('Missing source'); log.detail('Image ID', image.id); log.detail('Source', image.source); log.detail('File size', `${image.fileSize || 0} bytes`); log.detail('Alias', image.alias || 'null'); }); // Test 7: Get image by alias (using resolve endpoint) await runTest('Resolve project-scoped alias', async () => { const resolved = await resolveAlias('@test-logo'); if (!resolved.imageId) { throw new Error('Alias not resolved'); } if (resolved.imageId !== testContext.uploadedImageId) { throw new Error('Resolved to wrong image'); } if (resolved.scope !== 'project') { throw new Error(`Wrong scope: ${resolved.scope}`); } log.detail('Resolved image ID', resolved.imageId); log.detail('Scope', resolved.scope); log.detail('Alias', resolved.alias); }); // Test 8: Update image metadata await runTest('Update image metadata', async () => { const result = await api(`${endpoints.images}/${testContext.uploadedImageId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ focalPoint: { x: 0.5, y: 0.3 }, meta: { description: 'Updated description', tags: ['test', 'logo', 'updated'], }, }), }); if (!result.data.data) { throw new Error('No image returned'); } // Verify update by fetching again const updated = await api(`${endpoints.images}/${testContext.uploadedImageId}`); const image = updated.data.data; if (!image.focalPoint || image.focalPoint.x !== 0.5 || image.focalPoint.y !== 0.3) { throw new Error('Focal point not updated'); } log.detail('Focal point', JSON.stringify(image.focalPoint)); log.detail('Meta', JSON.stringify(image.meta)); }); // Test 9: Update image alias (dedicated endpoint) await runTest('Update image alias', async () => { const result = await api(`${endpoints.images}/${testContext.uploadedImageId}/alias`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ alias: '@new-test-logo', }), }); if (!result.data.data) { throw new Error('No image returned'); } // Verify new alias works const resolved = await resolveAlias('@new-test-logo'); if (resolved.imageId !== testContext.uploadedImageId) { throw new Error('New alias not working'); } log.detail('New alias', '@new-test-logo'); log.detail('Resolved', '✓'); }); // Test 10: Verify old alias doesn't work after update await runTest('Old alias should not resolve after update', async () => { try { await resolveAlias('@test-logo'); throw new Error('Old alias should not resolve'); } catch (error: any) { // Expected to fail if (error.message.includes('should not resolve')) { throw error; } log.detail('Old alias correctly invalid', '✓'); } }); // Test 11: Remove image alias await runTest('Remove image alias', async () => { await api(`${endpoints.images}/${testContext.uploadedImageId}/alias`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ alias: null, }), }); // Verify image exists but has no alias const result = await api(`${endpoints.images}/${testContext.uploadedImageId}`); if (result.data.data.alias !== null) { throw new Error('Alias should be null'); } // Verify alias resolution fails try { await resolveAlias('@new-test-logo'); throw new Error('Removed alias should not resolve'); } catch (error: any) { if (error.message.includes('should not resolve')) { throw error; } log.detail('Alias removed', '✓'); } }); // Test 12: Generate image with manual reference await runTest('Generate with manual reference image', async () => { // First, reassign alias for reference await api(`${endpoints.images}/${testContext.uploadedImageId}/alias`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ alias: '@reference-logo', }), }); const result = await api(endpoints.generations, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: 'A product photo with the logo in corner', aspectRatio: '1:1', referenceImages: ['@reference-logo'], }), }); const generation = await waitForGeneration(result.data.data.id); if (generation.status !== 'success') { throw new Error(`Generation failed: ${generation.errorMessage}`); } // Verify referenced images tracked if (!generation.referencedImages || generation.referencedImages.length === 0) { throw new Error('Referenced images not tracked'); } const refFound = generation.referencedImages.some( (ref: any) => ref.alias === '@reference-logo' ); if (!refFound) { throw new Error('Reference image not found in referencedImages'); } log.detail('Generation ID', generation.id); log.detail('Referenced images', generation.referencedImages.length); // Save generated image if (generation.outputImageId) { const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`); const imageUrl = imageResult.data.data.storageUrl; const imageResponse = await fetch(imageUrl); const imageBuffer = await imageResponse.arrayBuffer(); await saveImage(imageBuffer, 'gen-with-reference.png'); } }); // Test 13: Generate with auto-detected reference in prompt await runTest('Generate with auto-detected reference', async () => { const result = await api(endpoints.generations, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: 'Create banner using @reference-logo with blue background', aspectRatio: '16:9', // NOTE: referenceImages NOT provided, should auto-detect }), }); const generation = await waitForGeneration(result.data.data.id); if (generation.status !== 'success') { throw new Error(`Generation failed: ${generation.errorMessage}`); } // Verify auto-detection worked if (!generation.referencedImages || generation.referencedImages.length === 0) { throw new Error('Auto-detection did not work'); } const autoDetected = generation.referencedImages.some( (ref: any) => ref.alias === '@reference-logo' ); if (!autoDetected) { throw new Error('Reference not auto-detected from prompt'); } log.detail('Auto-detected references', generation.referencedImages.length); }); // Test 14: Generate with project alias assignment await runTest('Generate with project alias assignment', async () => { const result = await api(endpoints.generations, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: 'A hero banner image', aspectRatio: '21:9', alias: '@hero-banner', }), }); const generation = await waitForGeneration(result.data.data.id); if (generation.status !== 'success') { throw new Error(`Generation failed: ${generation.errorMessage}`); } // Verify alias assigned to output image const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`); const image = imageResult.data.data; if (image.alias !== '@hero-banner') { throw new Error('Alias not assigned to output image'); } // Verify alias resolution works const resolved = await resolveAlias('@hero-banner'); if (resolved.imageId !== generation.outputImageId) { throw new Error('Alias resolution failed'); } log.detail('Output image alias', image.alias); log.detail('Alias resolution', '✓'); testContext.heroBannerId = generation.outputImageId; }); // Test 15: Alias conflict - new generation overwrites await runTest('Alias conflict resolution', async () => { // First generation has @hero alias (from previous test) const firstImageId = testContext.heroBannerId; // Create second generation with same alias const result = await api(endpoints.generations, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: 'A different hero image', aspectRatio: '21:9', alias: '@hero-banner', }), }); const generation = await waitForGeneration(result.data.data.id); if (generation.status !== 'success') { throw new Error(`Generation failed: ${generation.errorMessage}`); } const secondImageId = generation.outputImageId; // Verify second image has the alias const resolved = await resolveAlias('@hero-banner'); if (resolved.imageId !== secondImageId) { throw new Error('Second image should have the alias'); } // Verify first image lost the alias but still exists const firstImage = await api(`${endpoints.images}/${firstImageId}`); if (firstImage.data.data.alias !== null) { throw new Error('First image should have lost the alias'); } log.detail('Second image has alias', '✓'); log.detail('First image preserved', '✓'); log.detail('First image alias removed', '✓'); }); log.section('IMAGE UPLOAD & CRUD TESTS COMPLETED'); } main().catch(console.error);