568 lines
16 KiB
TypeScript
568 lines
16 KiB
TypeScript
import crypto from 'crypto'
|
|
import fs from 'fs/promises'
|
|
import os from 'os'
|
|
import path from 'path'
|
|
import {
|
|
formatSubjectDigest,
|
|
subjectFromInputs,
|
|
SubjectInputs
|
|
} from '../src/subject'
|
|
|
|
describe('subjectFromInputs', () => {
|
|
const blankInputs: SubjectInputs = {
|
|
subjectPath: '',
|
|
subjectName: '',
|
|
subjectDigest: '',
|
|
subjectChecksums: ''
|
|
}
|
|
|
|
describe('when no inputs are provided', () => {
|
|
it('throws an error', async () => {
|
|
await expect(subjectFromInputs(blankInputs)).rejects.toThrow(
|
|
/one of subject-path, subject-digest, or subject-checksums must be provided/i
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when both subject path and subject digest are provided', () => {
|
|
it('throws an error', async () => {
|
|
const inputs: SubjectInputs = {
|
|
subjectName: 'foo',
|
|
subjectPath: 'path/to/subject',
|
|
subjectDigest: 'digest',
|
|
subjectChecksums: ''
|
|
}
|
|
|
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
|
/only one of subject-path, subject-digest, or subject-checksums may be provided/i
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when both subject path and subject checksums are provided', () => {
|
|
it('throws an error', async () => {
|
|
const inputs: SubjectInputs = {
|
|
subjectName: '',
|
|
subjectPath: 'path/to/subject',
|
|
subjectDigest: '',
|
|
subjectChecksums: 'path/to/checksums'
|
|
}
|
|
|
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
|
/only one of subject-path, subject-digest, or subject-checksums may be provided/i
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when both subject digest and subject checksums are provided', () => {
|
|
it('throws an error', async () => {
|
|
const inputs: SubjectInputs = {
|
|
subjectName: 'foo',
|
|
subjectPath: '',
|
|
subjectDigest: 'digest',
|
|
subjectChecksums: 'path/to/checksums'
|
|
}
|
|
|
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
|
/only one of subject-path, subject-digest, or subject-checksums may be provided/i
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when subject digest is provided but not the name', () => {
|
|
it('throws an error', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectDigest: 'digest'
|
|
}
|
|
|
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
|
/subject-name must be provided when using subject-digest/i
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when specifying a subject digest', () => {
|
|
const name = 'Subject'
|
|
|
|
describe('when the digest is malformed', () => {
|
|
it('throws an error', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectDigest: 'digest',
|
|
subjectName: name
|
|
}
|
|
|
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
|
/subject-digest must be in the format "sha256:<hex-digest>"/i
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when the algorithm is not supported', () => {
|
|
it('throws an error', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectDigest: 'md5:deadbeef',
|
|
subjectName: name
|
|
}
|
|
|
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
|
/subject-digest must be in the format "sha256:<hex-digest>"/i
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when the sha256 digest is malformed', () => {
|
|
it('throws an error', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectDigest: 'sha256:deadbeef',
|
|
subjectName: name
|
|
}
|
|
|
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
|
/subject-digest must be in the format "sha256:<hex-digest>"/i
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when the sha256 digest is valid', () => {
|
|
const alg = 'sha256'
|
|
const digest =
|
|
'7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
|
|
|
|
it('returns the subject', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectDigest: `${alg}:${digest}`,
|
|
subjectName: name
|
|
}
|
|
|
|
const subject = await subjectFromInputs(inputs)
|
|
|
|
expect(subject).toBeDefined()
|
|
expect(subject).toHaveLength(1)
|
|
expect(subject[0].name).toEqual(name)
|
|
expect(subject[0].digest).toEqual({ [alg]: digest })
|
|
})
|
|
})
|
|
|
|
describe('when the downcaseName is true', () => {
|
|
const imageName = 'ghcr.io/FOO/bar'
|
|
const alg = 'sha256'
|
|
const digest =
|
|
'7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
|
|
|
|
it('returns the subject (with name downcased)', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectDigest: `${alg}:${digest}`,
|
|
subjectName: imageName,
|
|
downcaseName: true
|
|
}
|
|
|
|
const subject = await subjectFromInputs(inputs)
|
|
|
|
expect(subject).toBeDefined()
|
|
expect(subject).toHaveLength(1)
|
|
expect(subject[0].name).toEqual(imageName.toLowerCase())
|
|
expect(subject[0].digest).toEqual({ [alg]: digest })
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when specifying a subject path', () => {
|
|
describe('when the file does NOT exist', () => {
|
|
it('throws an error', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectPath: '/f/a/k/e'
|
|
}
|
|
|
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
|
/could not find subject at path/i
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when the file exists', () => {
|
|
let dir = ''
|
|
const filename = 'subject'
|
|
const content = 'file content'
|
|
|
|
const expectedDigest = crypto
|
|
.createHash('sha256')
|
|
.update(content)
|
|
.digest('hex')
|
|
|
|
beforeEach(async () => {
|
|
// Set-up temp directory
|
|
const tmpDir = await fs.realpath(os.tmpdir())
|
|
dir = await fs.mkdtemp(tmpDir + path.sep)
|
|
|
|
// Write file to temp directory
|
|
await fs.writeFile(path.join(dir, filename), content)
|
|
|
|
// Add files for glob testing
|
|
for (let i = 0; i < 3; i++) {
|
|
await fs.writeFile(path.join(dir, `${filename}-${i}`), content)
|
|
await fs.writeFile(path.join(dir, `other-${i}`), content)
|
|
}
|
|
})
|
|
|
|
afterEach(async () => {
|
|
// Clean-up temp directory
|
|
await fs.rm(dir, { recursive: true })
|
|
})
|
|
|
|
describe('when no name is provided', () => {
|
|
it('returns the subject', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectPath: path.join(dir, filename)
|
|
}
|
|
|
|
const subject = await subjectFromInputs(inputs)
|
|
|
|
expect(subject).toBeDefined()
|
|
expect(subject).toHaveLength(1)
|
|
expect(subject[0].name).toEqual(filename)
|
|
expect(subject[0].digest).toEqual({ sha256: expectedDigest })
|
|
})
|
|
})
|
|
|
|
describe('when a name is provided', () => {
|
|
const name = 'mysubject'
|
|
|
|
it('returns the subject', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectPath: path.join(dir, filename),
|
|
subjectName: name
|
|
}
|
|
|
|
const subject = await subjectFromInputs(inputs)
|
|
|
|
expect(subject).toBeDefined()
|
|
expect(subject).toHaveLength(1)
|
|
expect(subject[0].name).toEqual(name)
|
|
expect(subject[0].digest).toEqual({ sha256: expectedDigest })
|
|
})
|
|
})
|
|
|
|
describe('when a file glob is supplied', () => {
|
|
it('returns the multiple subjects', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectPath: path.join(dir, 'subject-*')
|
|
}
|
|
|
|
const subjects = await subjectFromInputs(inputs)
|
|
|
|
expect(subjects).toBeDefined()
|
|
expect(subjects).toHaveLength(3)
|
|
|
|
subjects.forEach((subject, i) => {
|
|
expect(subject.name).toEqual(`${filename}-${i}`)
|
|
expect(subject.digest).toEqual({ sha256: expectedDigest })
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when a file glob is supplied which also matches non-files', () => {
|
|
it('returns the subjects (excluding non-files)', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectPath: `${dir}*`
|
|
}
|
|
|
|
const subjects = await subjectFromInputs(inputs)
|
|
|
|
expect(subjects).toBeDefined()
|
|
expect(subjects).toHaveLength(7)
|
|
})
|
|
})
|
|
|
|
describe('when a comma-separated list is supplied', () => {
|
|
it('returns the multiple subjects', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectPath: `${path.join(dir, 'subject-1')},${path.join(dir, 'subject-2')}`
|
|
}
|
|
|
|
const subjects = await subjectFromInputs(inputs)
|
|
|
|
expect(subjects).toBeDefined()
|
|
expect(subjects).toHaveLength(2)
|
|
|
|
expect(subjects).toContainEqual({
|
|
name: 'subject-1',
|
|
digest: { sha256: expectedDigest }
|
|
})
|
|
expect(subjects).toContainEqual({
|
|
name: 'subject-2',
|
|
digest: { sha256: expectedDigest }
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when a multi-line list is supplied', () => {
|
|
it('returns the multiple subjects', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectPath: `${path.join(dir, 'subject-0')}\n${path.join(dir, 'subject-2')}`
|
|
}
|
|
|
|
const subjects = await subjectFromInputs(inputs)
|
|
|
|
expect(subjects).toBeDefined()
|
|
expect(subjects).toHaveLength(2)
|
|
|
|
expect(subjects).toContainEqual({
|
|
name: 'subject-0',
|
|
digest: { sha256: expectedDigest }
|
|
})
|
|
expect(subjects).toContainEqual({
|
|
name: 'subject-2',
|
|
digest: { sha256: expectedDigest }
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when an excluding glob is supplied', () => {
|
|
it('returns the multiple subjects', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectPath: `${path.join(dir, 'subject-*')},!${path.join(dir, 'subject-1')}`
|
|
}
|
|
|
|
const subjects = await subjectFromInputs(inputs)
|
|
|
|
expect(subjects).toBeDefined()
|
|
expect(subjects).toHaveLength(2)
|
|
|
|
expect(subjects).toContainEqual({
|
|
name: 'subject-0',
|
|
digest: { sha256: expectedDigest }
|
|
})
|
|
expect(subjects).toContainEqual({
|
|
name: 'subject-2',
|
|
digest: { sha256: expectedDigest }
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when a multi-line glob list is supplied', () => {
|
|
it('returns the multiple subjects', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectPath: `${path.join(dir, 'subject-*')}\n ${path.join(dir, 'other-*')} `
|
|
}
|
|
|
|
const subjects = await subjectFromInputs(inputs)
|
|
|
|
expect(subjects).toBeDefined()
|
|
expect(subjects).toHaveLength(6)
|
|
|
|
expect(subjects).toContainEqual({
|
|
name: 'subject-0',
|
|
digest: { sha256: expectedDigest }
|
|
})
|
|
expect(subjects).toContainEqual({
|
|
name: 'subject-1',
|
|
digest: { sha256: expectedDigest }
|
|
})
|
|
expect(subjects).toContainEqual({
|
|
name: 'subject-2',
|
|
digest: { sha256: expectedDigest }
|
|
})
|
|
|
|
expect(subjects).toContainEqual({
|
|
name: 'other-0',
|
|
digest: { sha256: expectedDigest }
|
|
})
|
|
expect(subjects).toContainEqual({
|
|
name: 'other-1',
|
|
digest: { sha256: expectedDigest }
|
|
})
|
|
expect(subjects).toContainEqual({
|
|
name: 'other-2',
|
|
digest: { sha256: expectedDigest }
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when duplicate subjects are supplied', () => {
|
|
let otherDir = ''
|
|
|
|
// Add duplicate subject in alternate directory
|
|
beforeEach(async () => {
|
|
// Set-up temp directory
|
|
const tmpDir = await fs.realpath(os.tmpdir())
|
|
otherDir = await fs.mkdtemp(tmpDir + path.sep)
|
|
|
|
// Write file to temp directory
|
|
await fs.writeFile(path.join(otherDir, filename), content)
|
|
})
|
|
|
|
it('returns de-duplicated subjects', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectPath: `${path.join(dir, 'subject')}, ${path.join(otherDir, 'subject')} `
|
|
}
|
|
const subjects = await subjectFromInputs(inputs)
|
|
|
|
expect(subjects).toBeDefined()
|
|
expect(subjects).toHaveLength(1)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when specifying a subject checksums file', () => {
|
|
const checksums = `
|
|
187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d demo_0.0.1_linux_amd64
|
|
badline
|
|
5d8b4751ef31f9440d843fcfa4e53ca2e25b1cb1f13fd355fdc7c24b41fe645293291ea9297ba3989078abb77ebbaac66be073618a9e4974dbd0361881d4c718 demo_0.0.1_darwin_arm64`
|
|
|
|
let dir = ''
|
|
const filename = 'checksums'
|
|
|
|
beforeEach(async () => {
|
|
// Set-up temp directory
|
|
const tmpDir = await fs.realpath(os.tmpdir())
|
|
dir = await fs.mkdtemp(tmpDir + path.sep)
|
|
|
|
// Write file to temp directory
|
|
await fs.writeFile(path.join(dir, filename), checksums)
|
|
})
|
|
|
|
afterEach(async () => {
|
|
// Clean-up temp directory
|
|
await fs.rm(dir, { recursive: true })
|
|
})
|
|
|
|
describe('when the specified path is NOT a file', () => {
|
|
it('throws an error', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectChecksums: dir
|
|
}
|
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
|
/subject checksums file not found/i
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when the specific path is a file', () => {
|
|
it('returns the multiple subjects', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectChecksums: path.join(dir, filename)
|
|
}
|
|
const subjects = await subjectFromInputs(inputs)
|
|
|
|
expect(subjects).toBeDefined()
|
|
expect(subjects).toHaveLength(2)
|
|
|
|
expect(subjects).toContainEqual({
|
|
name: 'demo_0.0.1_linux_amd64',
|
|
digest: {
|
|
sha256:
|
|
'187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d'
|
|
}
|
|
})
|
|
expect(subjects).toContainEqual({
|
|
name: 'demo_0.0.1_darwin_arm64',
|
|
digest: {
|
|
sha512:
|
|
'5d8b4751ef31f9440d843fcfa4e53ca2e25b1cb1f13fd355fdc7c24b41fe645293291ea9297ba3989078abb77ebbaac66be073618a9e4974dbd0361881d4c718'
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when specifying a subject checksums string', () => {
|
|
const checksums = `
|
|
f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_linux_386
|
|
187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d *demo_0.0.1_linux_amd64
|
|
9ecbf449e286a8a8748c161c52aa28b6b2fc64ab86f94161c5d1b3abc18156c5 demo_0.0.1_linux_arm64`
|
|
|
|
it('returns the multiple subjects', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectChecksums: checksums
|
|
}
|
|
const subjects = await subjectFromInputs(inputs)
|
|
|
|
expect(subjects).toBeDefined()
|
|
expect(subjects).toHaveLength(3)
|
|
|
|
expect(subjects).toContainEqual({
|
|
name: 'demo_0.0.1_linux_386',
|
|
digest: {
|
|
sha256:
|
|
'f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e'
|
|
}
|
|
})
|
|
expect(subjects).toContainEqual({
|
|
name: 'demo_0.0.1_linux_amd64',
|
|
digest: {
|
|
sha256:
|
|
'187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d'
|
|
}
|
|
})
|
|
expect(subjects).toContainEqual({
|
|
name: 'demo_0.0.1_linux_arm64',
|
|
digest: {
|
|
sha256:
|
|
'9ecbf449e286a8a8748c161c52aa28b6b2fc64ab86f94161c5d1b3abc18156c5'
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when specifying a subject checksums string with an unrecognized digest', () => {
|
|
const checksums = `f861e demo_0.0.1_linux_386`
|
|
|
|
it('throws an error', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectChecksums: checksums
|
|
}
|
|
|
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
|
/unknown digest algorithm/i
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when specifying a subject checksums string with an invalid digest', () => {
|
|
const checksums =
|
|
'!!!!e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_linux_386'
|
|
|
|
it('throws an error', async () => {
|
|
const inputs: SubjectInputs = {
|
|
...blankInputs,
|
|
subjectChecksums: checksums
|
|
}
|
|
|
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(/invalid digest/i)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('subjectDigest', () => {
|
|
it('returns the digest', () => {
|
|
const subject = {
|
|
name: 'foo',
|
|
digest: { sha1: 'deadbeef' }
|
|
}
|
|
|
|
const digest = formatSubjectDigest(subject)
|
|
expect(digest).toEqual('sha1:deadbeef')
|
|
})
|
|
})
|