Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions __tests__/helpers/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* Sample data factories for GitHub API responses and event payloads used in
* tests. Keeping these in one place means a test only describes the data that
* matters to it and inherits sensible defaults for the rest.
*/

export interface PullRequestFileFixture {
sha: string
filename: string
blob_url: string
raw_url: string
contents_url: string
}

/**
* A changed file entry as returned (after mapping) by
* `Vet.pullRequestGetChangedFiles`.
*/
export function changedLockfileFile(
filename: string,
overrides: Partial<PullRequestFileFixture> = {}
): PullRequestFileFixture {
return {
sha: 'abc123',
filename,
blob_url: `https://github.com/owner/repo/blob/main/${filename}`,
raw_url: `https://github.com/owner/repo/raw/main/${filename}`,
contents_url: `https://api.github.com/repos/owner/repo/contents/${filename}`,
...overrides
}
}

/**
* Shape of `octokit.rest.repos.compareCommitsWithBasehead` responses used by
* `Vet.pullRequestGetChangedFiles`.
*/
export function compareCommitsResponse(
files: PullRequestFileFixture[],
status: 'ahead' | 'behind' | 'diverged' | 'identical' = 'ahead',
httpStatus = 200
): {
status: number
data: { status: string; files: PullRequestFileFixture[] }
} {
return {
status: httpStatus,
data: {
status,
files
}
}
}

/**
* Shape of a raw `octokit.rest.repos.getContent` response used by
* `Vet.pullRequestCheckoutFileByPath` (mediaType format: 'raw').
*/
export function getContentRawResponse(
content: string,
httpStatus = 200
): { status: number; data: string } {
return {
status: httpStatus,
data: content
}
}

export interface CommentFixture {
id: number
body: string
}

/**
* Shape of `octokit.rest.issues.listComments` responses.
*/
export function listCommentsResponse(comments: CommentFixture[]): {
data: CommentFixture[]
} {
return { data: comments }
}

/**
* Shape of `octokit.rest.repos.getLatestRelease` responses.
*/
export function getLatestReleaseResponse(tagName: string): {
data: { tag_name: string }
} {
return { data: { tag_name: tagName } }
}

/**
* A minimal `pull_request` event payload (the JSON GitHub writes to
* GITHUB_EVENT_PATH) for the same-repository case.
*/
export const samplePullRequestPayload = {
number: 123,
head: {
ref: 'feature-branch',
repo: { full_name: 'owner/repo' }
}
}

/**
* A `pull_request` event payload originating from a forked repository.
*/
export const sampleForkedPullRequestPayload = {
number: 123,
head: {
ref: 'feature-branch',
repo: { full_name: 'contributor/repo' }
}
}

/**
* Stringified event JSON as read from GITHUB_EVENT_PATH for a push event.
*/
export const samplePushEventJson = JSON.stringify({
ref: 'refs/heads/main',
repository: { full_name: 'owner/repo' }
})

/**
* Stringified event JSON as read from GITHUB_EVENT_PATH for a pull_request
* event.
*/
export const samplePullRequestEventJson = JSON.stringify({
pull_request: samplePullRequestPayload
})
233 changes: 233 additions & 0 deletions __tests__/helpers/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/**
* Reusable mocks for testing vet-action.
*
* These helpers centralize the GitHub Actions environment, GitHub API (Octokit)
* and vet binary lifecycle mocks that every test needs. They cover the
* repetitive *setup*, not the module mock declarations: `vi.mock('<module>')`
* calls are hoisted per file by Vitest and must stay in the test file itself.
*
* A test file that uses these helpers is expected to declare (at minimum):
*
* vi.mock('@actions/core')
* vi.mock('@actions/github', () => ({
* getOctokit: vi.fn(),
* context: { payload: {} }
* }))
* vi.mock('node:fs')
* vi.mock('@actions/exec')
* vi.mock('@actions/tool-cache')
* vi.mock('@actions/artifact')
* vi.mock('../src/rpc')
*
* See docs/testing.md for the full walkthrough.
*/
import { vi } from 'vitest'
import * as core from '@actions/core'
import { context } from '@actions/github'
import { Vet } from '../../src/vet'

type VetConfigArg = ConstructorParameters<typeof Vet>[0]

/**
* The standard GitHub Actions environment for a hosted runner. Override
* individual values per test via `stubGitHubActionsEnv`.
*/
export const defaultActionsEnv: Record<string, string> = {
GITHUB_TOKEN: 'mock-token',
GITHUB_REPOSITORY: 'owner/repo',
GITHUB_REPOSITORY_OWNER: 'owner',
GITHUB_BASE_REF: 'main',
GITHUB_HEAD_REF: 'feature-branch',
GITHUB_REF_NAME: 'feature-branch',
RUNNER_TEMP: '/tmp'
}

/**
* Stub the GitHub Actions runner environment using `vi.stubEnv`. Pass overrides
* to change or add variables for a specific test. Returns the resolved env map.
*
* Remember to enable `vi.unstubAllEnvs()` (or `unstubEnvs: true` in the Vitest
* config) between tests so stubs don't leak.
*/
export function stubGitHubActionsEnv(
overrides: Record<string, string> = {}
): Record<string, string> {
const env = { ...defaultActionsEnv, ...overrides }
for (const [key, value] of Object.entries(env)) {
vi.stubEnv(key, value)
}
return env
}

