update tests to remove axios mocks and mock fetch instead

This commit is contained in:
ddivad195
2024-01-30 21:24:54 +00:00
parent 501681319f
commit e5b7da2730
+267 -235
View File
@@ -1,13 +1,10 @@
import { publishOCIArtifact } from '../src/ghcr-client'
import axios from 'axios'
import * as fsHelper from '../src/fs-helper'
import * as ociContainer from '../src/oci-container'
// Mocks
let fsReadFileSyncMock: jest.SpyInstance
let axiosPostMock: jest.SpyInstance
let axiosPutMock: jest.SpyInstance
let axiosHeadMock: jest.SpyInstance
let fetchMock: jest.SpyInstance
const token = 'test-token'
const registry = new URL('https://ghcr.io')
@@ -81,28 +78,48 @@ describe('publishOCIArtifact', () => {
.spyOn(fsHelper, 'readFileContents')
.mockImplementation()
axiosPostMock = jest.spyOn(axios, 'post').mockImplementation()
axiosPutMock = jest.spyOn(axios, 'put').mockImplementation()
axiosHeadMock = jest.spyOn(axios, 'head').mockImplementation()
fetchMock = jest.spyOn(global, 'fetch').mockImplementation()
})
it('publishes layer blobs & then a manifest to the provided registry', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
fetchMock.mockImplementation(async (url, options) => {
if (options.method === 'HEAD') {
validateRequestConfig(url, options)
// Simulate none of the blobs existing currently
return Promise.resolve({
status: 404
})
} else if (options.method === 'POST') {
// Simulate successful initiation of uploads for all blobs & return location
validateRequestConfig(url, options)
return Promise.resolve({
status: 202,
headers: {
get: (header: string) => {
if (header === 'location') {
return `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
}
}
}
})
} else if (options.method === 'PUT') {
// Simulate successful upload of all blobs & then the manifest
validateRequestConfig(url, options)
if ((url as string).includes('manifest')) {
return Promise.resolve({
status: 201,
headers: {
get: (header: string) => {
if (header === 'docker-content-digest') {
return '1234567678'
}
}
}
})
}
return Promise.resolve({
status: 201
})
}
})
@@ -111,22 +128,6 @@ describe('publishOCIArtifact', () => {
return Buffer.from('test')
})
// Simulate successful upload of all blobs & then the manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(201, url, config)
if ((url as string).includes('manifest')) {
return {
status: 201,
headers: { 'docker-content-digest': '1234567678' }
}
}
return {
status: 201
}
})
await publishOCIArtifact(
token,
registry,
@@ -137,28 +138,58 @@ describe('publishOCIArtifact', () => {
testManifest
)
expect(axiosHeadMock).toHaveBeenCalledTimes(3)
expect(axiosPostMock).toHaveBeenCalledTimes(3)
expect(axiosPutMock).toHaveBeenCalledTimes(4)
expect(fetchMock).toHaveBeenCalledTimes(10)
expect(
fetchMock.mock.calls.filter(call => call[1].method === 'HEAD')
).toHaveLength(3)
expect(
fetchMock.mock.calls.filter(call => call[1].method === 'POST')
).toHaveLength(3)
expect(
fetchMock.mock.calls.filter(call => call[1].method === 'PUT')
).toHaveLength(4)
})
it('skips uploading all layer blobs when they all already exist', async () => {
// Simulate all blobs already existing
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(200, url, config)
return {
status: 200
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
fetchMock.mockImplementation(async (url, options) => {
if (options.method === 'HEAD') {
// Simulate none of the blobs existing currently
validateRequestConfig(url, options)
return Promise.resolve({
status: 200
})
} else if (options.method === 'POST') {
// Simulate successful initiation of uploads for all blobs & return location
validateRequestConfig(url, options)
return Promise.resolve({
status: 202,
headers: {
get: (header: string) => {
if (header === 'location') {
return `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
}
}
}
})
} else if (options.method === 'PUT') {
// Simulate successful upload of all blobs & then the manifest
validateRequestConfig(url, options)
if ((url as string).includes('manifest')) {
return Promise.resolve({
status: 201,
headers: {
get: (header: string) => {
if (header === 'docker-content-digest') {
return '1234567678'
}
}
}
})
}
return Promise.resolve({
status: 201
})
}
})
@@ -167,22 +198,6 @@ describe('publishOCIArtifact', () => {
return Buffer.from('test')
})
// Simulate successful upload of all blobs & then the manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(201, url, config)
if ((url as string).includes('manifest')) {
return {
status: 201,
headers: { 'docker-content-digest': '1234567678' }
}
}
return {
status: 201
}
})
await publishOCIArtifact(
token,
registry,
@@ -194,40 +209,66 @@ describe('publishOCIArtifact', () => {
)
// We should only head all the blobs and then upload the manifest
expect(axiosHeadMock).toHaveBeenCalledTimes(3)
expect(axiosPostMock).toHaveBeenCalledTimes(0)
expect(axiosPutMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledTimes(4)
expect(
fetchMock.mock.calls.filter(call => call[1].method === 'HEAD')
).toHaveLength(3)
expect(
fetchMock.mock.calls.filter(call => call[1].method === 'POST')
).toHaveLength(0)
expect(
fetchMock.mock.calls.filter(call => call[1].method === 'PUT')
).toHaveLength(1)
})
it('skips uploading layer blobs that already exist', async () => {
// Simulate some blobs already existing
let count = 0
axiosHeadMock.mockImplementation(async (url, config) => {
fetchMock.mockImplementation(async (url, options) => {
count++
if (count === 1) {
// report the first blob as being there
validateRequestConfig(200, url, config)
return {
status: 200
if (options.method === 'HEAD') {
validateRequestConfig(url, options)
if (count === 1) {
return Promise.resolve({
status: 200
})
} else {
// report all others are missing
return Promise.resolve({
status: 404
})
}
} else {
// report all others are missing
validateRequestConfig(404, url, config)
return {
status: 404
}
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
} else if (options.method === 'POST') {
// Simulate successful initiation of uploads for all blobs & return location
validateRequestConfig(url, options)
return Promise.resolve({
status: 202,
headers: {
get: (header: string) => {
if (header === 'location') {
return `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
}
}
}
})
} else if (options.method === 'PUT') {
// Simulate successful upload of all blobs & then the manifest
validateRequestConfig(url, options)
if ((url as string).includes('manifest')) {
return Promise.resolve({
status: 201,
headers: {
get: (header: string) => {
if (header === 'docker-content-digest') {
return '1234567678'
}
}
}
})
}
return Promise.resolve({
status: 201
})
}
})
@@ -236,22 +277,6 @@ describe('publishOCIArtifact', () => {
return Buffer.from('test')
})
// Simulate successful upload of all blobs & then the manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(201, url, config)
if ((url as string).includes('manifest')) {
return {
status: 201,
headers: { 'docker-content-digest': '1234567678' }
}
}
return {
status: 201
}
})
await publishOCIArtifact(
token,
registry,
@@ -262,18 +287,27 @@ describe('publishOCIArtifact', () => {
testManifest
)
expect(fetchMock).toHaveBeenCalledTimes(8)
// We should only head all the blobs and then upload the missing blobs and manifest
expect(axiosHeadMock).toHaveBeenCalledTimes(3)
expect(axiosPostMock).toHaveBeenCalledTimes(2)
expect(axiosPutMock).toHaveBeenCalledTimes(3)
expect(
fetchMock.mock.calls.filter(call => call[1].method === 'HEAD')
).toHaveLength(3)
expect(
fetchMock.mock.calls.filter(call => call[1].method === 'POST')
).toHaveLength(2)
expect(
fetchMock.mock.calls.filter(call => call[1].method === 'PUT')
).toHaveLength(3)
})
it('throws an error if checking for existing blobs fails', async () => {
// Simulate failed response code
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(503, url, config)
return {
status: 503
fetchMock.mockImplementation(async (url, options) => {
if (options.method === 'HEAD') {
validateRequestConfig(url, options)
// Simulate failed response code
return Promise.resolve({
status: 503
})
}
})
@@ -291,19 +325,18 @@ describe('publishOCIArtifact', () => {
})
it('throws an error if initiating layer upload fails', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate failed initiation of uploads
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(503, url, config)
return {
status: 503
fetchMock.mockImplementation(async (url, options) => {
if (options.method === 'HEAD') {
// Simulate none of the blobs existing currently
validateRequestConfig(url, options)
return Promise.resolve({
status: 404
})
} else if (options.method === 'POST') {
// Simulate failed initiation of uploads
return Promise.resolve({
status: 503
})
}
})
@@ -321,20 +354,21 @@ describe('publishOCIArtifact', () => {
})
it('throws an error if the upload endpoint does not return a location', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate successful response code but no location header
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {}
fetchMock.mockImplementation(async (url, options) => {
validateRequestConfig(url, options)
if (options.method === 'HEAD') {
// Simulate none of the blobs existing currently
return Promise.resolve({
status: 404
})
} else if (options.method === 'POST') {
// Simulate successful response code but no location header
return Promise.resolve({
status: 202,
headers: {
get: () => {}
}
})
}
})
@@ -352,22 +386,32 @@ describe('publishOCIArtifact', () => {
})
it('throws an error if a layer upload fails', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
}
fetchMock.mockImplementation(async (url, options) => {
if (options.method === 'HEAD') {
validateRequestConfig(url, options)
// Simulate none of the blobs existing currently
return Promise.resolve({
status: 404
})
} else if (options.method === 'POST') {
// Simulate successful initiation of uploads for all blobs & return location
validateRequestConfig(url, options)
return Promise.resolve({
status: 202,
headers: {
get: (header: string) => {
if (header === 'location') {
return `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
}
}
}
})
} else if (options.method === 'PUT') {
// Simulate fails upload of all blobs & manifest
validateRequestConfig(url, options)
return Promise.resolve({
status: 500
})
}
})
@@ -376,14 +420,6 @@ describe('publishOCIArtifact', () => {
return Buffer.from('test')
})
// Simulate fails upload of all blobs & manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(500, url, config)
return {
status: 500
}
})
await expect(
publishOCIArtifact(
token,
@@ -398,22 +434,37 @@ describe('publishOCIArtifact', () => {
})
it('throws an error if a manifest upload fails', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
fetchMock.mockImplementation(async (url, options) => {
if (options.method === 'HEAD') {
// Simulate none of the blobs existing currently
validateRequestConfig(url, options)
return Promise.resolve({
status: 404
})
} else if (options.method === 'POST') {
// Simulate successful initiation of uploads for all blobs & return location
validateRequestConfig(url, options)
return Promise.resolve({
status: 202,
headers: {
get: (header: string) => {
if (header === 'location') {
return `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
}
}
}
})
} else if (options.method === 'PUT') {
// Simulate unsuccessful upload of all blobs & then the manifest
validateRequestConfig(url, options)
if (url.includes('manifest')) {
return Promise.resolve({
status: 500
})
}
return Promise.resolve({
status: 201
})
}
})
@@ -422,21 +473,6 @@ describe('publishOCIArtifact', () => {
return Buffer.from('test')
})
// Simulate successful upload of all blobs & then the manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
if (url.includes('manifest')) {
validateRequestConfig(500, url, config)
return {
status: 500
}
}
validateRequestConfig(201, url, config)
return {
status: 201
}
})
await expect(
publishOCIArtifact(
token,
@@ -451,22 +487,32 @@ describe('publishOCIArtifact', () => {
})
it('throws an error if reading one of the files fails', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
}
fetchMock.mockImplementation(async (url, options) => {
if (options.method === 'HEAD') {
// Simulate none of the blobs existing currently
validateRequestConfig(url, options)
return Promise.resolve({
status: 404
})
} else if (options.method === 'POST') {
// Simulate successful initiation of uploads for all blobs & return location
validateRequestConfig(url, options)
return Promise.resolve({
status: 202,
headers: {
get: (header: string) => {
if (header === 'location') {
return `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
}
}
}
})
} else if (options.method === 'PUT') {
// Simulate successful upload of all blobs & then the manifest
validateRequestConfig(url, options)
return Promise.resolve({
status: 201
})
}
})
@@ -475,14 +521,6 @@ describe('publishOCIArtifact', () => {
throw new Error('failed to read a file: test')
})
// Simulate successful upload of all blobs & then the manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(201, url, config)
return {
status: 201
}
})
await expect(
publishOCIArtifact(
token,
@@ -520,10 +558,10 @@ describe('publishOCIArtifact', () => {
})
})
// We expect all axios calls to have auth headers set and to not intercept any status codes so we can handle them.
// This function verifies that given an axios request config.
// We expect all fetch calls to have auth headers set
// This function verifies that given an request config.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function validateRequestConfig(status: number, url: string, config: any): void {
function validateRequestConfig(url: string, config: any): void {
// Basic URL checks
expect(url).toBeDefined()
if (!url.startsWith(registry.toString())) {
@@ -536,12 +574,6 @@ function validateRequestConfig(status: number, url: string, config: any): void {
// Config checks
expect(config).toBeDefined()
expect(config.validateStatus).toBeDefined()
if (config.validateStatus) {
// Check axios will not intercept this status
expect(config.validateStatus(status)).toBe(true)
}
expect(config.headers).toBeDefined()
if (config.headers) {
// Check the auth header is set