Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions .cspell/project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ streetsidesoftware
prerelease
workflow_dispatch
checkout
startgroup
endgroup
Comment thread
goanpeca marked this conversation as resolved.
Outdated
ncconfig
nvmrc

Expand Down
115 changes: 115 additions & 0 deletions __tests__/commands/command-logs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { headCommand } from '../../src/commands/head.ts'
import { hideCommand } from '../../src/commands/hide.ts'
import { unhideCommand } from '../../src/commands/unhide.ts'
import { tryStat } from '../../src/fs.ts'
import { captureStdout, makeFixture, makeInputs, seedFile, type TestFixture } from '../_helpers.ts'

// These tests pin the user-visible log surface (group labels + info lines) and
// the missing-source error wording for the small command wrappers, plus the
// tryStat error-swallowing contract. They exist to keep mutation coverage on
// code that is otherwise only exercised for its return value.
describe('head/hide/unhide log + error surface', () => {
let fx: TestFixture

beforeEach(async () => {
fx = await makeFixture('gh-action-logs')
})

afterEach(async () => {
await rm(fx.workDir, { recursive: true, force: true })
})

it('head: groups the probe and logs size/type/sha1', async () => {
await seedFile(fx, 'h.txt', 'head-me')
let result: Awaited<ReturnType<typeof headCommand>> | undefined
const out = await captureStdout(async () => {
result = await headCommand(fx.bucket, makeInputs('head', fx, { source: 'h.txt' }))
})
expect(result?.contentSha1).toBeTruthy()
expect(out).toContain('::group::head b2://gh-action-logs/h.txt')
expect(out).toContain(
`size=${result?.size} type=${result?.contentType} sha1=${result?.contentSha1}`,
)
expect(out).toContain('::endgroup::')
})

it('head: requires a source and names the action in the error', async () => {
await expect(headCommand(fx.bucket, makeInputs('head', fx))).rejects.toThrow(
"'source' input is required for 'head' action (the B2 file name)",
)
})

it('hide: groups the call and logs the created marker', async () => {
await seedFile(fx, 'g.txt', 'hide-me')
let result: Awaited<ReturnType<typeof hideCommand>> | undefined
const out = await captureStdout(async () => {
result = await hideCommand(fx.bucket, makeInputs('hide', fx, { source: 'g.txt' }))
})
expect(out).toContain('::group::hide b2://gh-action-logs/g.txt')
expect(out).toContain(`hidden: g.txt (marker fileId=${result?.fileId})`)
expect(out).toContain('::endgroup::')
})

it('hide: requires a source and names the action in the error', async () => {
await expect(hideCommand(fx.bucket, makeInputs('hide', fx))).rejects.toThrow(
"'source' input is required for 'hide' action (the B2 file name)",
)
})

it('unhide: logs the removed marker when one exists', async () => {
await seedFile(fx, 'u.txt', 'unhide-me')
await fx.bucket.hideFile('u.txt')
let result: Awaited<ReturnType<typeof unhideCommand>> | undefined
const out = await captureStdout(async () => {
result = await unhideCommand(fx.bucket, makeInputs('unhide', fx, { source: 'u.txt' }))
})
expect(result?.removedMarkerFileId).not.toBeNull()
Comment thread
goanpeca marked this conversation as resolved.
Outdated
expect(out).toContain('::group::unhide b2://gh-action-logs/u.txt')
expect(out).toContain(
`removed hide marker fileId=${result?.removedMarkerFileId}, u.txt is now visible`,
)
})

it('unhide: reports a no-op when there is no hide marker', async () => {
await seedFile(fx, 'v.txt', 'visible')
let result: Awaited<ReturnType<typeof unhideCommand>> | undefined
const out = await captureStdout(async () => {
result = await unhideCommand(fx.bucket, makeInputs('unhide', fx, { source: 'v.txt' }))
})
expect(result?.removedMarkerFileId).toBeNull()
expect(out).toContain('no hide marker found for v.txt (already visible or non-existent)')
})

it('unhide: requires a source and names the action in the error', async () => {
await expect(unhideCommand(fx.bucket, makeInputs('unhide', fx))).rejects.toThrow(
"'source' input is required for 'unhide' action (the B2 file name)",
)
})
})

