more test coverage (#18)

Signed-off-by: Brian DeHamer <bdehamer@github.com>
This commit is contained in:
Brian DeHamer
2024-02-29 17:02:56 -08:00
committed by GitHub
parent e6f9108958
commit 3b95763d7e
6 changed files with 332 additions and 39 deletions
+5 -5
View File
@@ -34,8 +34,8 @@ attest:
contents: write # TODO: Update this
```
The `id-token` permission gives the action the ability to mint the OIDC
token necessary to request a Sigstore signing certificate. The `contents`
The `id-token` permission gives the action the ability to mint the OIDC token
necessary to request a Sigstore signing certificate. The `contents`
permission is necessary to persist the attestation.
1. Add the following to your workflow after your artifact has been built:
@@ -99,9 +99,9 @@ See [action.yml](action.yml)
<!-- markdownlint-disable MD013 -->
| Name | Description | Example |
| ------------- | -------------------------------------------------------------- | ----------------------- |
| `bundle-path` | Absolute path to the file containing the generated attestation | `/tmp/attestaion.jsonl` |
| Name | Description | Example |
| ------------- | -------------------------------------------------------------- | ------------------------ |
| `bundle-path` | Absolute path to the file containing the generated attestation | `/tmp/attestation.jsonl` |
<!-- markdownlint-enable MD013 -->
+21 -2
View File
@@ -9,6 +9,7 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import { mockFulcio, mockRekor, mockTSA } from '@sigstore/mock'
import * as oci from '@sigstore/oci'
import nock from 'nock'
import { SEARCH_PUBLIC_GOOD_URL } from '../src/endpoints'
import * as main from '../src/main'
@@ -43,7 +44,7 @@ describe('action', () => {
'base64'
)}.}`
const subjectName = 'subject'
const subjectName = 'registry/foo/bar'
const subjectDigest =
'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
const predicate = '{}'
@@ -189,6 +190,9 @@ describe('action', () => {
})
describe('when the repository is public', () => {
const getRegCredsSpy = jest.spyOn(oci, 'getRegistryCredentials')
const attachArtifactSpy = jest.spyOn(oci, 'attachArtifactToImage')
const inputs = {
'subject-digest': subjectDigest,
'subject-name': subjectName,
@@ -206,13 +210,26 @@ describe('action', () => {
// Mock the action's inputs
getInputMock.mockImplementation(mockInput(inputs))
getBooleanInputMock.mockImplementation(() => false)
// This is where we mock the push-to-registry input
getBooleanInputMock.mockImplementation(() => true)
await mockFulcio({
baseURL: 'https://fulcio.sigstore.dev',
strict: false
})
await mockRekor({ baseURL: 'https://rekor.sigstore.dev' })
getRegCredsSpy.mockImplementation(() => ({
username: 'username',
password: 'password'
}))
attachArtifactSpy.mockImplementation(async () =>
Promise.resolve({
digest: 'sha256:123456',
mediaType: 'application/vnd.cncf.notary.v2',
size: 123456
})
)
})
it('invokes the action w/o error', async () => {
@@ -220,6 +237,8 @@ describe('action', () => {
expect(runMock).toHaveReturned()
expect(setFailedMock).not.toHaveBeenCalled()
expect(getRegCredsSpy).toHaveBeenCalledWith(subjectName)
expect(attachArtifactSpy).toHaveBeenCalled()
expect(infoMock).toHaveBeenNthCalledWith(
1,
expect.stringMatching(
+92
View File
@@ -0,0 +1,92 @@
import fs from 'fs/promises'
import os from 'os'
import path from 'path'
import { predicateFromInputs } from '../src/predicate'
describe('subjectFromInputs', () => {
afterEach(() => {
process.env['INPUT_PREDICATE'] = ''
process.env['INPUT_PREDICATE-PATH'] = ''
process.env['INPUT_PREDICATE-TYPE'] = ''
})
describe('when no inputs are provided', () => {
it('throws an error', () => {
expect(() => predicateFromInputs()).toThrow(/predicate-type/i)
})
})
describe('when neither predicate path nor predicate are provided', () => {
beforeEach(() => {
process.env['INPUT_PREDICATE-TYPE'] = 'https://example.com/predicate'
})
it('throws an error', () => {
expect(() => predicateFromInputs()).toThrow(
/one of predicate-path or predicate must be provided/i
)
})
})
describe('when both predicate path and predicate are provided', () => {
beforeEach(() => {
process.env['INPUT_PREDICATE-PATH'] = 'path/to/predicate'
process.env['INPUT_PREDICATE'] = '{}'
process.env['INPUT_PREDICATE-TYPE'] = 'https://example.com/predicate'
})
it('throws an error', () => {
expect(() => predicateFromInputs()).toThrow(
/only one of predicate-path or predicate may be provided/i
)
})
})
describe('when specifying a predicate path', () => {
let dir = ''
const filename = 'subject'
const content = '{}'
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)
})
afterEach(async () => {
// Clean-up temp directory
await fs.rm(dir, { recursive: true })
})
beforeEach(() => {
process.env['INPUT_PREDICATE-PATH'] = path.join(dir, filename)
process.env['INPUT_PREDICATE-TYPE'] = 'https://example.com/predicate'
})
it('returns the predicate', () => {
expect(predicateFromInputs()).toEqual({
type: 'https://example.com/predicate',
params: {}
})
})
})
describe('when specifying a predicate value', () => {
const content = '{}'
beforeEach(() => {
process.env['INPUT_PREDICATE'] = content
process.env['INPUT_PREDICATE-TYPE'] = 'https://example.com/predicate'
})
it('returns the predicate', () => {
expect(predicateFromInputs()).toEqual({
type: 'https://example.com/predicate',
params: {}
})
})
})
})
+205
View File
@@ -0,0 +1,205 @@
import crypto from 'crypto'
import fs from 'fs/promises'
import os from 'os'
import path from 'path'
import { subjectFromInputs } from '../src/subject'
describe('subjectFromInputs', () => {
afterEach(() => {
process.env['INPUT_SUBJECT-PATH'] = ''
process.env['INPUT_SUBJECT-DIGEST'] = ''
process.env['INPUT_SUBJECT-NAME'] = ''
})
describe('when no inputs are provided', () => {
it('throws an error', async () => {
await expect(subjectFromInputs()).rejects.toThrow(
/one of subject-path or subject-digest must be provided/i
)
})
})
describe('when both subject path and subject digest are provided', () => {
beforeEach(() => {
process.env['INPUT_SUBJECT-PATH'] = 'path/to/subject'
process.env['INPUT_SUBJECT-DIGEST'] = 'digest'
})
it('throws an error', async () => {
await expect(subjectFromInputs()).rejects.toThrow(
/only one of subject-path or subject-digest may be provided/i
)
})
})
describe('when subject digest is provided but not the name', () => {
beforeEach(() => {
process.env['INPUT_SUBJECT-DIGEST'] = 'digest'
})
it('throws an error', async () => {
await expect(subjectFromInputs()).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', () => {
beforeEach(() => {
process.env['INPUT_SUBJECT-DIGEST'] = 'digest'
process.env['INPUT_SUBJECT-NAME'] = 'subject'
})
it('throws an error', async () => {
await expect(subjectFromInputs()).rejects.toThrow(
/subject-digest must be in the format "sha256:<hex-digest>"/i
)
})
})
describe('when the alogrithm is not supported', () => {
beforeEach(() => {
process.env['INPUT_SUBJECT-DIGEST'] = 'md5:deadbeef'
process.env['INPUT_SUBJECT-NAME'] = 'subject'
})
it('throws an error', async () => {
await expect(subjectFromInputs()).rejects.toThrow(
/subject-digest must be in the format "sha256:<hex-digest>"/i
)
})
})
describe('when the sha256 digest is malformed', () => {
beforeEach(() => {
process.env['INPUT_SUBJECT-DIGEST'] = 'sha256:deadbeef'
process.env['INPUT_SUBJECT-NAME'] = 'subject'
})
it('throws an error', async () => {
await expect(subjectFromInputs()).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'
beforeEach(() => {
process.env['INPUT_SUBJECT-DIGEST'] = `${alg}:${digest}`
process.env['INPUT_SUBJECT-NAME'] = name
})
it('returns the subject', async () => {
const subject = await subjectFromInputs()
expect(subject).toBeDefined()
expect(subject).toHaveLength(1)
expect(subject[0].name).toEqual(name)
expect(subject[0].digest).toEqual({ [alg]: digest })
})
})
})
describe('when specifying a subject path', () => {
describe('when the file does NOT exist', () => {
beforeEach(() => {
process.env['INPUT_SUBJECT-PATH'] = '/f/a/k/e'
})
it('throws an error', async () => {
await expect(subjectFromInputs()).rejects.toThrow(
/could not find subject at path/i
)
})
})
})
describe('when the file eixts', () => {
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)
}
})
afterEach(async () => {
// Clean-up temp directory
await fs.rm(dir, { recursive: true })
})
describe('when no name is provided', () => {
beforeEach(() => {
process.env['INPUT_SUBJECT-PATH'] = path.join(dir, filename)
})
it('returns the subject', async () => {
const subject = await subjectFromInputs()
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'
beforeEach(() => {
process.env['INPUT_SUBJECT-PATH'] = path.join(dir, filename)
process.env['INPUT_SUBJECT-NAME'] = name
})
it('returns the subject', async () => {
const subject = await subjectFromInputs()
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', () => {
beforeEach(async () => {
process.env['INPUT_SUBJECT-PATH'] = path.join(dir, 'subject-*')
})
it('returns the multiple subjects', async () => {
const subjects = await subjectFromInputs()
expect(subjects).toBeDefined()
expect(subjects).toHaveLength(3)
/* eslint-disable-next-line github/array-foreach */
subjects.forEach((subject, i) => {
expect(subject.name).toEqual(`${filename}-${i}`)
expect(subject.digest).toEqual({ sha256: expectedDigest })
})
})
})
})
})
Generated Vendored
+4 -15
View File
@@ -65122,10 +65122,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.storePredicate = exports.predicateFromInputs = void 0;
exports.predicateFromInputs = void 0;
const core = __importStar(__nccwpck_require__(2186));
const fs_1 = __importDefault(__nccwpck_require__(7147));
const path = __importStar(__nccwpck_require__(1017));
// Returns the predicate specified by the action's inputs. The predicate value
// may be specified as a path to a file or as a string.
const predicateFromInputs = () => {
@@ -65135,25 +65134,15 @@ const predicateFromInputs = () => {
if (!predicatePath && !predicateStr) {
throw new Error('One of predicate-path or predicate must be provided');
}
if (predicatePath && predicateStr) {
throw new Error('Only one of predicate-path or predicate may be provided');
}
const params = predicatePath
? fs_1.default.readFileSync(predicatePath, 'utf-8')
: predicateStr;
return { type: predicateType, params: JSON.parse(params) };
};
exports.predicateFromInputs = predicateFromInputs;
const storePredicate = (predicate) => {
// random tempfile
const basePath = process.env['RUNNER_TEMP'];
if (!basePath) {
throw new Error('Missing RUNNER_TEMP environment variable');
}
const tmpDir = fs_1.default.mkdtempSync(path.join(basePath, path.sep));
const tempFile = path.join(tmpDir, 'predicate.json');
// write predicate to file
fs_1.default.writeFileSync(tempFile, JSON.stringify(predicate.params));
return tempFile;
};
exports.storePredicate = storePredicate;
/***/ }),
+5 -17
View File
@@ -1,6 +1,6 @@
import * as core from '@actions/core'
import fs from 'fs'
import * as path from 'path'
import type { Predicate } from '@actions/attest'
// Returns the predicate specified by the action's inputs. The predicate value
@@ -14,25 +14,13 @@ export const predicateFromInputs = (): Predicate => {
throw new Error('One of predicate-path or predicate must be provided')
}
if (predicatePath && predicateStr) {
throw new Error('Only one of predicate-path or predicate may be provided')
}
const params = predicatePath
? fs.readFileSync(predicatePath, 'utf-8')
: predicateStr
return { type: predicateType, params: JSON.parse(params) }
}
export const storePredicate = (predicate: Predicate): string => {
// random tempfile
const basePath = process.env['RUNNER_TEMP']
if (!basePath) {
throw new Error('Missing RUNNER_TEMP environment variable')
}
const tmpDir = fs.mkdtempSync(path.join(basePath, path.sep))
const tempFile = path.join(tmpDir, 'predicate.json')
// write predicate to file
fs.writeFileSync(tempFile, JSON.stringify(predicate.params))
return tempFile
}