update tests to remove axios mocks and mock fetch instead

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