more test coverage (#18)
Signed-off-by: Brian DeHamer <bdehamer@github.com>
This commit is contained in:
@@ -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
@@ -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(
|
||||
|
||||
@@ -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: {}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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 })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
+4
-15
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user