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'
2023-12-01 01:32:45 +00:00
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
2023-12-11 22:07:48 +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
2023-12-11 22:07:48 +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 )
} )
2024-04-19 14:03:47 +00:00
it ( 'should retry if invalid body response' , async ( ) = > {
const mockPost = jest
. fn ( ( ) = > {
const msgSucceeded = new http . IncomingMessage ( new net . Socket ( ) )
msgSucceeded . statusCode = 200
return {
message : msgSucceeded ,
readBody : async ( ) = > {
return Promise . resolve (
` {"ok": true, "signedUploadUrl": "http://localhost:8080/upload"} `
)
}
}
} )
. mockImplementationOnce ( ( ) = > {
const msgFailed = new http . IncomingMessage ( new net . Socket ( ) )
msgFailed . statusCode = 502
msgFailed . statusMessage = 'Bad Gateway'
return {
message : msgFailed ,
readBody : async ( ) = > {
return Promise . resolve ( '💥' )
}
}
} )
const mockHttpClient = (
HttpClient as unknown as jest . Mock
) . mockImplementation ( ( ) = > {
return {
post : mockPost
}
} )
const client = internalArtifactTwirpClient ( clientOptions )
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 )
} )
2023-08-08 20:19:43 +00:00
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
}
} )
2023-12-11 22:07:48 +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
}
} )
2023-12-11 22:07:48 +00:00
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 )
} )
2023-12-11 16:26:54 +00:00
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
}
} )
2023-12-11 22:07:48 +00:00
const client = internalArtifactTwirpClient ( clientOptions )
2023-12-11 16:26:54 +00:00
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 )
} )
2023-12-11 22:07:48 +00:00
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 )
2023-12-11 22:07:48 +00:00
expect ( mockHttpClient ) . toHaveBeenCalledTimes ( 1 )
expect ( mockPost ) . toHaveBeenCalledTimes ( 1 )
} )
2023-08-04 20:00:58 +00:00
} )