describe('tryStat', () => {
let fx: TestFixture

beforeEach(async () => {
fx = await makeFixture('gh-action-try-stat')
})

afterEach(async () => {
await rm(fx.workDir, { recursive: true, force: true })
})

it('returns Stats for an existing path', async () => {
const p = join(fx.workDir, 'present.txt')
await writeFile(p, 'x')
const s = await tryStat(p)
expect(s?.isFile()).toBe(true)
})

it('returns undefined instead of throwing for a missing path', async () => {
const s = await tryStat(join(fx.workDir, 'does', 'not', 'exist.txt'))
expect(s).toBeUndefined()
})
})
63 changes: 62 additions & 1 deletion __tests__/commands/head-purge-multipresign.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { type PresignedFile, presignCommand } from '../../src/commands/presign.t
import { purgeCommand } from '../../src/commands/purge.ts'
import { uploadCommand } from '../../src/commands/upload.ts'
import { setSummaryJsonOutput } from '../../src/outputs.ts'
import { captureStdout, makeFixture, makeInputs, type TestFixture } from '../_helpers.ts'
import { captureStdout, makeFixture, makeInputs, seedFile, type TestFixture } from '../_helpers.ts'

function inputs(action: Parameters<typeof makeInputs>[0], over: Record<string, unknown> = {}) {
return makeInputs(action, { bucket: 'gh-action-hpx', ...over })
Expand Down Expand Up @@ -83,6 +83,67 @@ describe('purge command', () => {
/'allow-bucket-purge' must be true/,
)
})

it('purges the whole bucket when allow-bucket-purge is set, warning loudly', async () => {
await seedFile(fx, 'a/x.txt', 'x')
await seedFile(fx, 'top.txt', 'y')
let result: Awaited<ReturnType<typeof purgeCommand>> | undefined
const out = await captureStdout(async () => {
result = await purgeCommand(fx.bucket, inputs('purge', { allowBucketPurge: true }))
})
expect(result?.errors).toBe(0)
expect(result?.files.length).toBeGreaterThanOrEqual(2)
expect(result?.files.every((f) => f.action === 'upload' && !f.skipped)).toBe(true)
expect(out).toContain('::warning::')
expect(out).toContain('permanently delete EVERY version in bucket "gh-action-hpx"')
expect(out).toContain('::group::purge b2://gh-action-hpx/ (all versions)')
expect(out).toMatch(/ {2}purged top\.txt \(/)
const after = await fx.bucket.listFileVersions({})
expect(after.files).toHaveLength(0)
})

it('previews a whole-bucket purge with no warning under dry-run', async () => {
await seedFile(fx, 'a/x.txt', 'x')
let result: Awaited<ReturnType<typeof purgeCommand>> | undefined
const out = await captureStdout(async () => {
result = await purgeCommand(
fx.bucket,
inputs('purge', { allowBucketPurge: true, dryRun: true }),
)
})
expect(result?.files.length).toBeGreaterThan(0)
expect(result?.files.every((f) => f.skipped && f.action === 'skip')).toBe(true)
expect(out).not.toContain('::warning::')
expect(out).toContain('::group::dry-run b2://gh-action-hpx/ (all versions)')
expect(out).toMatch(/ {2}would purge a\/x\.txt \(/)
const after = await fx.bucket.listFileVersions({})
expect(after.files.length).toBeGreaterThan(0)
})

it('appends a trailing slash to a prefix source that lacks one', async () => {
// 'log' must become prefix 'log/', so 'log/inside.txt' is purged but the
// sibling 'logs.txt' (which a bare 'log' prefix would also match) survives.
await seedFile(fx, 'log/inside.txt', 'in')
await seedFile(fx, 'logs.txt', 'out')
const result = await purgeCommand(fx.bucket, inputs('purge', { source: 'log' }))
expect(result.errors).toBe(0)
const inside = await fx.bucket.listFileVersions({ prefix: 'log/' })
expect(inside.files).toHaveLength(0)
const sibling = await fx.bucket.listFileVersions({ prefix: 'logs.txt' })
expect(sibling.files.length).toBeGreaterThan(0)
})

it('warns with the failed-purge message when a version cannot be deleted', async () => {
await seedFile(fx, 'pf/a.txt', 'a')
fx.sim.injectFailure({ on: 'b2_delete_file_version', status: 500, code: 'internal_error' })
let result: Awaited<ReturnType<typeof purgeCommand>> | undefined
const out = await captureStdout(async () => {
result = await purgeCommand(fx.bucket, inputs('purge', { source: 'pf/' }))
})
expect(result?.errors).toBeGreaterThanOrEqual(1)
expect(out).toContain('::warning::')
expect(out).toContain('failed to purge')
})
})

describe('presign command (prefix mode)', () => {
Expand Down
Loading
Loading