/** * Integration tests for feedback screenshot flow * * These tests verify the complete flow: * 1. Generate SAS URL for upload * 2. Upload screenshot to blob storage * 3. Submit feedback with screenshot metadata * 4. Retrieve feedback with screenshot URL * * TODO-4: Requires blob storage to be available in test environment * Skip these tests if AZURE_BLOB_CONNECTION_STRING is not set */ import { describe, it, expect, beforeAll } from 'vitest'; import { createFeedbackClient, type FeedbackClient } from './index.js'; // Check if blob storage is available const blobStorageAvailable = !!( process.env.AZURE_BLOB_CONNECTION_STRING || (process.env.AZURE_BLOB_ACCOUNT_NAME && process.env.AZURE_BLOB_ACCOUNT_KEY) ); const describeIntegration = blobStorageAvailable ? describe : describe.skip; describeIntegration('Feedback Screenshot Integration', () => { let client: FeedbackClient; const testBaseUrl = process.env.TEST_API_URL || 'http://localhost:4003'; const testAuthToken = process.env.TEST_AUTH_TOKEN || 'test-token'; beforeAll(() => { client = createFeedbackClient({ baseUrl: testBaseUrl, getAuthToken: () => testAuthToken, }); }); it('should complete full screenshot submission flow', async () => { // Create a test image blob (1x1 pixel PNG) const testPngData = new Uint8Array([ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR chunk 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1x1 pixel 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, // IDAT chunk 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, 0x18, 0xDD, 0x8D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, // IEND chunk 0x44, 0xAE, 0x42, 0x60, 0x82, ]); const testBlob = new Blob([testPngData], { type: 'image/png' }); // Submit feedback with screenshot const result = await client.submitWithScreenshot({ type: 'bug', title: 'Integration test screenshot', body: 'This is a test feedback with screenshot', screenshot: { blob: testBlob, contentType: 'image/png', }, deviceContext: { osVersion: 'Test OS 1.0', appVersion: '1.0.0', deviceModel: 'Test Device', screenResolution: '1920x1080', locale: 'en-US', }, }); // Verify response expect(result).toBeDefined(); expect(result.id).toBeDefined(); expect(result.type).toBe('bug'); expect(result.title).toBe('Integration test screenshot'); expect(result.status).toBe('new'); expect(result.screenshotBlobPath).toBeDefined(); expect(result.screenshotBlobPath).toContain('feedbackScreenshots'); // TODO: Verify screenshot can be retrieved via admin API // const screenshotRes = await fetch(`${testBaseUrl}/api/feedback/${result.id}/screenshot`, { // headers: { Authorization: `Bearer ${adminToken}` }, // }); // expect(screenshotRes.ok).toBe(true); }, 30000); // 30 second timeout for upload it('should submit feedback without screenshot', async () => { const result = await client.submitWithScreenshot({ type: 'feature', title: 'Integration test without screenshot', body: 'This is a test feedback without screenshot', }); expect(result).toBeDefined(); expect(result.id).toBeDefined(); expect(result.type).toBe('feature'); expect(result.screenshotBlobPath).toBeUndefined(); }); it('should track upload progress', async () => { const progressCallbacks: number[] = []; const testBlob = new Blob(['test data'], { type: 'image/png' }); try { await client.submitWithScreenshot({ type: 'bug', title: 'Progress test', screenshot: { blob: testBlob, contentType: 'image/png', }, }, (loaded, total) => { progressCallbacks.push(loaded); }); } catch (err) { // Expected to fail with invalid PNG, but progress should still be called } // Progress callback may or may not be called depending on upload speed // Just verify the callback mechanism exists expect(progressCallbacks).toBeDefined(); }); }); describe('Feedback Client Unit Tests (no blob storage required)', () => { it('should validate screenshot content types', () => { const validTypes = ['image/png', 'image/jpeg', 'image/webp']; const invalidTypes = ['image/gif', 'application/pdf', 'text/plain']; for (const type of validTypes) { expect(type).toMatch(/^image\/(png|jpeg|webp)$/); } for (const type of invalidTypes) { expect(type).not.toMatch(/^image\/(png|jpeg|webp)$/); } }); it('should enforce 5MB size limit', () => { const maxSize = 5 * 1024 * 1024; // 5MB const underLimit = maxSize - 1; const overLimit = maxSize + 1; expect(underLimit).toBeLessThanOrEqual(maxSize); expect(overLimit).toBeGreaterThan(maxSize); }); });