import { BaseE2ETest } from '../../lib/base-test'; class ReviewE2ETest extends BaseE2ETest { // Review commands async runListCommand(appId?: string) { const args = ['review', 'list']; if (appId) { args.push(appId); } return this.execute(args); } async runOpenCommand(appVersion: string) { return this.execute(['review', 'open', appVersion]); } async runOpenCommandWithoutVersion() { return this.execute(['review', 'open']); } // Public wrapper methods to access protected assertions public checkSuccess(result: any) { this.assertSuccess(result); } public checkFailure(result: any) { this.assertFailure(result); } public checkOutputContains(result: any, text: string) { this.assertOutputContains(result, text); } public checkOutputMatchesBaseline(result: any, baselinePath: string, maxLines?: number) { this.assertOutputMatchesBaseline(result, baselinePath, maxLines); } public clearHistory() { this.cliExecutor.clearProcessHistory(); } public executePublic(args: string[], options?: any) { return this.execute(args, options); } } describe('Review Commands E2E Tests', () => { let testInstance: ReviewE2ETest; beforeAll(() => { testInstance = new ReviewE2ETest(); }); afterAll(async () => { if (testInstance) { await testInstance.cleanup(); } }); afterEach(() => { // Clean up process history after each test if (testInstance) { testInstance.clearHistory(); } }); describe('review list', () => { it('should list all reviews when no app ID is provided', async () => { const result = await testInstance.runListCommand(); expect(result.exitCode).toBe(0); expect(result.stdout).toBeTruthy(); // Check baseline format (first 5 lines - header + 1 entry) testInstance.checkOutputMatchesBaseline(result, 'review/list.txt', 4); }, 15000); it('should filter reviews by app ID when provided', async () => { const result = await testInstance.runListCommand('ocp_shakedown'); expect(result.exitCode).toBe(0); expect(result.stdout).toBeTruthy(); // Check for table headers expect(result.stdout).toMatch(/App ID/); expect(result.stdout).toMatch(/Version/); expect(result.stdout).toMatch(/Review Status/); expect(result.stdout).toMatch(/Created At/); expect(result.stdout).toMatch(/Updated At/); // If there are results, they should only be for the specified app const lines = result.stdout.split('\n'); const dataLines = lines.filter(line => line.trim() && !line.includes('App ID') && !line.includes('---') && line.includes('ocp_shakedown') ); // Each data line should contain the app ID we filtered for dataLines.forEach(line => { if (line.trim()) { expect(line).toMatch(/ocp_shakedown/); } }); }, 15000); it('should handle non-existent app ID gracefully', async () => { const result = await testInstance.runListCommand('nonexistent-app-id'); expect(result.exitCode).toBe(0); expect(result.stdout).toBeTruthy(); // Should still show headers even with no results expect(result.stdout).toMatch(/App ID/); expect(result.stdout).toMatch(/Version/); expect(result.stdout).toMatch(/Review Status/); }, 15000); it('should display review statuses correctly', async () => { const result = await testInstance.runListCommand(); expect(result.exitCode).toBe(0); expect(result.stdout).toBeTruthy(); // Should contain valid review statuses const validStatuses = ['IN_REVIEW', 'APPROVED', 'REJECTED']; const containsValidStatus = validStatuses.some(status => result.stdout.includes(status) ); // Strip beta warning before checking for data lines const outputWithoutWarning = result.stdout.replace(/You are running beta version[^\n]+\n/g, ''); // If there are any reviews, they should have valid statuses // Check for lines that look like data (contain app IDs with version patterns) const hasDataLines = outputWithoutWarning.split('\n').some(line => line.trim() && !line.includes('App ID') && !line.includes('---') && !line.includes('Active environment') && /\d+\.\d+/.test(line) // Contains version number pattern ); if (hasDataLines) { expect(containsValidStatus).toBe(true); } }, 15000); it('should complete within reasonable time', async () => { const result = await testInstance.runListCommand(); expect(result.executionTime).toBeLessThan(15000); // 15 seconds expect(result.timedOut).toBe(false); }, 20000); }); describe('review open', () => { it('should fail when app version parameter is missing', async () => { const result = await testInstance.runOpenCommandWithoutVersion(); expect(result.exitCode).not.toBe(0); expect(result.stderr).toBeTruthy(); expect(result.stderr).toMatch(/Missing required parameter.*appVersion/); }); it('should handle non-existent app version gracefully', async () => { const result = await testInstance.runOpenCommand('nonexistent-app@1.0.0'); expect(result.exitCode).not.toBe(0); expect(result.stderr).toBeTruthy(); }, 15000); it('should handle malformed app version parameter', async () => { const result = await testInstance.runOpenCommand('invalid-format'); expect(result.exitCode).not.toBe(0); expect(result.stderr).toBeTruthy(); }, 15000); it('should complete within reasonable time', async () => { // Use a dummy app version for timing test const result = await testInstance.runOpenCommand('dummy-app@1.0.0'); expect(result.executionTime).toBeLessThan(10000); // 10 seconds expect(result.timedOut).toBe(false); }, 15000); it('should attempt to open valid review URL', async () => { // Get a real app version from the review list first const listResult = await testInstance.runListCommand(); if (listResult.exitCode === 0 && listResult.stdout) { const lines = listResult.stdout.split('\n'); const dataLine = lines.find(line => line.trim() && !line.includes('App ID') && !line.includes('---') && line.includes('.') // Likely contains version numbers ); if (dataLine) { const parts = dataLine.trim().split(/\s+/); if (parts.length >= 2) { const appId = parts[0]; const version = parts[1]; const appVersion = `${appId}@${version}`; const result = await testInstance.runOpenCommand(appVersion); // Command might succeed (if review exists) or fail (if not found) expect(result.exitCode).toBeDefined(); if (result.exitCode === 0) { // If successful, should mention opening or URL expect(result.stdout).toMatch(/Review URL|Opened review|url/i); } else { // If failed, should have error message expect(result.stderr).toBeTruthy(); } } else { // Skip test if no valid app version found expect(true).toBe(true); } } else { // Skip test if no data found expect(true).toBe(true); } } else { // Skip test if list command failed expect(true).toBe(true); } }, 25000); }); describe('Error Scenarios', () => { it('should handle malformed command gracefully', async () => { const result = await testInstance.executePublic(['review', 'invalid-command']); expect(result.exitCode).not.toBe(0); expect(result.stderr).toBeTruthy(); }); it('should handle unexpected parameters for list command', async () => { const result = await testInstance.executePublic(['review', 'list', 'param1', 'param2']); expect(result.exitCode).not.toBe(0); }); }); describe('Performance Tests', () => { it('should complete review list within reasonable time', async () => { const result = await testInstance.runListCommand(); expect(result.executionTime).toBeLessThan(15000); // 15 seconds expect(result.executionTime).toBeGreaterThan(0); }, 20000); it('should complete review open within reasonable time', async () => { const result = await testInstance.runOpenCommand('dummy-app@1.0.0'); expect(result.executionTime).toBeLessThan(10000); // 10 seconds expect(result.executionTime).toBeGreaterThan(0); }, 15000); it('should handle multiple concurrent list requests', async () => { const promises = Array(3).fill(null).map(() => testInstance.runListCommand()); const results = await Promise.all(promises); results.forEach(result => { expect(result.exitCode).toBe(0); expect(result.stdout).toBeTruthy(); expect(result.executionTime).toBeLessThan(20000); }); }, 30000); }); describe('Output Validation', () => { it('should format output properly', async () => { const result = await testInstance.runListCommand(); expect(result.exitCode).toBe(0); expect(result.stdout).toBeTruthy(); // Output should be tabular format const lines = result.stdout.trim().split('\n').filter(line => line.trim()); expect(lines.length).toBeGreaterThanOrEqual(1); // At least header // Header line should contain all expected columns const headerLine = lines.find(line => line.includes('App ID') && line.includes('Version') && line.includes('Review Status') ); expect(headerLine).toBeTruthy(); }, 15000); it('should not contain sensitive information in output', async () => { const result = await testInstance.runListCommand(); expect(result.exitCode).toBe(0); expect(result.stdout).toBeTruthy(); // Output should not contain sensitive data patterns expect(result.stdout).not.toMatch(/password/i); expect(result.stdout).not.toMatch(/token/i); expect(result.stdout).not.toMatch(/secret/i); expect(result.stdout).not.toMatch(/api.*key/i); }, 10000); it('should handle empty results gracefully', async () => { const result = await testInstance.runListCommand('definitely-nonexistent-app-12345'); expect(result.exitCode).toBe(0); expect(result.stdout).toBeTruthy(); // Should still show headers expect(result.stdout).toMatch(/App ID/); expect(result.stdout).toMatch(/Version/); expect(result.stdout).toMatch(/Review Status/); }, 15000); }); describe('Data Consistency', () => { it('should return consistent results across multiple calls', async () => { const result1 = await testInstance.runListCommand(); const result2 = await testInstance.runListCommand(); expect(result1.exitCode).toBe(result2.exitCode); expect(result1.exitCode).toBe(0); // Results should be consistent (allowing for minor timing differences) const lines1 = result1.stdout.split('\n').filter(line => line.trim()); const lines2 = result2.stdout.split('\n').filter(line => line.trim()); // Should have same number of header lines at minimum expect(lines1.length).toBeGreaterThan(0); expect(lines2.length).toBeGreaterThan(0); }, 25000); it('should filter results correctly when app ID is provided', async () => { const allResult = await testInstance.runListCommand(); const filteredResult = await testInstance.runListCommand('ocp_shakedown'); expect(allResult.exitCode).toBe(0); expect(filteredResult.exitCode).toBe(0); // Filtered results should be subset of all results (or empty) const filteredLines = filteredResult.stdout.split('\n').filter(line => line.trim() && !line.includes('App ID') && !line.includes('---') && line.includes('ocp_shakedown') ); // Each filtered line should be for the specified app filteredLines.forEach(line => { if (line.trim()) { expect(line).toMatch(/ocp_shakedown/); } }); }, 25000); }); });