export interface MockOctokit {
rest: {
issues: {
listComments: ReturnType<typeof vi.fn>
createComment: ReturnType<typeof vi.fn>
updateComment: ReturnType<typeof vi.fn>
}
repos: {
getLatestRelease: ReturnType<typeof vi.fn>
compareCommitsWithBasehead: ReturnType<typeof vi.fn>
getContent: ReturnType<typeof vi.fn>
}
}
}

/**
* Build a mock Octokit with every REST endpoint the action calls stubbed as a
* `vi.fn()`. Configure behaviour per test on the returned object, e.g.
*
* octokit.rest.repos.getContent.mockResolvedValue(getContentRawResponse('...'))
*/
export function createMockOctokit(): MockOctokit {
return {
rest: {
issues: {
listComments: vi.fn(),
createComment: vi.fn(),
updateComment: vi.fn()
},
repos: {
getLatestRelease: vi.fn(),
compareCommitsWithBasehead: vi.fn(),
getContent: vi.fn()
}
}
}
}

export interface MockCommentsProxyClient {
createPullRequestComment: ReturnType<typeof vi.fn>
}

/**
* Build a mock GitHub Comments Proxy client used for the forked-PR comment
* fallback path (see Vet.addOrUpdatePullRequestCommentWithGitHubCommentsProxy).
*/
export function createMockCommentsProxyClient(): MockCommentsProxyClient {
return {
createPullRequestComment: vi.fn()
}
}

export interface TestVetDeps {
octokit?: MockOctokit
proxyClient?: MockCommentsProxyClient
}

export interface TestVet {
vet: Vet
octokit: MockOctokit
proxyClient: MockCommentsProxyClient
}

/**
* Construct a `Vet` instance wired up for testing. Octokit and the comments
* proxy client (both private fields populated in the constructor) are replaced
* with mocks so no real network client is created. Pass partial config to
* override the sensible defaults.
*/
export function createTestVet(
config: Partial<VetConfigArg> = {},
deps: TestVetDeps = {}
): TestVet {
const octokit = deps.octokit ?? createMockOctokit()
const proxyClient = deps.proxyClient ?? createMockCommentsProxyClient()

const vet = new Vet({
cloudMode: false,
pullRequestNumber: 123,
pullRequestComment: true,
...config
})

// @ts-expect-error - injecting mock octokit into private field
vet.octokit = octokit
// @ts-expect-error - injecting mock comments proxy client into private field
vet.commentsProxyClient = proxyClient

return { vet, octokit, proxyClient }
}

export interface VetBinaryLifecycleOptions {
version?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
runVetImpl?: (...args: any[]) => Promise<string>
}

/**
* Stub the vet binary lifecycle (release lookup, download, extraction and
* execution) so `Vet.run()` can be driven end to end without touching the
* network or filesystem. By default `runVet` returns a parseable version banner
* so `verifyVetBinary` succeeds.
*/
export function stubVetBinaryLifecycle(
vet: Vet,
options: VetBinaryLifecycleOptions = {}
): void {
const version = options.version ?? '1.2.3'

// @ts-expect-error - stubbing private method
vet.getLatestRelease = vi
.fn()
.mockResolvedValue('https://example.com/vet.tar.gz')
// @ts-expect-error - stubbing private method
vet.downloadBinary = vi.fn().mockResolvedValue('/mock/vet.tar.gz')
// @ts-expect-error - stubbing private method
vet.extractBinary = vi.fn().mockResolvedValue('/mock/bin')
// @ts-expect-error - stubbing private method
vet.runVet = options.runVetImpl
? vi.fn(options.runVetImpl)
: vi.fn().mockResolvedValue(`Version: ${version}`)
}

export interface MockCoreSummary {
clear: ReturnType<typeof vi.fn>
addRaw: ReturnType<typeof vi.fn>
write: ReturnType<typeof vi.fn>
}

/**
* Wire a chainable mock onto `core.summary` (requires `vi.mock('@actions/core')`
* in the test file) and return it for assertions.
*/
export function createMockCoreSummary(): MockCoreSummary {
const summary: MockCoreSummary = {
clear: vi.fn().mockReturnThis(),
addRaw: vi.fn().mockReturnThis(),
write: vi.fn().mockResolvedValue(undefined)
}

// core.summary's methods have precise signatures (returning Summary); treat
// the target as a generic record so the chainable vi.fn() mocks assign cleanly.
const mockedSummary = core.summary as unknown as Record<string, unknown>
mockedSummary.clear = summary.clear
mockedSummary.addRaw = summary.addRaw
mockedSummary.write = summary.write

return summary
}

/**
* Set `context.payload.pull_request` so the forked-PR detection in
* `Vet.pullRequestHeadRef()` can be exercised. Requires the test file to mock
* `@actions/github` with a writable `context` object, e.g.
* `vi.mock('@actions/github', () => ({ getOctokit: vi.fn(), context: { payload: {} } }))`.
*/
export function stubPullRequestContext(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pullRequest: Record<string, any> = {}
): Record<string, unknown> {
const pr = {
number: 123,
head: {
ref: 'feature-branch',
repo: { full_name: 'owner/repo' }
},
...pullRequest
}

context.payload = { pull_request: pr }
return pr
}
2 changes: 1 addition & 1 deletion __tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { describe, it, expect, vi } from 'vitest'
import * as main from '../src/main'

// Mock the action's entrypoint
const runMock = vi.spyOn(main, 'run').mockImplementation()
const runMock = vi.spyOn(main, 'run').mockImplementation(async () => {})

describe('index', () => {
it('calls run when imported', async () => {
Expand Down
Loading
Loading