2023-11-17 20:04:42 +00:00
/**
* Unit tests for the action's main functionality, src/main.ts
*
* These should be run as if the action was called from a workflow.
* Specifically, the inputs listed in `action.yml` should be set as environment
* variables following the pattern `INPUT_<INPUT_NAME>`.
*/
import * as core from '@actions/core'
2024-03-01 16:45:32 +00:00
import * as attest from '@actions/attest'
2023-11-17 20:04:42 +00:00
import * as main from '../src/main'
2024-03-01 16:45:32 +00:00
import * as cfg from '../src/config'
2024-03-25 17:44:45 +00:00
import * as fsHelper from '../src/fs-helper'
import * as ghcr from '../src/ghcr-client'
2023-11-17 20:04:42 +00:00
2024-03-01 16:45:32 +00:00
const ghcrUrl = new URL ( 'https://ghcr.io' )
2023-11-17 20:04:42 +00:00
// Mock the GitHub Actions core library
let setFailedMock : jest.SpyInstance
let setOutputMock : jest.SpyInstance
2024-03-01 16:45:32 +00:00
// Mock the IA Toolkit
2023-11-17 20:04:42 +00:00
let createTempDirMock : jest.SpyInstance
let createArchivesMock : jest.SpyInstance
2024-01-29 20:31:00 +00:00
let stageActionFilesMock : jest.SpyInstance
2024-04-15 13:45:54 +01:00
let ensureCorrectShaCheckedOutMock : jest.SpyInstance
2023-11-17 20:04:42 +00:00
let publishOCIArtifactMock : jest.SpyInstance
2024-03-01 16:45:32 +00:00
// Mock the config resolution
let resolvePublishActionOptionsMock : jest.SpyInstance
// Mock generating attestation
let generateAttestationMock : jest.SpyInstance
2024-01-29 20:31:00 +00:00
describe ( 'run' , () => {
2023-11-17 20:04:42 +00:00
beforeEach (() => {
jest . clearAllMocks ()
// Core mocks
setFailedMock = jest . spyOn ( core , 'setFailed' ). mockImplementation ()
setOutputMock = jest . spyOn ( core , 'setOutput' ). mockImplementation ()
// FS mocks
createTempDirMock = jest
2024-03-25 17:44:45 +00:00
. spyOn ( fsHelper , 'createTempDir' )
2023-11-17 20:04:42 +00:00
. mockImplementation ()
createArchivesMock = jest
2024-03-25 17:44:45 +00:00
. spyOn ( fsHelper , 'createArchives' )
2023-11-17 20:04:42 +00:00
. mockImplementation ()
2024-01-29 20:31:00 +00:00
stageActionFilesMock = jest
2024-03-25 17:44:45 +00:00
. spyOn ( fsHelper , 'stageActionFiles' )
2023-11-17 21:31:12 +00:00
. mockImplementation ()
2024-04-15 13:45:54 +01:00
ensureCorrectShaCheckedOutMock = jest
2024-04-15 15:43:26 +01:00
. spyOn ( fsHelper , 'ensureTagAndRefCheckedOut' )
2024-04-15 13:45:54 +01:00
. mockImplementation ()
2023-11-17 20:04:42 +00:00
// GHCR Client mocks
publishOCIArtifactMock = jest
2024-03-25 17:44:45 +00:00
. spyOn ( ghcr , 'publishOCIArtifact' )
2023-11-17 20:04:42 +00:00
. mockImplementation ()
2024-01-29 20:31:00 +00:00
2024-03-01 16:45:32 +00:00
// Config mocks
resolvePublishActionOptionsMock = jest
. spyOn ( cfg , 'resolvePublishActionOptions' )
2024-01-29 20:31:00 +00:00
. mockImplementation ()
2024-03-01 16:45:32 +00:00
// Attestation mocks
generateAttestationMock = jest
. spyOn ( attest , 'attestProvenance' )
2024-01-29 20:31:00 +00:00
. mockImplementation ()
2023-11-17 20:04:42 +00:00
})
2024-03-01 16:45:32 +00:00
it ( 'fails if the action ref is not a tag' , async () => {
const options = baseOptions ()
options . ref = 'refs/heads/main' // This is a branch, not a tag
resolvePublishActionOptionsMock . mockReturnValueOnce ( options )
2024-01-29 20:31:00 +00:00
2024-01-30 14:02:59 +00:00
await main . run ()
2023-11-17 20:04:42 +00:00
expect ( setFailedMock ). toHaveBeenCalledWith (
2024-03-01 16:45:32 +00:00
'The ref refs/heads/main is not a valid tag reference.'
2023-11-17 20:04:42 +00:00
)
})
2024-03-01 16:45:32 +00:00
it ( 'fails if the value of the tag ref is not a valid semver' , async () => {
2024-01-29 20:31:00 +00:00
const tags = [ 'test' , 'v1.0' , 'chicken' , '111111' ]
for ( const tag of tags ) {
2024-03-01 16:45:32 +00:00
const options = baseOptions ()
options . ref = `refs/tags/ ${ tag } `
resolvePublishActionOptionsMock . mockReturnValueOnce ( options )
2024-01-29 20:31:00 +00:00
2024-01-30 14:02:59 +00:00
await main . run ()
2024-01-29 20:31:00 +00:00
expect ( setFailedMock ). toHaveBeenCalledWith (
2024-03-01 16:45:32 +00:00
` ${ tag } is not a valid semantic version tag, and so cannot be uploaded to the action package.`
2024-01-29 20:31:00 +00:00
)
}
})
2024-04-15 13:45:54 +01:00
it ( 'fails if ensuring the correct SHA is checked out errors' , async () => {
resolvePublishActionOptionsMock . mockReturnValue ( baseOptions ())
ensureCorrectShaCheckedOutMock . mockImplementation (() => {
throw new Error ( 'Something went wrong' )
})
// Run the action
await main . run ()
// Check the results
expect ( setFailedMock ). toHaveBeenCalledWith ( 'Something went wrong' )
})
2024-03-01 16:45:32 +00:00
it ( 'fails if creating staging temp directory fails' , async () => {
resolvePublishActionOptionsMock . mockReturnValue ( baseOptions ())
2024-01-29 20:31:00 +00:00
2024-04-15 13:45:54 +01:00
ensureCorrectShaCheckedOutMock . mockImplementation (() => {})
2024-01-29 20:31:00 +00:00
createTempDirMock . mockImplementation (() => {
throw new Error ( 'Something went wrong' )
})
// Run the action
2024-01-30 14:02:59 +00:00
await main . run ()
2024-01-29 20:31:00 +00:00
// Check the results
expect ( setFailedMock ). toHaveBeenCalledWith ( 'Something went wrong' )
})
2024-03-01 16:45:32 +00:00
it ( 'fails if staging files fails' , async () => {
resolvePublishActionOptionsMock . mockReturnValue ( baseOptions ())
2023-11-17 20:04:42 +00:00
2024-04-15 13:45:54 +01:00
ensureCorrectShaCheckedOutMock . mockImplementation (() => {})
2024-03-01 16:45:32 +00:00
createTempDirMock . mockImplementation (() => {
return 'tmpDir/staging'
})
stageActionFilesMock . mockImplementation (() => {
2023-11-17 20:04:42 +00:00
throw new Error ( 'Something went wrong' )
})
// Run the action
2024-01-30 14:02:59 +00:00
await main . run ()
2023-11-17 20:04:42 +00:00
// Check the results
expect ( setFailedMock ). toHaveBeenCalledWith ( 'Something went wrong' )
})
2024-03-01 16:45:32 +00:00
it ( 'fails if creating archives temp directory fails' , async () => {
resolvePublishActionOptionsMock . mockReturnValue ( baseOptions ())
2024-04-15 13:45:54 +01:00
ensureCorrectShaCheckedOutMock . mockImplementation (() => {})
2024-03-01 16:45:32 +00:00
createTempDirMock . mockImplementation (( _ , path : string ) => {
if ( path === 'staging' ) {
return 'staging'
2023-11-17 21:31:12 +00:00
}
2024-03-01 16:45:32 +00:00
throw new Error ( 'Something went wrong' )
})
stageActionFilesMock . mockImplementation (() => {})
// Run the action
await main . run ()
// Check the results
expect ( setFailedMock ). toHaveBeenCalledWith ( 'Something went wrong' )
})
it ( 'fails if creating archives fails' , async () => {
resolvePublishActionOptionsMock . mockReturnValue ( baseOptions ())
2024-04-15 13:45:54 +01:00
ensureCorrectShaCheckedOutMock . mockImplementation (() => {})
2024-03-01 16:45:32 +00:00
createTempDirMock . mockImplementation (() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock . mockImplementation (() => {})
2024-01-29 20:31:00 +00:00
createArchivesMock . mockImplementation (() => {
throw new Error ( 'Something went wrong' )
})
// Run the action
2024-01-30 14:02:59 +00:00
await main . run ()
2024-01-29 20:31:00 +00:00
// Check the results
expect ( setFailedMock ). toHaveBeenCalledWith ( 'Something went wrong' )
2023-11-17 21:31:12 +00:00
})
2024-01-29 20:31:00 +00:00
it ( 'fails if publishing OCI artifact fails' , async () => {
2024-03-01 16:45:32 +00:00
resolvePublishActionOptionsMock . mockReturnValue ( baseOptions ())
2024-04-15 13:45:54 +01:00
ensureCorrectShaCheckedOutMock . mockImplementation (() => {})
2024-03-01 16:45:32 +00:00
createTempDirMock . mockImplementation (() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock . mockImplementation (() => {})
2024-01-29 20:31:00 +00:00
createArchivesMock . mockImplementation (() => {
return {
zipFile : {
path : 'test' ,
size : 5 ,
sha256 : '123'
},
tarFile : {
path : 'test2' ,
size : 52 ,
sha256 : '1234'
}
}
})
publishOCIArtifactMock . mockImplementation (() => {
throw new Error ( 'Something went wrong' )
})
// Run the action
2024-01-30 14:02:59 +00:00
await main . run ()
2024-01-29 20:31:00 +00:00
// Check the results
expect ( setFailedMock ). toHaveBeenCalledWith ( 'Something went wrong' )
2023-11-17 21:31:12 +00:00
})
2024-03-01 16:45:32 +00:00
it ( 'fails if creating attestation fails' , async () => {
resolvePublishActionOptionsMock . mockReturnValue ( baseOptions ())
2023-11-17 21:31:12 +00:00
2024-04-15 13:45:54 +01:00
ensureCorrectShaCheckedOutMock . mockImplementation (() => {})
2024-03-01 16:45:32 +00:00
createTempDirMock . mockImplementation (() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock . mockImplementation (() => {})
2023-11-17 21:31:12 +00:00
2024-01-29 20:31:00 +00:00
createArchivesMock . mockImplementation (() => {
return {
zipFile : {
path : 'test' ,
size : 5 ,
sha256 : '123'
},
tarFile : {
path : 'test2' ,
size : 52 ,
sha256 : '1234'
}
}
})
2023-11-17 21:31:12 +00:00
2024-03-01 16:45:32 +00:00
publishOCIArtifactMock . mockImplementation (() => {
return {
packageURL : 'https://ghcr.io/v2/test-org/test-repo:1.2.3' ,
manifestDigest : 'sha256:my-test-digest'
}
2024-01-29 20:31:00 +00:00
})
2023-11-17 21:31:12 +00:00
2024-03-01 16:45:32 +00:00
generateAttestationMock . mockImplementation ( async () => {
throw new Error ( 'Something went wrong' )
})
// Run the action
await main . run ()
// Check the results
expect ( setFailedMock ). toHaveBeenCalledWith ( 'Something went wrong' )
})
2024-04-15 12:26:26 +01:00
it ( 'uploads the artifact, returns package metadata from GHCR, and skips writing attestation in enterprise' , async () => {
2024-03-01 16:45:32 +00:00
const options = baseOptions ()
options . isEnterprise = true
resolvePublishActionOptionsMock . mockReturnValue ( options )
2024-04-15 13:45:54 +01:00
ensureCorrectShaCheckedOutMock . mockImplementation (() => {})
2024-03-01 16:45:32 +00:00
createTempDirMock . mockImplementation (() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock . mockImplementation (() => {})
createArchivesMock . mockImplementation (() => {
return {
zipFile : {
path : 'test' ,
size : 5 ,
sha256 : '123'
},
tarFile : {
path : 'test2' ,
size : 52 ,
sha256 : '1234'
}
}
2024-01-29 20:31:00 +00:00
})
publishOCIArtifactMock . mockImplementation (() => {
return {
packageURL : 'https://ghcr.io/v2/test-org/test-repo:1.2.3' ,
2024-01-30 16:13:51 +00:00
manifestDigest : 'sha256:my-test-digest'
2024-01-29 20:31:00 +00:00
}
})
// Run the action
2024-01-30 14:02:59 +00:00
await main . run ()
2024-01-29 20:31:00 +00:00
// Check the results
expect ( publishOCIArtifactMock ). toHaveBeenCalledTimes ( 1 )
// Check outputs
expect ( setOutputMock ). toHaveBeenCalledTimes ( 3 )
expect ( setOutputMock ). toHaveBeenCalledWith (
'package-url' ,
'https://ghcr.io/v2/test-org/test-repo:1.2.3'
)
expect ( setOutputMock ). toHaveBeenCalledWith (
'package-manifest' ,
expect . any ( String )
)
expect ( setOutputMock ). toHaveBeenCalledWith (
'package-manifest-sha' ,
'sha256:my-test-digest'
)
})
2024-03-01 16:45:32 +00:00
2024-04-15 12:26:26 +01:00
it ( 'uploads the artifact, returns package metadata from GHCR, and creates an attestation in non-enterprise for public repo' , async () => {
2024-03-01 16:45:32 +00:00
resolvePublishActionOptionsMock . mockReturnValue ( baseOptions ())
2024-04-15 13:45:54 +01:00
ensureCorrectShaCheckedOutMock . mockImplementation (() => {})
2024-03-01 16:45:32 +00:00
createTempDirMock . mockImplementation (() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock . mockImplementation (() => {})
createArchivesMock . mockImplementation (() => {
return {
zipFile : {
path : 'test' ,
size : 5 ,
sha256 : '123'
},
tarFile : {
path : 'test2' ,
size : 52 ,
sha256 : '1234'
}
}
})
publishOCIArtifactMock . mockImplementation (() => {
return {
packageURL : 'https://ghcr.io/v2/test-org/test-repo:1.2.3' ,
manifestDigest : 'sha256:my-test-digest'
}
})
2024-04-15 12:26:26 +01:00
generateAttestationMock . mockImplementation ( async options => {
expect ( options ). toHaveProperty ( 'skipWrite' , false )
return {
attestationID : 'test-attestation-id' ,
certificate : 'test' ,
bundle : {
mediaType : 'application/vnd.cncf.notary.v2+jwt' ,
verificationMaterial : {
publicKey : {
hint : 'test-hint'
}
}
}
}
})
// Run the action
await main . run ()
// Check the results
expect ( publishOCIArtifactMock ). toHaveBeenCalledTimes ( 1 )
// Check outputs
expect ( setOutputMock ). toHaveBeenCalledTimes ( 4 )
expect ( setOutputMock ). toHaveBeenCalledWith (
'package-url' ,
'https://ghcr.io/v2/test-org/test-repo:1.2.3'
)
expect ( setOutputMock ). toHaveBeenCalledWith (
'package-manifest' ,
expect . any ( String )
)
expect ( setOutputMock ). toHaveBeenCalledWith (
'package-manifest-sha' ,
'sha256:my-test-digest'
)
expect ( setOutputMock ). toHaveBeenCalledWith (
'attestation-id' ,
'test-attestation-id'
)
})
it ( 'uploads the artifact, returns package metadata from GHCR, and creates an attestation but skips storing it in non-enterprise for private repo' , async () => {
const opts = baseOptions ()
opts . repositoryVisibility = 'private'
resolvePublishActionOptionsMock . mockReturnValue ( opts )
2024-04-15 13:45:54 +01:00
ensureCorrectShaCheckedOutMock . mockImplementation (() => {})
2024-04-15 12:26:26 +01:00
createTempDirMock . mockImplementation (() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock . mockImplementation (() => {})
createArchivesMock . mockImplementation (() => {
return {
zipFile : {
path : 'test' ,
size : 5 ,
sha256 : '123'
},
tarFile : {
path : 'test2' ,
size : 52 ,
sha256 : '1234'
}
}
})
publishOCIArtifactMock . mockImplementation (() => {
return {
packageURL : 'https://ghcr.io/v2/test-org/test-repo:1.2.3' ,
manifestDigest : 'sha256:my-test-digest'
}
})
generateAttestationMock . mockImplementation ( async options => {
expect ( options ). toHaveProperty ( 'skipWrite' , true )
2024-03-01 16:45:32 +00:00
return {
attestationID : 'test-attestation-id' ,
certificate : 'test' ,
bundle : {
mediaType : 'application/vnd.cncf.notary.v2+jwt' ,
verificationMaterial : {
publicKey : {
hint : 'test-hint'
}
}
}
}
})
// Run the action
await main . run ()
// Check the results
expect ( publishOCIArtifactMock ). toHaveBeenCalledTimes ( 1 )
// Check outputs
expect ( setOutputMock ). toHaveBeenCalledTimes ( 4 )
expect ( setOutputMock ). toHaveBeenCalledWith (
'package-url' ,
'https://ghcr.io/v2/test-org/test-repo:1.2.3'
)
expect ( setOutputMock ). toHaveBeenCalledWith (
'package-manifest' ,
expect . any ( String )
)
expect ( setOutputMock ). toHaveBeenCalledWith (
'package-manifest-sha' ,
'sha256:my-test-digest'
)
expect ( setOutputMock ). toHaveBeenCalledWith (
'attestation-id' ,
'test-attestation-id'
)
})
2024-01-29 20:31:00 +00:00
})
2024-03-01 16:45:32 +00:00
function baseOptions () : cfg . PublishActionOptions {
return {
nameWithOwner : 'nameWithOwner' ,
workspaceDir : 'workspaceDir' ,
event : 'release' ,
apiBaseUrl : 'apiBaseUrl' ,
runnerTempDir : 'runnerTempDir' ,
sha : 'sha' ,
repositoryId : 'repositoryId' ,
repositoryOwnerId : 'repositoryOwnerId' ,
isEnterprise : false ,
containerRegistryUrl : ghcrUrl ,
token : 'token' ,
2024-04-15 12:03:02 +01:00
ref : 'refs/tags/v1.2.3' ,
repositoryVisibility : 'public'
2024-03-01 16:45:32 +00:00
}
}