1
0
Fork 0
toolkit/packages/artifact/__tests__/artifact-http-client.test.ts

301 lines
8.6 KiB
TypeScript
Raw Normal View History

2023-08-08 20:19:43 +00:00
import * as http from 'http'
import * as net from 'net'
import {HttpClient} from '@actions/http-client'
import * as config from '../src/internal/shared/config'
2023-12-01 00:31:27 +00:00
import {internalArtifactTwirpClient} from '../src/internal/shared/artifact-twirp-client'
import {noopLogs} from './common'
2023-12-12 03:01:08 +00:00
import {NetworkError, UsageError} from '../src/internal/shared/errors'
2023-08-04 20:00:58 +00:00
2023-08-08 20:19:43 +00:00
jest.mock('@actions/http-client')
2023-08-04 20:00:58 +00:00
const clientOptions = {
maxAttempts: 5,
retryIntervalMs: 1,
retryMultiplier: 1.5
}
2023-08-08 20:19:43 +00:00
describe('artifact-http-client', () => {
2023-08-04 20:00:58 +00:00
beforeAll(() => {
2023-12-01 00:31:27 +00:00
noopLogs()
2023-08-08 20:19:43 +00:00
jest
.spyOn(config, 'getResultsServiceUrl')
.mockReturnValue('http://localhost:8080')
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('token')
2023-08-04 20:00:58 +00:00
})
2023-08-07 21:24:58 +00:00
beforeEach(() => {
2023-08-08 19:49:05 +00:00
jest.clearAllMocks()
2023-08-07 21:24:58 +00:00
})
2023-08-08 20:19:43 +00:00
it('should successfully create a client', () => {
2023-12-01 00:31:27 +00:00
const client = internalArtifactTwirpClient()
2023-08-04 20:00:58 +00:00
expect(client).toBeDefined()
})
2023-08-08 20:19:43 +00:00
it('should make a request', async () => {
2023-08-08 19:49:05 +00:00
const mockPost = jest.fn(() => {
2023-08-07 23:26:07 +00:00
const msg = new http.IncomingMessage(new net.Socket())
msg.statusCode = 200
return {
message: msg,
2023-08-09 14:10:43 +00:00
readBody: async () => {
2023-08-08 20:19:43 +00:00
return Promise.resolve(
`{"ok": true, "signedUploadUrl": "http://localhost:8080/upload"}`
)
}
2023-08-07 23:26:07 +00:00
}
})
2023-08-08 20:19:43 +00:00
const mockHttpClient = (
HttpClient as unknown as jest.Mock
).mockImplementation(() => {
2023-08-08 19:49:05 +00:00
return {
post: mockPost
}
})
2023-08-07 23:26:07 +00:00
2023-12-01 00:31:27 +00:00
const client = internalArtifactTwirpClient()
2023-08-08 20:19:43 +00:00
const artifact = await client.CreateArtifact({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678',
name: 'artifact',
version: 4
})
2023-08-07 21:24:58 +00:00
2023-08-08 20:19:43 +00:00
expect(mockHttpClient).toHaveBeenCalledTimes(1)
2023-08-07 23:26:07 +00:00
expect(mockPost).toHaveBeenCalledTimes(1)
2023-08-04 20:00:58 +00:00
expect(artifact).toBeDefined()
2023-08-07 21:24:58 +00:00
expect(artifact.ok).toBe(true)
2023-08-08 20:19:43 +00:00
expect(artifact.signedUploadUrl).toBe('http://localhost:8080/upload')
2023-08-07 21:24:58 +00:00
})
2023-08-08 20:19:43 +00:00
it('should retry if the request fails', async () => {
2023-08-08 19:49:05 +00:00
const mockPost = jest
2023-08-08 20:19:43 +00:00
.fn(() => {
const msgSucceeded = new http.IncomingMessage(new net.Socket())
msgSucceeded.statusCode = 200
return {
message: msgSucceeded,
2023-08-09 14:10:43 +00:00
readBody: async () => {
2023-08-08 20:19:43 +00:00
return Promise.resolve(
`{"ok": true, "signedUploadUrl": "http://localhost:8080/upload"}`
)
}
}
})
.mockImplementationOnce(() => {
const msgFailed = new http.IncomingMessage(new net.Socket())
msgFailed.statusCode = 500
msgFailed.statusMessage = 'Internal Server Error'
return {
message: msgFailed,
2023-08-09 14:10:43 +00:00
readBody: async () => {
2023-08-08 20:19:43 +00:00
return Promise.resolve(`{"ok": false}`)
}
}
})
const mockHttpClient = (
HttpClient as unknown as jest.Mock
).mockImplementation(() => {
2023-08-07 23:26:07 +00:00
return {
2023-08-08 20:19:43 +00:00
post: mockPost
2023-08-07 23:26:07 +00:00
}
2023-08-07 21:24:58 +00:00
})
2023-08-08 20:19:43 +00:00
const client = internalArtifactTwirpClient(clientOptions)
2023-08-08 20:19:43 +00:00
const artifact = await client.CreateArtifact({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678',
name: 'artifact',
version: 4
})
expect(mockHttpClient).toHaveBeenCalledTimes(1)
expect(artifact).toBeDefined()
expect(artifact.ok).toBe(true)
expect(artifact.signedUploadUrl).toBe('http://localhost:8080/upload')
expect(mockPost).toHaveBeenCalledTimes(2)
})
it('should fail if the request fails 5 times', async () => {
const mockPost = jest.fn(() => {
2023-08-08 19:49:05 +00:00
const msgFailed = new http.IncomingMessage(new net.Socket())
msgFailed.statusCode = 500
2023-08-08 20:19:43 +00:00
msgFailed.statusMessage = 'Internal Server Error'
2023-08-07 21:24:58 +00:00
return {
2023-08-08 19:49:05 +00:00
message: msgFailed,
2023-08-09 14:10:43 +00:00
readBody: async () => {
2023-08-08 20:19:43 +00:00
return Promise.resolve(`{"ok": false}`)
}
2023-08-07 21:24:58 +00:00
}
})
2023-08-08 20:19:43 +00:00
const mockHttpClient = (
HttpClient as unknown as jest.Mock
).mockImplementation(() => {
2023-08-07 21:24:58 +00:00
return {
2023-08-08 19:49:05 +00:00
post: mockPost
2023-08-07 21:24:58 +00:00
}
})
const client = internalArtifactTwirpClient(clientOptions)
2023-08-08 20:19:43 +00:00
await expect(async () => {
await client.CreateArtifact({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678',
name: 'artifact',
2023-08-07 21:24:58 +00:00
version: 4
2023-08-08 20:19:43 +00:00
})
}).rejects.toThrowError(
'Failed to make request after 5 attempts: Failed request: (500) Internal Server Error'
2023-08-07 21:24:58 +00:00
)
2023-08-08 19:49:05 +00:00
expect(mockHttpClient).toHaveBeenCalledTimes(1)
2023-08-08 20:19:43 +00:00
expect(mockPost).toHaveBeenCalledTimes(5)
2023-08-04 20:00:58 +00:00
})
2023-08-08 19:49:05 +00:00
2023-08-08 20:19:43 +00:00
it('should fail immediately if there is a non-retryable error', async () => {
const mockPost = jest.fn(() => {
const msgFailed = new http.IncomingMessage(new net.Socket())
msgFailed.statusCode = 401
msgFailed.statusMessage = 'Unauthorized'
return {
message: msgFailed,
2023-08-09 14:10:43 +00:00
readBody: async () => {
2023-08-08 20:19:43 +00:00
return Promise.resolve(`{"ok": false}`)
2023-08-08 19:49:05 +00:00
}
2023-08-08 20:19:43 +00:00
}
})
2023-08-08 19:49:05 +00:00
2023-08-08 20:19:43 +00:00
const mockHttpClient = (
HttpClient as unknown as jest.Mock
).mockImplementation(() => {
return {
post: mockPost
}
})
const client = internalArtifactTwirpClient(clientOptions)
2023-08-08 19:49:05 +00:00
await expect(async () => {
2023-08-08 20:19:43 +00:00
await client.CreateArtifact({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678',
name: 'artifact',
version: 4
2023-08-08 19:49:05 +00:00
})
2023-08-08 20:19:43 +00:00
}).rejects.toThrowError(
'Received non-retryable error: Failed request: (401) Unauthorized'
)
2023-08-08 19:49:05 +00:00
expect(mockHttpClient).toHaveBeenCalledTimes(1)
expect(mockPost).toHaveBeenCalledTimes(1)
})
it('should fail with a descriptive error', async () => {
// 409 duplicate error
const mockPost = jest.fn(() => {
const msgFailed = new http.IncomingMessage(new net.Socket())
msgFailed.statusCode = 409
msgFailed.statusMessage = 'Conflict'
return {
message: msgFailed,
readBody: async () => {
return Promise.resolve(
`{"msg": "an artifact with this name already exists on the workflow run"}`
)
}
}
})
const mockHttpClient = (
HttpClient as unknown as jest.Mock
).mockImplementation(() => {
return {
post: mockPost
}
})
const client = internalArtifactTwirpClient(clientOptions)
await expect(async () => {
await client.CreateArtifact({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678',
name: 'artifact',
version: 4
})
await client.CreateArtifact({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678',
name: 'artifact',
version: 4
})
}).rejects.toThrowError(
'Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run'
)
expect(mockHttpClient).toHaveBeenCalledTimes(1)
expect(mockPost).toHaveBeenCalledTimes(1)
})
it('should properly describe a network failure', async () => {
class FakeNodeError extends Error {
code: string
constructor(code: string) {
super()
this.code = code
}
}
const mockPost = jest.fn(() => {
throw new FakeNodeError('ENOTFOUND')
})
const mockHttpClient = (
HttpClient as unknown as jest.Mock
).mockImplementation(() => {
return {
post: mockPost
}
})
const client = internalArtifactTwirpClient()
await expect(async () => {
await client.CreateArtifact({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678',
name: 'artifact',
version: 4
})
2023-12-12 03:01:08 +00:00
}).rejects.toThrowError(new NetworkError('ENOTFOUND').message)
expect(mockHttpClient).toHaveBeenCalledTimes(1)
expect(mockPost).toHaveBeenCalledTimes(1)
})
it('should properly describe a usage error', async () => {
const mockPost = jest.fn(() => {
const msgFailed = new http.IncomingMessage(new net.Socket())
msgFailed.statusCode = 403
msgFailed.statusMessage = 'Forbidden'
return {
message: msgFailed,
readBody: async () => {
return Promise.resolve(
`{"msg": "insufficient usage to create artifact"}`
)
}
}
})
const mockHttpClient = (
HttpClient as unknown as jest.Mock
).mockImplementation(() => {
return {
post: mockPost
}
})
const client = internalArtifactTwirpClient()
await expect(async () => {
await client.CreateArtifact({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678',
name: 'artifact',
version: 4
})
}).rejects.toThrowError(new UsageError().message)
expect(mockHttpClient).toHaveBeenCalledTimes(1)
expect(mockPost).toHaveBeenCalledTimes(1)
})
2023-08-04 20:00:58 +00:00
})