|
1 | 1 | import { runSnykCLI } from '../util/runSnykCLI'; |
2 | 2 | import { isWindowsOperatingSystem, describeIf } from '../../utils'; |
3 | 3 | import { EXIT_CODES } from '../../../src/cli/exit-codes'; |
| 4 | +import { FakeServer, fakeServer, getFirstIPv4Address } from '../../acceptance/fake-server'; |
| 5 | +import { getAvailableServerPort } from '../util/getServerPort'; |
| 6 | +const { promisify } = require('util'); |
4 | 7 |
|
5 | 8 | jest.setTimeout(1000 * 60); |
6 | 9 |
|
@@ -38,4 +41,143 @@ describe('exit code behaviour - general', () => { |
38 | 41 |
|
39 | 42 | expect(code).toEqual(EXIT_CODES.EX_UNAVAILABLE); |
40 | 43 | }); |
| 44 | + |
| 45 | + describe.only('[SNYK-0099] Maintenance error', () => { |
| 46 | + let server: FakeServer; |
| 47 | + let apiPort: string; |
| 48 | + let fakeServerUrl: string; |
| 49 | + let fakeServerHost: string; |
| 50 | + let fakeServerEnv: Record<string, string>; |
| 51 | + |
| 52 | + const serverToken = 'random'; |
| 53 | + const apiPath = '/api/v1'; |
| 54 | + |
| 55 | + const errorObject = { |
| 56 | + jsonapi: { version: '1.0' }, |
| 57 | + errors: [ |
| 58 | + { |
| 59 | + id: '11111111-2222-3333-4444-555555555555', |
| 60 | + links: { |
| 61 | + about: |
| 62 | + 'https://docs.snyk.io/scan-with-snyk/error-catalog#snyk-0099', |
| 63 | + }, |
| 64 | + status: '503', |
| 65 | + code: 'SNYK-0099', |
| 66 | + title: 'Unavailable due to maintenance', |
| 67 | + detail: '', |
| 68 | + meta: { |
| 69 | + links: [ |
| 70 | + 'https://status.snyk.io/', |
| 71 | + 'https://privatecloudstatus.snyk.io', |
| 72 | + ], |
| 73 | + isErrorCatalogError: true, |
| 74 | + classification: 'UNSUPPORTED', |
| 75 | + level: 'error', |
| 76 | + }, |
| 77 | + }, |
| 78 | + ], |
| 79 | + description: |
| 80 | + 'We are currently unavailable due to a maintenance window. For additional information please visit our status pages. Thank you for your patience.', |
| 81 | + }; |
| 82 | + |
| 83 | + beforeEach(async () => { |
| 84 | + apiPort = await getAvailableServerPort(process); |
| 85 | + fakeServerHost = 'http://' + getFirstIPv4Address() + ':' + apiPort; |
| 86 | + fakeServerUrl = fakeServerHost + apiPath; |
| 87 | + fakeServerEnv = { |
| 88 | + ...process.env, |
| 89 | + SNYK_API: fakeServerUrl, |
| 90 | + SNYK_DISABLE_ANALYTICS: '1', |
| 91 | + SNYK_HTTP_PROTOCOL_UPGRADE: '0', |
| 92 | + }; |
| 93 | + |
| 94 | + server = fakeServer(apiPath, serverToken); |
| 95 | + const serverListen = promisify(server.listen); |
| 96 | + await serverListen(apiPort); |
| 97 | + }); |
| 98 | + |
| 99 | + afterEach(async () => { |
| 100 | + const serverClose = promisify(server.close); |
| 101 | + await serverClose(); |
| 102 | + }); |
| 103 | + |
| 104 | + // TODO: unskip this once this is supported |
| 105 | + describe.skip('no retry-after header in error response', () => { |
| 106 | + beforeEach(async () => { |
| 107 | + server.setGlobalResponse( |
| 108 | + errorObject, |
| 109 | + parseInt(errorObject['errors'][0].status), |
| 110 | + ); |
| 111 | + }) |
| 112 | + |
| 113 | + it('Does not attempt any retries', async () => { |
| 114 | + await runSnykCLI(`test -d --log-level=trace`, { |
| 115 | + env: { |
| 116 | + ...fakeServerEnv, |
| 117 | + // apply a user configured attempts of 10 |
| 118 | + INTERNAL_NETWORK_REQUEST_MAX_ATTEMPTS: '10', |
| 119 | + }, |
| 120 | + }); |
| 121 | + |
| 122 | + // // we should expect to override user configured attempts |
| 123 | + // const expectedRetryAfterDebugMsg = 'Retry ultimately failed after 1 attempts'; |
| 124 | + // expect(stderr).toContain(expectedRetryAfterDebugMsg); |
| 125 | + |
| 126 | + // Count how many times an endpoint was hit |
| 127 | + const requests = server.getRequests(); |
| 128 | + const testEndpointHits = requests.filter(r => |
| 129 | + r.url.includes('/test-dep-graph') || r.url.includes('/vuln/') |
| 130 | + ).length; |
| 131 | + |
| 132 | + expect(testEndpointHits).toBe(1); // Only 1 attempt, no retries |
| 133 | + }); |
| 134 | + }); |
| 135 | + |
| 136 | + describe('retry-after header in error response', () => { |
| 137 | + it('Respects retry-after header', async () => { |
| 138 | + server.setGlobalResponse( |
| 139 | + errorObject, |
| 140 | + parseInt(errorObject['errors'][0].status), |
| 141 | + { 'retry-after': '1' }, |
| 142 | + ); |
| 143 | + |
| 144 | + const { stderr } = await runSnykCLI(`test -d --log-level=trace`, { |
| 145 | + env: { |
| 146 | + ...fakeServerEnv, |
| 147 | + INTERNAL_NETWORK_REQUEST_MAX_ATTEMPTS: '2', |
| 148 | + }, |
| 149 | + }); |
| 150 | + |
| 151 | + const expectedRetryAfterDebugMsg = 'Retrying request, reason: retry after 1s'; |
| 152 | + const expectedRetryAfterHeaderDebugMsg = 'Retry-After:[1]'; |
| 153 | + expect(stderr).toContain(expectedRetryAfterDebugMsg); |
| 154 | + expect(stderr).toContain(expectedRetryAfterHeaderDebugMsg); |
| 155 | + |
| 156 | + const requests = server.getRequests(); |
| 157 | + const testEndpointHits = requests.filter(r => |
| 158 | + r.url.includes('/test-dep-graph') || r.url.includes('/vuln/') |
| 159 | + ).length; |
| 160 | + |
| 161 | + expect(testEndpointHits).toBe(2); // expected 2 network attempts |
| 162 | + }); |
| 163 | + }); |
| 164 | + |
| 165 | + it('Correct exit code', async () => { |
| 166 | + server.setGlobalResponse( |
| 167 | + errorObject, |
| 168 | + parseInt(errorObject['errors'][0].status), |
| 169 | + ); |
| 170 | + |
| 171 | + const { code, stdout } = await runSnykCLI(`test`, { |
| 172 | + env: { |
| 173 | + ...fakeServerEnv, |
| 174 | + INTERNAL_NETWORK_REQUEST_MAX_ATTEMPTS: '1', |
| 175 | + }, |
| 176 | + }); |
| 177 | + |
| 178 | + |
| 179 | + expect(stdout).toContain(errorObject['errors'][0].code); |
| 180 | + expect(code).toEqual(EXIT_CODES.EX_TEMPFAIL); |
| 181 | + }); |
| 182 | + }); |
41 | 183 | }); |
0 commit comments