diff --git a/packages/artifact/__tests__/artifact-http-client.test.ts b/packages/artifact/__tests__/artifact-http-client.test.ts index 0477b731..91327114 100644 --- a/packages/artifact/__tests__/artifact-http-client.test.ts +++ b/packages/artifact/__tests__/artifact-http-client.test.ts @@ -1,183 +1,200 @@ -import * as http from "http" -import * as net from "net" -import { HttpClient } from "@actions/http-client" -import * as config from "../src/internal/shared/config" -import { createArtifactTwirpClient } from "../src/internal/shared/artifact-twirp-client" -import * as core from "@actions/core" +import * as http from 'http' +import * as net from 'net' +import {HttpClient} from '@actions/http-client' +import * as config from '../src/internal/shared/config' +import {createArtifactTwirpClient} from '../src/internal/shared/artifact-twirp-client' +import * as core from '@actions/core' -jest.mock("@actions/http-client") +jest.mock('@actions/http-client') -describe("artifact-http-client", () => { +describe('artifact-http-client', () => { beforeAll(() => { // mock all output so that there is less noise when running tests - jest.spyOn(console, "log").mockImplementation(() => {}) - jest.spyOn(core, "debug").mockImplementation(() => {}) - jest.spyOn(core, "info").mockImplementation(() => {}) - jest.spyOn(core, "warning").mockImplementation(() => {}) - jest.spyOn(config, "getResultsServiceUrl").mockReturnValue("http://localhost:8080") - jest.spyOn(config, "getRuntimeToken").mockReturnValue("token") + jest.spyOn(console, 'log').mockImplementation(() => {}) + jest.spyOn(core, 'debug').mockImplementation(() => {}) + jest.spyOn(core, 'info').mockImplementation(() => {}) + jest.spyOn(core, 'warning').mockImplementation(() => {}) + jest + .spyOn(config, 'getResultsServiceUrl') + .mockReturnValue('http://localhost:8080') + jest.spyOn(config, 'getRuntimeToken').mockReturnValue('token') }) beforeEach(() => { jest.clearAllMocks() }) - it("should successfully create a client", () => { - const client = createArtifactTwirpClient("upload") + it('should successfully create a client', () => { + const client = createArtifactTwirpClient('upload') expect(client).toBeDefined() }) - it("should make a request", async () => { + it('should make a request', async () => { const mockPost = jest.fn(() => { const msg = new http.IncomingMessage(new net.Socket()) msg.statusCode = 200 return { message: msg, - readBody: () => {return Promise.resolve(`{"ok": true, "signedUploadUrl": "http://localhost:8080/upload"}`)} + readBody: () => { + return Promise.resolve( + `{"ok": true, "signedUploadUrl": "http://localhost:8080/upload"}` + ) + } } }) - const mockHttpClient = (HttpClient as unknown as jest.Mock).mockImplementation(() => { + const mockHttpClient = ( + HttpClient as unknown as jest.Mock + ).mockImplementation(() => { return { post: mockPost } }) - const client = createArtifactTwirpClient("upload") - const artifact = await client.CreateArtifact( - { - workflowRunBackendId: "1234", - workflowJobRunBackendId: "5678", - name: "artifact", - version: 4 - } - ) + const client = createArtifactTwirpClient('upload') + const artifact = await client.CreateArtifact({ + workflowRunBackendId: '1234', + workflowJobRunBackendId: '5678', + name: 'artifact', + version: 4 + }) - expect(mockHttpClient).toHaveBeenCalledTimes(1) + expect(mockHttpClient).toHaveBeenCalledTimes(1) expect(mockPost).toHaveBeenCalledTimes(1) expect(artifact).toBeDefined() expect(artifact.ok).toBe(true) - expect(artifact.signedUploadUrl).toBe("http://localhost:8080/upload") + expect(artifact.signedUploadUrl).toBe('http://localhost:8080/upload') }) - it("should retry if the request fails", async () => { + it('should retry if the request fails', async () => { const mockPost = jest - .fn(() => { - const msgSucceeded = new http.IncomingMessage(new net.Socket()) - msgSucceeded.statusCode = 200 - return { - message: msgSucceeded, - readBody: () => {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, - readBody: () => {return Promise.resolve(`{"ok": false}`)} - } - }) - const mockHttpClient = (HttpClient as unknown as jest.Mock).mockImplementation(() => { + .fn(() => { + const msgSucceeded = new http.IncomingMessage(new net.Socket()) + msgSucceeded.statusCode = 200 + return { + message: msgSucceeded, + readBody: () => { + 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, + readBody: () => { + return Promise.resolve(`{"ok": false}`) + } + } + }) + const mockHttpClient = ( + HttpClient as unknown as jest.Mock + ).mockImplementation(() => { return { post: mockPost } }) const client = createArtifactTwirpClient( - "upload", + 'upload', 5, // retry 5 times 1, // wait 1 ms 1.5 // backoff factor ) - const artifact = await client.CreateArtifact( - { - workflowRunBackendId: "1234", - workflowJobRunBackendId: "5678", - name: "artifact", - version: 4 - } - ) + 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(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(() => { - const msgFailed = new http.IncomingMessage(new net.Socket()) - msgFailed.statusCode = 500 - msgFailed.statusMessage = "Internal Server Error" - return { - message: msgFailed, - readBody: () => {return Promise.resolve(`{"ok": false}`)} + it('should fail if the request fails 5 times', async () => { + const mockPost = jest.fn(() => { + const msgFailed = new http.IncomingMessage(new net.Socket()) + msgFailed.statusCode = 500 + msgFailed.statusMessage = 'Internal Server Error' + return { + message: msgFailed, + readBody: () => { + return Promise.resolve(`{"ok": false}`) } - }) + } + }) - const mockHttpClient = (HttpClient as unknown as jest.Mock).mockImplementation(() => { - return { - post: mockPost - } - }) - const client = createArtifactTwirpClient( - "upload", - 5, // retry 5 times - 1, // wait 1 ms - 1.5 // backoff factor - ) + const mockHttpClient = ( + HttpClient as unknown as jest.Mock + ).mockImplementation(() => { + return { + post: mockPost + } + }) + const client = createArtifactTwirpClient( + 'upload', + 5, // retry 5 times + 1, // wait 1 ms + 1.5 // backoff factor + ) await expect(async () => { - await client.CreateArtifact( - { - workflowRunBackendId: "1234", - workflowJobRunBackendId: "5678", - name: "artifact", - version: 4 - } - ) - }).rejects.toThrowError("Failed to make request after 5 attempts: Failed request: (500) Internal Server Error") + await client.CreateArtifact({ + workflowRunBackendId: '1234', + workflowJobRunBackendId: '5678', + name: 'artifact', + version: 4 + }) + }).rejects.toThrowError( + 'Failed to make request after 5 attempts: Failed request: (500) Internal Server Error' + ) expect(mockHttpClient).toHaveBeenCalledTimes(1) expect(mockPost).toHaveBeenCalledTimes(5) }) - - 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, - readBody: () => {return Promise.resolve(`{"ok": false}`)} - } - }) - const mockHttpClient = (HttpClient as unknown as jest.Mock).mockImplementation(() => { - return { - post: mockPost + 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, + readBody: () => { + return Promise.resolve(`{"ok": false}`) } - }) - const client = createArtifactTwirpClient( - "upload", - 5, // retry 5 times - 1, // wait 1 ms - 1.5 // backoff factor - ) + } + }) + + const mockHttpClient = ( + HttpClient as unknown as jest.Mock + ).mockImplementation(() => { + return { + post: mockPost + } + }) + const client = createArtifactTwirpClient( + 'upload', + 5, // retry 5 times + 1, // wait 1 ms + 1.5 // backoff factor + ) await expect(async () => { - await client.CreateArtifact( - { - workflowRunBackendId: "1234", - workflowJobRunBackendId: "5678", - name: "artifact", - version: 4 - } - ) - }).rejects.toThrowError("Received non-retryable error: Failed request: (401) Unauthorized") + await client.CreateArtifact({ + workflowRunBackendId: '1234', + workflowJobRunBackendId: '5678', + name: 'artifact', + version: 4 + }) + }).rejects.toThrowError( + 'Received non-retryable error: Failed request: (401) Unauthorized' + ) expect(mockHttpClient).toHaveBeenCalledTimes(1) expect(mockPost).toHaveBeenCalledTimes(1) }) }) - diff --git a/packages/artifact/src/internal/shared/artifact-twirp-client.ts b/packages/artifact/src/internal/shared/artifact-twirp-client.ts index 97b1b5f6..e582db45 100644 --- a/packages/artifact/src/internal/shared/artifact-twirp-client.ts +++ b/packages/artifact/src/internal/shared/artifact-twirp-client.ts @@ -1,15 +1,15 @@ -import { HttpClient, HttpClientResponse, HttpCodes } from "@actions/http-client" -import { BearerCredentialHandler } from "@actions/http-client/lib/auth" -import { info } from "@actions/core" -import { ArtifactServiceClientJSON } from "../../generated" -import { getResultsServiceUrl, getRuntimeToken } from "./config" +import {HttpClient, HttpClientResponse, HttpCodes} from '@actions/http-client' +import {BearerCredentialHandler} from '@actions/http-client/lib/auth' +import {info} from '@actions/core' +import {ArtifactServiceClientJSON} from '../../generated' +import {getResultsServiceUrl, getRuntimeToken} from './config' // The twirp http client must implement this interface interface Rpc { request( service: string, method: string, - contentType: "application/json" | "application/protobuf", + contentType: 'application/json' | 'application/protobuf', data: object | Uint8Array ): Promise } @@ -21,7 +21,12 @@ class ArtifactHttpClient implements Rpc { private baseRetryIntervalMilliseconds: number = 3000 private retryMultiplier: number = 1.5 - constructor(userAgent: string, maxAttempts?: number, baseRetryIntervalMilliseconds?: number, retryMultiplier?: number) { + constructor( + userAgent: string, + maxAttempts?: number, + baseRetryIntervalMilliseconds?: number, + retryMultiplier?: number + ) { const token = getRuntimeToken() this.baseUrl = getResultsServiceUrl() if (maxAttempts) { @@ -34,10 +39,9 @@ class ArtifactHttpClient implements Rpc { this.retryMultiplier = retryMultiplier } - this.httpClient = new HttpClient( - userAgent, - [new BearerCredentialHandler(token)], - ) + this.httpClient = new HttpClient(userAgent, [ + new BearerCredentialHandler(token) + ]) } // This function satisfies the Rpc interface. It is compatible with the JSON @@ -45,17 +49,19 @@ class ArtifactHttpClient implements Rpc { async request( service: string, method: string, - contentType: "application/json" | "application/protobuf", + contentType: 'application/json' | 'application/protobuf', data: object | Uint8Array ): Promise { let url = `${this.baseUrl}/twirp/${service}/${method}` let headers = { - "Content-Type": contentType, + 'Content-Type': contentType } info(`Making request to ${url} with data: ${JSON.stringify(data)}`) try { - const response = await this.retryableRequest(() => this.httpClient.post(url, JSON.stringify(data), headers)) + const response = await this.retryableRequest(() => + this.httpClient.post(url, JSON.stringify(data), headers) + ) const body = await response.readBody() return JSON.parse(body) } catch (error) { @@ -64,10 +70,10 @@ class ArtifactHttpClient implements Rpc { } async retryableRequest( - operation: () => Promise, + operation: () => Promise ): Promise { let attempt = 0 - let errorMessage = "" + let errorMessage = '' while (attempt < this.maxAttempts) { let isRetryable = false @@ -91,12 +97,17 @@ class ArtifactHttpClient implements Rpc { } if (attempt + 1 === this.maxAttempts) { - throw new Error(`Failed to make request after ${this.maxAttempts} attempts: ${errorMessage}`) + throw new Error( + `Failed to make request after ${this.maxAttempts} attempts: ${errorMessage}` + ) } - const retryTimeMilliseconds = this.getExponentialRetryTimeMilliseconds(attempt) + const retryTimeMilliseconds = + this.getExponentialRetryTimeMilliseconds(attempt) info( - `Attempt ${attempt + 1} of ${this.maxAttempts} failed with error: ${errorMessage}. Retrying request in ${retryTimeMilliseconds} ms...` + `Attempt ${attempt + 1} of ${ + this.maxAttempts + } failed with error: ${errorMessage}. Retrying request in ${retryTimeMilliseconds} ms...` ) await this.sleep(retryTimeMilliseconds) attempt++ @@ -131,14 +142,15 @@ class ArtifactHttpClient implements Rpc { getExponentialRetryTimeMilliseconds(attempt: number): number { if (attempt < 0) { - throw new Error("attempt should be a positive integer") + throw new Error('attempt should be a positive integer') } if (attempt === 0) { return this.baseRetryIntervalMilliseconds } - const minTime = this.baseRetryIntervalMilliseconds * this.retryMultiplier ** (attempt) + const minTime = + this.baseRetryIntervalMilliseconds * this.retryMultiplier ** attempt const maxTime = minTime * this.retryMultiplier // returns a random number between minTime and maxTime (exclusive) @@ -147,15 +159,15 @@ class ArtifactHttpClient implements Rpc { } export function createArtifactTwirpClient( - type: "upload" | "download", + type: 'upload' | 'download', maxAttempts?: number, baseRetryIntervalMilliseconds?: number, retryMultiplier?: number ): ArtifactServiceClientJSON { const client = new ArtifactHttpClient( - `@actions/artifact-${type}`, - maxAttempts, - baseRetryIntervalMilliseconds, + `@actions/artifact-${type}`, + maxAttempts, + baseRetryIntervalMilliseconds, retryMultiplier ) return new ArtifactServiceClientJSON(client)