|
1 | | -import { afterEach, expect, test, vi } from 'vitest'; |
2 | | -import { AwsLambdaExtension } from '../src/lambda-extension/aws-lambda-extension'; |
| 1 | +import { afterEach, beforeEach, expect, test, vi } from 'vitest'; |
| 2 | + |
| 3 | +beforeEach(() => { |
| 4 | + vi.resetModules(); |
| 5 | + vi.useFakeTimers(); |
| 6 | + vi.setSystemTime(0); |
| 7 | +}); |
3 | 8 |
|
4 | 9 | afterEach(() => { |
5 | 10 | vi.restoreAllMocks(); |
| 11 | + vi.useRealTimers(); |
6 | 12 | }); |
7 | 13 |
|
8 | | -test('logs polling failures and exits instead of leaving the tunnel running', async () => { |
9 | | - const error = new Error('Extension API connection failed'); |
| 14 | +async function setupExtension() { |
| 15 | + const { AwsLambdaExtension } = await import('../src/lambda-extension/aws-lambda-extension'); |
10 | 16 | vi.spyOn(AwsLambdaExtension.prototype, 'register').mockResolvedValue(); |
11 | 17 | vi.spyOn(AwsLambdaExtension.prototype, 'startSentryTunnel').mockImplementation(() => {}); |
12 | | - vi.spyOn(AwsLambdaExtension.prototype, 'next').mockRejectedValue(error); |
| 18 | + const next = vi.spyOn(AwsLambdaExtension.prototype, 'next').mockImplementation(() => new Promise(() => {})); |
13 | 19 | const log = vi.spyOn(console, 'error').mockImplementation(() => {}); |
14 | 20 | const exit = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); |
| 21 | + return { next, log, exit }; |
| 22 | +} |
| 23 | + |
| 24 | +test.each(['ECONNRESET', 'ECONNREFUSED', 'EPIPE', 'ETIMEDOUT', 'EAI_AGAIN'])( |
| 25 | + 'recovers from %s without exiting', |
| 26 | + async code => { |
| 27 | + const { next, exit, log } = await setupExtension(); |
| 28 | + next.mockRejectedValueOnce(Object.assign(new Error('Connection failed'), { code })).mockResolvedValueOnce(); |
| 29 | + |
| 30 | + const { POLL_RETRY_DELAYS } = await import('../src/lambda-extension/index'); |
| 31 | + await vi.advanceTimersByTimeAsync(0); |
| 32 | + expect(next).toHaveBeenCalledTimes(1); |
| 33 | + await vi.advanceTimersByTimeAsync(POLL_RETRY_DELAYS[0] - 1); |
| 34 | + expect(next).toHaveBeenCalledTimes(1); |
| 35 | + await vi.advanceTimersByTimeAsync(1); |
| 36 | + |
| 37 | + expect(next).toHaveBeenCalledTimes(3); |
| 38 | + expect(exit).not.toHaveBeenCalled(); |
| 39 | + expect(log).not.toHaveBeenCalled(); |
| 40 | + }, |
| 41 | +); |
| 42 | + |
| 43 | +test('logs and exits after exhausting consecutive connection retries', async () => { |
| 44 | + const { next, exit, log } = await setupExtension(); |
| 45 | + const error = Object.assign(new Error('Connection reset'), { code: 'ECONNRESET' }); |
| 46 | + const attempts: number[] = []; |
| 47 | + next.mockImplementation(async () => { |
| 48 | + attempts.push(Date.now()); |
| 49 | + throw error; |
| 50 | + }); |
| 51 | + |
| 52 | + const { POLL_RETRY_DELAYS } = await import('../src/lambda-extension/index'); |
| 53 | + const retryBudget = POLL_RETRY_DELAYS.reduce((total, delay) => total + delay, 0); |
| 54 | + await vi.advanceTimersByTimeAsync(retryBudget - 1); |
| 55 | + expect(exit).not.toHaveBeenCalled(); |
| 56 | + await vi.advanceTimersByTimeAsync(1); |
| 57 | + |
| 58 | + expect(attempts).toEqual([0, POLL_RETRY_DELAYS[0], POLL_RETRY_DELAYS[0] + POLL_RETRY_DELAYS[1], retryBudget]); |
| 59 | + expect(exit).toHaveBeenCalledExactlyOnceWith(1); |
| 60 | + expect(log).toHaveBeenCalledExactlyOnceWith('Error in Lambda Extension', error); |
| 61 | + expect(vi.getTimerCount()).toBe(0); |
| 62 | +}); |
| 63 | + |
| 64 | +test('resets the retry budget after a successful poll', async () => { |
| 65 | + const { next, exit } = await setupExtension(); |
| 66 | + const error = Object.assign(new Error('Connection reset'), { code: 'ECONNRESET' }); |
| 67 | + next |
| 68 | + .mockRejectedValueOnce(error) |
| 69 | + .mockRejectedValueOnce(error) |
| 70 | + .mockResolvedValueOnce() |
| 71 | + .mockRejectedValueOnce(error) |
| 72 | + .mockRejectedValueOnce(error) |
| 73 | + .mockRejectedValueOnce(error) |
| 74 | + .mockResolvedValueOnce(); |
| 75 | + |
| 76 | + const { POLL_RETRY_DELAYS } = await import('../src/lambda-extension/index'); |
| 77 | + const retryBudget = POLL_RETRY_DELAYS.reduce((total, delay) => total + delay, 0); |
| 78 | + await vi.advanceTimersByTimeAsync(POLL_RETRY_DELAYS[0] + POLL_RETRY_DELAYS[1] + retryBudget); |
| 79 | + |
| 80 | + expect(next).toHaveBeenCalledTimes(8); |
| 81 | + expect(exit).not.toHaveBeenCalled(); |
| 82 | +}); |
| 83 | + |
| 84 | +test.each([ |
| 85 | + new Error('Failed to advance to next event: Forbidden'), |
| 86 | + new Error('Failed to advance to next event: Container error'), |
| 87 | + Object.assign(new Error('Invalid URL'), { code: 'ERR_INVALID_URL' }), |
| 88 | +])('logs and exits immediately for $message', async error => { |
| 89 | + const { next, log, exit } = await setupExtension(); |
| 90 | + next.mockRejectedValue(error); |
15 | 91 |
|
16 | 92 | await import('../src/lambda-extension/index'); |
| 93 | + await vi.advanceTimersByTimeAsync(0); |
17 | 94 |
|
18 | | - await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1)); |
19 | | - expect(log).toHaveBeenCalledWith('Error in Lambda Extension', error); |
| 95 | + expect(next).toHaveBeenCalledTimes(1); |
| 96 | + expect(exit).toHaveBeenCalledExactlyOnceWith(1); |
| 97 | + expect(log).toHaveBeenCalledExactlyOnceWith('Error in Lambda Extension', error); |
| 98 | + expect(vi.getTimerCount()).toBe(0); |
20 | 99 | }); |
0 commit comments