-
Notifications
You must be signed in to change notification settings - Fork 96
[DevTools] PR4: Testing #2918
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/dev-tools
Are you sure you want to change the base?
[DevTools] PR4: Testing #2918
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,334 @@ | ||
import { afterEach, beforeEach, describe, it, mock } from 'node:test'; | ||
import assert from 'node:assert'; | ||
import { SocketHandlerService } from '../services/socket_handlers.js'; | ||
import { SOCKET_EVENTS } from '../shared/socket_events.js'; | ||
import { LocalStorageManager } from '../local_storage_manager.js'; | ||
Check failure on line 5 in packages/cli/src/commands/sandbox/sandbox-devtools/integration-tests/error_scenarios.test.ts
|
||
import { Server } from 'socket.io'; | ||
import { BackendIdentifier } from '@aws-amplify/plugin-types'; | ||
import { LogLevel, Printer, printer } from '@aws-amplify/cli-core'; | ||
import { ResourceService } from '../services/resource_service.js'; | ||
import { ShutdownService } from '../services/shutdown_service.js'; | ||
import { Sandbox, SandboxStatus } from '@aws-amplify/sandbox'; | ||
import { SandboxStatusData } from '../shared/socket_types.js'; | ||
Check failure on line 12 in packages/cli/src/commands/sandbox/sandbox-devtools/integration-tests/error_scenarios.test.ts
|
||
import { createServer } from 'node:http'; | ||
import express from 'express'; | ||
import { io as socketIOClient } from 'socket.io-client'; | ||
|
||
/** | ||
* This integration test focuses on complex error scenarios, specifically: | ||
* 1. Network failures and timeouts | ||
* 2. Resource state inconsistencies | ||
* 3. Storage corruption | ||
* 4. Authentication/permission errors | ||
* 5. AWS service limits and throttling | ||
*/ | ||
void describe('Complex Error Scenarios Integration Test', () => { | ||
let server: ReturnType<typeof createServer>; | ||
let io: Server; | ||
let clientSocket: ReturnType<typeof socketIOClient>; | ||
let socketHandlerService: SocketHandlerService; | ||
let mockSandbox: Sandbox; | ||
let mockShutdownService: ShutdownService; | ||
let mockResourceService: ResourceService; | ||
let mockPrinter: Printer; | ||
let storageManager: LocalStorageManager; | ||
let mockGetSandboxState: () => Promise<SandboxStatus>; | ||
let mockBackendId: BackendIdentifier; | ||
let port: number; | ||
|
||
// Define the return type of mock.fn() | ||
type MockFn = ReturnType<typeof mock.fn>; | ||
|
||
// This is intentionally using the done callback pattern for setup | ||
beforeEach((t, done) => { | ||
mock.reset(); | ||
port = 3340; // Use a different port than other tests | ||
|
||
// Set up a real express server and socket.io server | ||
const app = express(); | ||
// eslint-disable-next-line @typescript-eslint/no-misused-promises | ||
server = createServer(app); | ||
io = new Server(server); | ||
|
||
// Start the server | ||
server.listen(port); | ||
|
||
// Set up mocks | ||
mockPrinter = { | ||
print: mock.fn(), | ||
log: mock.fn(), | ||
} as unknown as Printer; | ||
mock.method(printer, 'log'); | ||
|
||
mockBackendId = { name: 'test-backend' } as BackendIdentifier; | ||
|
||
mockSandbox = { | ||
start: mock.fn(() => Promise.resolve()), | ||
stop: mock.fn(() => Promise.resolve()), | ||
delete: mock.fn(() => Promise.resolve()), | ||
getState: mock.fn(() => 'running'), | ||
on: mock.fn(), | ||
} as unknown as Sandbox; | ||
|
||
mockShutdownService = { | ||
shutdown: mock.fn(), | ||
} as unknown as ShutdownService; | ||
|
||
mockResourceService = { | ||
getDeployedBackendResources: mock.fn(() => | ||
Promise.resolve({ | ||
name: 'test-backend', | ||
status: 'running', | ||
resources: [], | ||
region: 'us-east-1', | ||
}), | ||
), | ||
} as unknown as ResourceService; | ||
|
||
mockGetSandboxState = mock.fn(() => Promise.resolve('running')); | ||
|
||
// Use real storage manager with a test identifier to ensure isolation | ||
storageManager = new LocalStorageManager('complex-error-test', { | ||
maxLogSizeMB: 50, | ||
}); | ||
|
||
// Create the socket handler service with real and mocked dependencies | ||
socketHandlerService = new SocketHandlerService( | ||
io, | ||
mockSandbox, | ||
mockGetSandboxState, | ||
mockBackendId, | ||
mockShutdownService, | ||
mockResourceService, | ||
storageManager, | ||
undefined, // No active log pollers for this test | ||
Check failure on line 104 in packages/cli/src/commands/sandbox/sandbox-devtools/integration-tests/error_scenarios.test.ts
|
||
undefined, // No toggle start times for this test | ||
mockPrinter, | ||
); | ||
|
||
// Set up socket handlers when a client connects | ||
io.on('connection', (socket) => { | ||
socketHandlerService.setupSocketHandlers(socket); | ||
}); | ||
|
||
// Create a real client socket | ||
clientSocket = socketIOClient(`http://localhost:${port}`); | ||
|
||
// Wait for the client to connect then call done | ||
clientSocket.on('connect', () => { | ||
done(); | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
// Clean up | ||
try { | ||
storageManager.clearAll(); | ||
clientSocket.close(); | ||
void io.close(); | ||
server.close(); | ||
} catch (err) { | ||
printer.log(`Cleanup error: ${String(err)}`, LogLevel.ERROR); | ||
} | ||
}); | ||
|
||
void it('should handle rate limit and throttling errors from AWS', async () => { | ||
// Setup rate limit error simulation | ||
const rateLimitError = new Error('Rate exceeded'); | ||
rateLimitError.name = 'ThrottlingException'; | ||
|
||
( | ||
mockResourceService.getDeployedBackendResources as unknown as MockFn | ||
).mock.mockImplementation(() => Promise.reject(rateLimitError)); | ||
|
||
// Set up a promise that will resolve when we receive an error response | ||
const responseReceived = new Promise<{ | ||
status: string; | ||
error: string; | ||
name: string; | ||
}>((resolve) => { | ||
clientSocket.on(SOCKET_EVENTS.DEPLOYED_BACKEND_RESOURCES, (data) => { | ||
resolve(data); | ||
}); | ||
}); | ||
|
||
// Request backend resources | ||
clientSocket.emit(SOCKET_EVENTS.GET_DEPLOYED_BACKEND_RESOURCES); | ||
|
||
// Wait for the response | ||
const response = await responseReceived; | ||
|
||
// Verify the appropriate error response | ||
assert.strictEqual(response.status, 'error'); | ||
assert.ok(response.error.includes('ThrottlingException')); | ||
|
||
// Verify appropriate logs were made | ||
const mockLogFn = mockPrinter.log as unknown as MockFn; | ||
const errorLogCalls = mockLogFn.mock.calls.filter((call) => | ||
String(call.arguments[0]).includes('ThrottlingException'), | ||
); | ||
assert.strictEqual(errorLogCalls.length >= 1, true); | ||
}); | ||
|
||
void it('should recover from storage corruption', async () => { | ||
// Simulate corrupted storage by mocking loadResources to throw an error | ||
const originalLoadResources = storageManager.loadResources; | ||
storageManager.loadResources = () => null; | ||
Comment on lines
+174
to
+176
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comments says "throw an error". Replacement function (which seems like maybe it should be a mock?) simply returns |
||
|
||
// Set up a promise that will resolve when we receive resources | ||
const resourcesReceived = new Promise<unknown>((resolve) => { | ||
clientSocket.on(SOCKET_EVENTS.SAVED_RESOURCES, (data) => { | ||
resolve(data); | ||
}); | ||
}); | ||
|
||
// Request saved resources | ||
clientSocket.emit(SOCKET_EVENTS.GET_SAVED_RESOURCES); | ||
Comment on lines
+178
to
+186
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seeing this socket request/response pattern makes me wish we'd invested a little time in creating a protocol to handle these details under the hood -- to make these requests look more like "normal" RPC calls. In lieu of that, for now, can we at lest rename "response" type event names to clearly indicate that they are in fact in response to a "call". Would be interested in a naming convention proposal. E.g., a straw man:
Or something like that. |
||
|
||
// Wait for the response | ||
const resources = await resourcesReceived; | ||
|
||
// Should recover by returning an empty array instead of crashing | ||
assert.deepStrictEqual(resources, []); | ||
Comment on lines
+191
to
+192
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test description is pretty lofty -- touting "recovery" from "corruption." (Sounds heroic, actually!) But, this test doesn't really demonstrate recovery. It seems to demonstrate that a |
||
|
||
// Restore the original method | ||
storageManager.loadResources = originalLoadResources; | ||
}); | ||
|
||
void it('should handle network timeouts gracefully', async () => { | ||
// Setup timeout simulation with a real delay | ||
( | ||
mockResourceService.getDeployedBackendResources as unknown as MockFn | ||
).mock.mockImplementation( | ||
() => | ||
new Promise<unknown>((_resolve, reject) => { | ||
setTimeout(() => reject(new Error('Request timed out')), 500); | ||
}), | ||
); | ||
|
||
// Set up a promise that will resolve when we receive an error response | ||
const responseReceived = new Promise<{ | ||
status: string; | ||
error: string; | ||
}>((resolve) => { | ||
clientSocket.on(SOCKET_EVENTS.DEPLOYED_BACKEND_RESOURCES, (data) => { | ||
resolve(data); | ||
}); | ||
}); | ||
|
||
// Request backend resources | ||
clientSocket.emit(SOCKET_EVENTS.GET_DEPLOYED_BACKEND_RESOURCES); | ||
|
||
// Wait for the response with a timeout longer than our simulated delay | ||
const response = await responseReceived; | ||
|
||
// Verify the appropriate error response | ||
assert.strictEqual(response.status, 'error'); | ||
assert.ok(response.error.includes('timed out')); | ||
}); | ||
|
||
void it('should handle authentication errors', async () => { | ||
// Setup authentication error simulation | ||
const authError = new Error( | ||
'The security token included in the request is invalid', | ||
); | ||
authError.name = 'InvalidCredentialsException'; | ||
|
||
( | ||
mockResourceService.getDeployedBackendResources as unknown as MockFn | ||
).mock.mockImplementation(() => Promise.reject(authError)); | ||
|
||
// Set up a promise that will resolve when we receive an error response | ||
const responseReceived = new Promise<{ | ||
status: string; | ||
error: string; | ||
}>((resolve) => { | ||
clientSocket.on(SOCKET_EVENTS.DEPLOYED_BACKEND_RESOURCES, (data) => { | ||
resolve(data); | ||
}); | ||
}); | ||
|
||
// Request backend resources | ||
clientSocket.emit(SOCKET_EVENTS.GET_DEPLOYED_BACKEND_RESOURCES); | ||
|
||
// Wait for the response | ||
const response = await responseReceived; | ||
|
||
// Verify the appropriate error response | ||
assert.strictEqual(response.status, 'error'); | ||
assert.ok(response.error.includes('InvalidCredentialsException')); | ||
}); | ||
|
||
void it('should handle cascading failures with graceful degradation', async () => { | ||
// First, make the resource service fail | ||
( | ||
mockResourceService.getDeployedBackendResources as unknown as MockFn | ||
).mock.mockImplementation(() => | ||
Promise.reject(new Error('Service failure')), | ||
); | ||
|
||
// Second, make sandbox state checking fail | ||
(mockGetSandboxState as unknown as MockFn).mock.mockImplementation(() => | ||
Promise.reject(new Error('State check failure')), | ||
); | ||
|
||
// Third, simulate corrupted storage | ||
const originalLoadResources = storageManager.loadResources; | ||
storageManager.loadResources = () => { | ||
throw new Error('Failed to parse JSON: Unexpected token'); | ||
}; | ||
|
||
// Set up promises that will resolve when we receive responses | ||
const resourcesResponseReceived = new Promise<{ | ||
status: string; | ||
error: string; | ||
}>((resolve) => { | ||
clientSocket.on(SOCKET_EVENTS.DEPLOYED_BACKEND_RESOURCES, (data) => { | ||
resolve(data); | ||
}); | ||
}); | ||
|
||
const statusResponseReceived = new Promise<SandboxStatusData>((resolve) => { | ||
clientSocket.on(SOCKET_EVENTS.SANDBOX_STATUS, (data) => { | ||
resolve(data); | ||
}); | ||
}); | ||
|
||
// Request multiple things simultaneously | ||
clientSocket.emit(SOCKET_EVENTS.GET_DEPLOYED_BACKEND_RESOURCES); | ||
clientSocket.emit(SOCKET_EVENTS.GET_SANDBOX_STATUS); | ||
clientSocket.emit(SOCKET_EVENTS.GET_SAVED_RESOURCES); | ||
|
||
// Wait for responses | ||
const resourcesResponse = await resourcesResponseReceived; | ||
const statusResponse = await statusResponseReceived; | ||
|
||
// Verify we got appropriate error responses | ||
assert.strictEqual(resourcesResponse.status, 'error'); | ||
assert.ok(resourcesResponse.error.includes('Service failure')); | ||
|
||
assert.strictEqual(statusResponse.status, 'unknown'); | ||
// Use optional chaining to handle possibly undefined error | ||
assert.ok(statusResponse.error?.includes('State check failure')); | ||
|
||
// Make sure the app is still functional after cascading errors | ||
// Now let's fix one of the services and make sure it works | ||
(mockGetSandboxState as unknown as MockFn).mock.mockImplementation(() => | ||
Promise.resolve('running'), | ||
); | ||
|
||
// Restore storage functionality | ||
storageManager.loadResources = originalLoadResources; | ||
|
||
const recoveredStatusPromise = new Promise<SandboxStatusData>((resolve) => { | ||
clientSocket.on(SOCKET_EVENTS.SANDBOX_STATUS, (data) => { | ||
resolve(data); | ||
}); | ||
}); | ||
|
||
clientSocket.emit(SOCKET_EVENTS.GET_SANDBOX_STATUS); | ||
|
||
const recoveredStatus = await recoveredStatusPromise; | ||
assert.strictEqual(recoveredStatus.status, 'running'); | ||
}); | ||
}); |
Check warning
Code scanning / CodeQL
Superfluous trailing arguments Warning