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'
import * as main from '../src/main'
import * as github from '@actions/github'
import * as fsHelper from '../src/fs-helper'
import * as ghcr from '../src/ghcr-client'
// Mock the GitHub Actions core library
let getInputMock : jest.SpyInstance
let setFailedMock : jest.SpyInstance
let setOutputMock : jest.SpyInstance
// Mock the filesystem helper
let createTempDirMock : jest.SpyInstance
let isDirectoryMock : jest.SpyInstance
let createArchivesMock : jest.SpyInstance
let removeDirMock : jest.SpyInstance
2024-01-26 16:33:11 -05:00
let getConsolidatedDirectoryMock : jest.SpyInstance
2024-01-26 13:09:44 -08:00
let isActionRepoMock : jest.SpyInstance
2023-11-17 20:04:42 +00:00
// Mock the GHCR Client
let publishOCIArtifactMock : jest.SpyInstance
describe ( 'action' , ( ) = > {
beforeEach ( ( ) = > {
jest . clearAllMocks ( )
// Core mocks
getInputMock = jest . spyOn ( core , 'getInput' ) . mockImplementation ( )
setFailedMock = jest . spyOn ( core , 'setFailed' ) . mockImplementation ( )
setOutputMock = jest . spyOn ( core , 'setOutput' ) . mockImplementation ( )
// FS mocks
createTempDirMock = jest
. spyOn ( fsHelper , 'createTempDir' )
. mockImplementation ( )
isDirectoryMock = jest . spyOn ( fsHelper , 'isDirectory' ) . mockImplementation ( )
createArchivesMock = jest
. spyOn ( fsHelper , 'createArchives' )
. mockImplementation ( )
removeDirMock = jest . spyOn ( fsHelper , 'removeDir' ) . mockImplementation ( )
2024-01-26 16:33:11 -05:00
getConsolidatedDirectoryMock = jest
. spyOn ( fsHelper , 'getConsolidatedDirectory' )
2023-11-22 15:35:22 +00:00
. mockImplementation ( )
2024-01-26 13:09:44 -08:00
isActionRepoMock = jest . spyOn ( fsHelper , 'isActionRepo' ) . mockImplementation ( )
2023-11-17 20:04:42 +00:00
// GHCR Client mocks
publishOCIArtifactMock = jest
. spyOn ( ghcr , 'publishOCIArtifact' )
. mockImplementation ( )
} )
it ( 'fails if no repository found' , async ( ) = > {
// Mock the environment
process . env . GITHUB_REPOSITORY = ''
// Run the action
2024-01-26 13:09:44 -08:00
await main . run ( 'directory1 directory2' )
2023-11-17 20:04:42 +00:00
// Check the results
expect ( setFailedMock ) . toHaveBeenCalledWith ( 'Could not find Repository.' )
} )
it ( 'fails if event is not a release' , async ( ) = > {
// Mock the environment
2024-01-25 10:54:19 -08:00
process . env . GITHUB_REPOSITORY = 'test-org/test-repo'
2023-11-17 20:04:42 +00:00
github . context . eventName = 'push'
// Run the action
2024-01-26 13:09:44 -08:00
await main . run ( 'directory1 directory2' )
2023-11-17 20:04:42 +00:00
// Check the results
expect ( setFailedMock ) . toHaveBeenCalledWith (
'Please ensure you have the workflow trigger as release.'
)
} )
it ( 'fails if release tag is not a valid semantic version' , async ( ) = > {
// Mock the environment
2024-01-25 10:54:19 -08:00
process . env . GITHUB_REPOSITORY = 'test-org/test-repo'
2023-11-17 20:04:42 +00:00
github . context . eventName = 'release'
github . context . payload = {
release : {
id : '123' ,
tag_name : 'invalid-tag'
}
}
// Run the action
2024-01-26 13:09:44 -08:00
await main . run ( 'directory1 directory2' )
2023-11-17 20:04:42 +00:00
// Check the results
expect ( setFailedMock ) . toHaveBeenCalledWith (
'invalid-tag is not a valid semantic version, and so cannot be uploaded as an Immutable Action.'
)
} )
2023-11-22 15:35:22 +00:00
it ( 'fails if multiple paths are provided and staging files fails' , async ( ) = > {
2023-11-17 20:04:42 +00:00
// Mock the environment
2024-01-25 10:54:19 -08:00
process . env . GITHUB_REPOSITORY = 'test-org/test-repo'
2023-11-17 20:04:42 +00:00
github . context . eventName = 'release'
github . context . payload = {
release : {
id : '123' ,
2024-01-25 10:54:19 -08:00
tag_name : 'v1.2.3'
2023-11-17 20:04:42 +00:00
}
}
getInputMock . mockImplementation ( ( name : string ) = > {
if ( name === 'path' ) {
2023-11-22 15:35:22 +00:00
return 'directory1 directory2'
2023-11-17 20:04:42 +00:00
} else if ( name === 'registry' ) {
return 'https://ghcr.io'
}
return ''
} )
2023-11-22 15:35:22 +00:00
isDirectoryMock . mockImplementation ( ( ) = > true )
2024-01-26 16:33:11 -05:00
getConsolidatedDirectoryMock . mockImplementation ( ( ) = > {
2023-11-22 15:35:22 +00:00
throw new Error ( 'Something went wrong' )
} )
2023-11-17 20:04:42 +00:00
// Run the action
2024-01-26 13:09:44 -08:00
await main . run ( 'directory1 directory2' )
2023-11-17 20:04:42 +00:00
// Check the results
2023-11-22 15:35:22 +00:00
expect ( setFailedMock ) . toHaveBeenCalledWith ( 'Something went wrong' )
2023-11-17 20:04:42 +00:00
} )
it ( 'fails if an error is thrown from dependent code' , async ( ) = > {
// Mock the environment
2024-01-25 10:54:19 -08:00
process . env . GITHUB_REPOSITORY = 'test-org/test-repo'
2023-11-17 20:04:42 +00:00
github . context . eventName = 'release'
github . context . payload = {
release : {
id : '123' ,
2024-01-25 10:54:19 -08:00
tag_name : 'v1.2.3'
2023-11-17 20:04:42 +00:00
}
}
getInputMock . mockImplementation ( ( name : string ) = > {
if ( name === 'path' ) {
return 'directory'
} else if ( name === 'registry' ) {
return 'https://ghcr.io'
}
return ''
} )
isDirectoryMock . mockImplementation ( ( ) = > true )
2024-01-26 13:09:44 -08:00
isActionRepoMock . mockImplementation ( ( ) = > true )
2023-11-17 20:04:42 +00:00
createTempDirMock . mockImplementation ( ( ) = > '/tmp/test' )
createArchivesMock . mockImplementation ( ( ) = > {
throw new Error ( 'Something went wrong' )
} )
// Run the action
2024-01-26 13:09:44 -08:00
await main . run ( 'directory' )
2023-11-17 20:04:42 +00:00
// Check the results
expect ( isDirectoryMock ) . toHaveBeenCalledWith ( 'directory' )
expect ( setFailedMock ) . toHaveBeenCalledWith ( 'Something went wrong' )
// Expect the files to be cleaned up
expect ( removeDirMock ) . toHaveBeenCalledWith ( '/tmp/test' )
} )
2023-11-22 14:25:27 +00:00
it ( 'successfully uploads if the release tag is a semver without v prefix' , async ( ) = > {
2024-01-25 10:54:19 -08:00
await testHappyPath ( '1.2.3' , 'test' )
2023-11-22 14:25:27 +00:00
} )
2023-11-17 20:04:42 +00:00
2023-11-22 14:25:27 +00:00
it ( 'successfully uploads if the release tag is a semver with v prefix' , async ( ) = > {
2024-01-25 10:54:19 -08:00
await testHappyPath ( 'v1.2.3' , 'test' )
2023-11-22 15:35:22 +00:00
} )
it ( 'successfully uploads if multiple paths are provided' , async ( ) = > {
2024-01-25 10:54:19 -08:00
await testHappyPath ( 'v1.2.3' , 'test test2' )
2023-11-22 14:25:27 +00:00
} )
} )
2023-11-17 20:04:42 +00:00
2023-11-22 14:25:27 +00:00
// Test that main successfully uploads and returns the manifest & package URL
2023-11-22 15:35:22 +00:00
async function testHappyPath ( version : string , path : string ) : Promise < void > {
2023-11-22 14:25:27 +00:00
// Mock the environment
2024-01-25 10:54:19 -08:00
process . env . GITHUB_REPOSITORY = 'test-org/test-repo'
2023-11-22 14:25:27 +00:00
github . context . eventName = 'release'
github . context . payload = {
release : {
id : '123' ,
tag_name : version
}
}
getInputMock . mockImplementation ( ( name : string ) = > {
if ( name === 'path' ) {
2023-11-22 15:35:22 +00:00
return path
2023-11-22 14:25:27 +00:00
} else if ( name === 'registry' ) {
return 'https://ghcr.io'
}
return ''
} )
2023-11-17 20:04:42 +00:00
2023-11-22 14:25:27 +00:00
isDirectoryMock . mockImplementation ( ( ) = > true )
2024-01-26 13:09:44 -08:00
isActionRepoMock . mockImplementation ( ( ) = > true )
2023-11-22 14:25:27 +00:00
2024-01-26 16:33:11 -05:00
getConsolidatedDirectoryMock . mockImplementation ( ( ) = > {
return { consolidatedDirectory : '/tmp/test' , needToCleanUpDir : false } // TODO: I don't understand why I have to name the variables here but not in the implementation code
2023-11-22 15:35:22 +00:00
} )
2023-11-22 14:25:27 +00:00
createTempDirMock . mockImplementation ( ( ) = > '/tmp/test' )
createArchivesMock . mockImplementation ( ( ) = > {
return {
zipFile : {
path : 'test' ,
size : 5 ,
sha256 : '123'
} ,
tarFile : {
path : 'test2' ,
size : 52 ,
sha256 : '1234'
2023-11-17 20:04:42 +00:00
}
2023-11-22 14:25:27 +00:00
}
} )
2023-11-17 20:04:42 +00:00
2023-11-22 14:25:27 +00:00
publishOCIArtifactMock . mockImplementation ( ( ) = > {
2024-01-25 10:54:19 -08:00
return new URL ( 'https://ghcr.io/v2/test-org/test-repo:1.2.3' )
2023-11-17 20:04:42 +00:00
} )
2023-11-22 14:25:27 +00:00
// Run the action
2024-01-26 13:09:44 -08:00
await main . run ( path )
2023-11-22 14:25:27 +00:00
expect ( publishOCIArtifactMock ) . toHaveBeenCalledTimes ( 1 )
// Check manifest is in output
expect ( setOutputMock ) . toHaveBeenCalledWith (
'package-url' ,
2024-01-25 10:54:19 -08:00
'https://ghcr.io/v2/test-org/test-repo:1.2.3'
2023-11-22 14:25:27 +00:00
)
expect ( setOutputMock ) . toHaveBeenCalledWith (
'package-manifest' ,
expect . any ( String )
)
// Validate the manifest
const manifest = JSON . parse ( setOutputMock . mock . calls [ 1 ] [ 1 ] )
expect ( manifest . mediaType ) . toEqual (
'application/vnd.oci.image.manifest.v1+json'
)
expect ( manifest . config . mediaType ) . toEqual (
'application/vnd.github.actions.package.config.v1+json'
)
expect ( manifest . layers . length ) . toEqual ( 3 )
expect ( manifest . annotations [ 'com.github.package.type' ] ) . toEqual (
'actions_oci_pkg'
)
2023-11-22 15:35:22 +00:00
// Expect all the temp files to be cleaned up
2023-11-22 14:25:27 +00:00
expect ( removeDirMock ) . toHaveBeenCalledWith ( '/tmp/test' )
2023-11-22 15:35:22 +00:00
expect ( removeDirMock ) . toHaveBeenCalledTimes (
createTempDirMock . mock . calls . length
)
2023-11-22 14:25:27 +00:00
}