277 lines
9.9 KiB
TypeScript
277 lines
9.9 KiB
TypeScript
import { RequestError } from "@octokit/request-error"
|
|
import { Artifact } from "../src/Artifact"
|
|
import { GithubArtifactUploader } from "../src/ArtifactUploader"
|
|
import type { Releases } from "../src/Releases"
|
|
|
|
const artifacts = [new Artifact("a/art1"), new Artifact("b/art2")]
|
|
const fakeReadStream = {}
|
|
const contentLength = 42
|
|
const releaseId = 100
|
|
const url = "http://api.example.com"
|
|
|
|
const deleteMock = jest.fn()
|
|
const listArtifactsMock = jest.fn()
|
|
const uploadMock = jest.fn()
|
|
|
|
// Mock response with browser_download_url
|
|
const mockUploadResponse = (name: string) => ({
|
|
data: {
|
|
browser_download_url: `https://github.com/octocat/Hello-World/releases/download/v1.0.0/${name}`,
|
|
name: name,
|
|
id: 1,
|
|
}
|
|
})
|
|
|
|
jest.mock("fs", () => {
|
|
const originalFs = jest.requireActual("fs")
|
|
return {
|
|
...originalFs,
|
|
promises: {},
|
|
createReadStream: () => fakeReadStream,
|
|
statSync: () => {
|
|
return { size: contentLength }
|
|
},
|
|
}
|
|
})
|
|
|
|
describe("ArtifactUploader", () => {
|
|
beforeEach(() => {
|
|
deleteMock.mockClear()
|
|
listArtifactsMock.mockClear()
|
|
uploadMock.mockClear()
|
|
})
|
|
|
|
it("returns asset URLs when upload succeeds", async () => {
|
|
mockListWithoutAssets()
|
|
mockUploadSuccess()
|
|
const uploader = createUploader(true)
|
|
|
|
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
|
|
|
|
expect(result).toEqual({
|
|
"art1": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art1",
|
|
"art2": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art2",
|
|
})
|
|
expect(uploadMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it("returns empty object when no artifacts are uploaded", async () => {
|
|
const uploader = createUploader(true)
|
|
|
|
const result = await uploader.uploadArtifacts([], releaseId, url)
|
|
|
|
expect(result).toEqual({})
|
|
expect(uploadMock).toHaveBeenCalledTimes(0)
|
|
})
|
|
|
|
it("excludes failed uploads from returned URLs", async () => {
|
|
mockListWithoutAssets()
|
|
mockUploadArtifact(401, 2)
|
|
const uploader = createUploader(true)
|
|
|
|
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
|
|
|
|
expect(result).toEqual({})
|
|
expect(uploadMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it("abort when upload failed with non-5xx response", async () => {
|
|
mockListWithoutAssets()
|
|
mockUploadArtifact(401, 2)
|
|
const uploader = createUploader(true)
|
|
|
|
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
|
|
|
|
expect(result).toEqual({})
|
|
expect(uploadMock).toHaveBeenCalledTimes(2)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art2", releaseId)
|
|
|
|
expect(deleteMock).toHaveBeenCalledTimes(0)
|
|
})
|
|
|
|
it("abort when upload failed with 5xx response after 3 attempts", async () => {
|
|
mockListWithoutAssets()
|
|
mockUploadArtifact(500, 4)
|
|
const uploader = createUploader(true)
|
|
|
|
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
|
|
|
|
expect(result).toEqual({})
|
|
expect(uploadMock).toHaveBeenCalledTimes(5)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art2", releaseId)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art2", releaseId)
|
|
|
|
expect(deleteMock).toHaveBeenCalledTimes(0)
|
|
})
|
|
|
|
it("replaces all artifacts", async () => {
|
|
mockDeleteSuccess()
|
|
mockListWithAssets()
|
|
mockUploadSuccess()
|
|
const uploader = createUploader(true)
|
|
|
|
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
|
|
|
|
expect(result).toEqual({
|
|
"art1": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art1",
|
|
"art2": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art2",
|
|
})
|
|
expect(uploadMock).toHaveBeenCalledTimes(2)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art2", releaseId)
|
|
|
|
expect(deleteMock).toHaveBeenCalledTimes(2)
|
|
expect(deleteMock).toHaveBeenCalledWith(1)
|
|
expect(deleteMock).toHaveBeenCalledWith(2)
|
|
})
|
|
|
|
it("replaces no artifacts when previous asset list empty", async () => {
|
|
mockDeleteSuccess()
|
|
mockListWithoutAssets()
|
|
mockUploadSuccess()
|
|
const uploader = createUploader(true)
|
|
|
|
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
|
|
|
|
expect(result).toEqual({
|
|
"art1": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art1",
|
|
"art2": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art2",
|
|
})
|
|
expect(uploadMock).toHaveBeenCalledTimes(2)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art2", releaseId)
|
|
|
|
expect(deleteMock).toHaveBeenCalledTimes(0)
|
|
})
|
|
|
|
it("retry when upload failed with 5xx response", async () => {
|
|
mockListWithoutAssets()
|
|
mockUploadArtifact(500, 2)
|
|
const uploader = createUploader(true)
|
|
|
|
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
|
|
|
|
expect(result).toEqual({})
|
|
expect(uploadMock).toHaveBeenCalledTimes(4)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art2", releaseId)
|
|
|
|
expect(deleteMock).toHaveBeenCalledTimes(0)
|
|
})
|
|
|
|
it("throws upload error when replacesExistingArtifacts is true", async () => {
|
|
mockListWithoutAssets()
|
|
mockUploadError()
|
|
const uploader = createUploader(true, true)
|
|
|
|
expect.hasAssertions()
|
|
try {
|
|
await uploader.uploadArtifacts(artifacts, releaseId, url)
|
|
} catch (error) {
|
|
expect(error).toEqual(Error("Failed to upload artifact art1. error."))
|
|
}
|
|
})
|
|
|
|
it("throws error from replace", async () => {
|
|
mockDeleteError()
|
|
mockListWithAssets()
|
|
mockUploadSuccess()
|
|
const uploader = createUploader(true)
|
|
|
|
expect.hasAssertions()
|
|
try {
|
|
await uploader.uploadArtifacts(artifacts, releaseId, url)
|
|
} catch (error) {
|
|
expect(error).toEqual("error")
|
|
}
|
|
})
|
|
|
|
it("updates all artifacts, delete none", async () => {
|
|
mockDeleteError()
|
|
mockListWithAssets()
|
|
mockUploadSuccess()
|
|
const uploader = createUploader(false)
|
|
|
|
const result = await uploader.uploadArtifacts(artifacts, releaseId, url)
|
|
|
|
expect(result).toEqual({
|
|
"art1": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art1",
|
|
"art2": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/art2",
|
|
})
|
|
expect(uploadMock).toHaveBeenCalledTimes(2)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art1", releaseId)
|
|
expect(uploadMock).toHaveBeenCalledWith(url, contentLength, "raw", fakeReadStream, "art2", releaseId)
|
|
|
|
expect(deleteMock).toHaveBeenCalledTimes(0)
|
|
})
|
|
|
|
function createUploader(replaces: boolean, throws = false): GithubArtifactUploader {
|
|
const MockReleases = jest.fn<Releases, any>(() => {
|
|
return {
|
|
create: jest.fn(),
|
|
deleteArtifact: deleteMock,
|
|
getByTag: jest.fn(),
|
|
listArtifactsForRelease: listArtifactsMock,
|
|
listReleases: jest.fn(),
|
|
update: jest.fn(),
|
|
uploadArtifact: uploadMock,
|
|
generateReleaseNotes: jest.fn(),
|
|
}
|
|
})
|
|
return new GithubArtifactUploader(new MockReleases(), replaces, throws)
|
|
}
|
|
|
|
function mockDeleteError(): any {
|
|
deleteMock.mockRejectedValue("error")
|
|
}
|
|
|
|
function mockDeleteSuccess(): any {
|
|
deleteMock.mockResolvedValue({})
|
|
}
|
|
|
|
function mockListWithAssets() {
|
|
listArtifactsMock.mockResolvedValue([
|
|
{
|
|
name: "art1",
|
|
id: 1,
|
|
},
|
|
{
|
|
name: "art2",
|
|
id: 2,
|
|
},
|
|
])
|
|
}
|
|
|
|
function mockListWithoutAssets() {
|
|
listArtifactsMock.mockResolvedValue([])
|
|
}
|
|
|
|
function mockUploadSuccess() {
|
|
uploadMock.mockImplementation((_, __, ___, ____, name) => Promise.resolve(mockUploadResponse(name)))
|
|
}
|
|
|
|
function mockUploadArtifact(status = 200, failures = 0) {
|
|
const error = new RequestError(`HTTP ${status}`, status, {
|
|
headers: {},
|
|
request: { method: "GET", url: "", headers: {} },
|
|
})
|
|
for (let index = 0; index < failures; index++) {
|
|
uploadMock.mockRejectedValueOnce(error)
|
|
}
|
|
uploadMock.mockResolvedValue({})
|
|
}
|
|
|
|
function mockUploadError() {
|
|
uploadMock.mockRejectedValue({
|
|
message: "error",
|
|
status: 502,
|
|
})
|
|
}
|
|
})
|