- diagnostics/subscribers: notify admin via email when debug session is cancelled (looks up session creator via getSession + getUserById) - diagnostics/subscribers: email session summary (logs/traces/screenshots) to admin when debug session completes - diagnostics/subscribers: send Slack alert via dispatchSlack for FATAL logs ingested during debug sessions (on-call engineer notification) - feedback-client/integration.test.ts: replace TODO-4 with clear NOTE, fix unused var lint errors - feedback-client/gdpr.test.ts: mark lifecycle policy as accepted, remove console.log + unused blobPath variable - Update WORKSPACE_TODO_AUDIT.md — P3 section: all 5 resolved - Typecheck clean, 1483/1483 tests pass
211 lines
5.4 KiB
TypeScript
211 lines
5.4 KiB
TypeScript
/**
|
|
* 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
|
|
*
|
|
* NOTE: Requires blob storage to be available in test environment.
|
|
* Tests are auto-skipped when AZURE_BLOB_CONNECTION_STRING is not set.
|
|
* In CI, set AZURE_BLOB_CONNECTION_STRING or AZURE_BLOB_ACCOUNT_NAME+KEY.
|
|
*/
|
|
|
|
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 {
|
|
// 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);
|
|
});
|
|
});
|