429 lines
13 KiB
TypeScript
429 lines
13 KiB
TypeScript
// tests/api/02-basic.ts
|
|
// Image Upload and CRUD Operations
|
|
|
|
import { join } from 'path';
|
|
import { api, log, runTest, saveImage, uploadFile, waitForGeneration, testContext, verifyImageAccessible, resolveAlias, exitWithTestResults } 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()
|
|
.then(() => exitWithTestResults())
|
|
.catch((error) => {
|
|
console.error('Unexpected error:', error);
|
|
process.exit(1);
|
|
});
|