mirror of
https://github.com/screenci/screenci.git
synced 2026-09-19 08:57:46 +08:00
249 lines
8.3 KiB
TypeScript
249 lines
8.3 KiB
TypeScript
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import path from 'node:path'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { logger } from './src/logger.js'
|
|
import {
|
|
SECRET_HEADER,
|
|
ANON_TOKEN_HEADER,
|
|
SCREENCI_TERMS_URL,
|
|
formatAnonTermsNotice,
|
|
} from './src/anonSession.js'
|
|
|
|
describe('resolveUploadCredential', () => {
|
|
let screenciDir: string
|
|
const originalFetch = global.fetch
|
|
const envFilePath = () => path.join(screenciDir, '.env')
|
|
|
|
beforeEach(async () => {
|
|
// The Terms notice is once-per-invocation; each test starts fresh.
|
|
const { resetAnonTermsNoticeShownForTests } = await import('./cli')
|
|
resetAnonTermsNoticeShownForTests()
|
|
screenciDir = mkdtempSync(path.join(tmpdir(), 'screenci-upload-cred-'))
|
|
vi.spyOn(logger, 'info').mockImplementation(() => {})
|
|
})
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch
|
|
rmSync(screenciDir, { recursive: true, force: true })
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('uses the real secret unchanged when SCREENCI_SECRET is set, without touching the anon token file', async () => {
|
|
const { resolveUploadCredential } = await import('./cli')
|
|
|
|
const result = await resolveUploadCredential(
|
|
screenciDir,
|
|
'https://api.example.com',
|
|
envFilePath(),
|
|
'sec_real'
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
credential: { header: SECRET_HEADER, value: 'sec_real' },
|
|
usedAnonCredential: false,
|
|
})
|
|
expect(existsSync(path.join(screenciDir, 'anon-session.json'))).toBe(false)
|
|
})
|
|
|
|
it('mints and uses an anon token when the session is pending', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
json: async () => ({ status: 'pending' }),
|
|
}) as unknown as typeof fetch
|
|
|
|
const { resolveUploadCredential } = await import('./cli')
|
|
const result = await resolveUploadCredential(
|
|
screenciDir,
|
|
'https://api.example.com',
|
|
envFilePath(),
|
|
undefined
|
|
)
|
|
|
|
expect(result.usedAnonCredential).toBe(true)
|
|
expect(result.credential.header).toBe(ANON_TOKEN_HEADER)
|
|
expect(existsSync(path.join(screenciDir, 'anon-session.json'))).toBe(true)
|
|
})
|
|
|
|
it('auto-graduates to the real secret when the session was claimed, and deletes the local anon state', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
json: async () => ({ status: 'claimed', secret: 'sec_claimed' }),
|
|
}) as unknown as typeof fetch
|
|
|
|
const { resolveUploadCredential } = await import('./cli')
|
|
const result = await resolveUploadCredential(
|
|
screenciDir,
|
|
'https://api.example.com',
|
|
envFilePath(),
|
|
undefined
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
credential: { header: SECRET_HEADER, value: 'sec_claimed' },
|
|
usedAnonCredential: false,
|
|
})
|
|
expect(readFileSync(envFilePath(), 'utf-8')).toContain(
|
|
'SCREENCI_SECRET=sec_claimed'
|
|
)
|
|
expect(existsSync(path.join(screenciDir, 'anon-session.json'))).toBe(false)
|
|
})
|
|
|
|
it('proceeds with the anon token (no nag, no silent re-mint) when the server has not seen the token yet', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
json: async () => ({ status: 'not_found' }),
|
|
}) as unknown as typeof fetch
|
|
|
|
const { resolveUploadCredential } = await import('./cli')
|
|
const result = await resolveUploadCredential(
|
|
screenciDir,
|
|
'https://api.example.com',
|
|
envFilePath(),
|
|
undefined
|
|
)
|
|
|
|
expect(result.usedAnonCredential).toBe(true)
|
|
expect(result.credential.header).toBe(ANON_TOKEN_HEADER)
|
|
// The old "Your previous trial expired or was already claimed. Starting a
|
|
// new one." nag is gone: gating happens before recording now.
|
|
expect(logger.info).not.toHaveBeenCalledWith(
|
|
expect.stringContaining('Starting a new one')
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('ensureAnonRecordingAllowedOrExit', () => {
|
|
let screenciDir: string
|
|
const originalFetch = global.fetch
|
|
let exitSpy: ReturnType<typeof vi.spyOn>
|
|
|
|
beforeEach(async () => {
|
|
// The Terms notice is once-per-invocation; each test starts fresh.
|
|
const { resetAnonTermsNoticeShownForTests } = await import('./cli')
|
|
resetAnonTermsNoticeShownForTests()
|
|
screenciDir = mkdtempSync(path.join(tmpdir(), 'screenci-anon-gate-'))
|
|
vi.spyOn(logger, 'error').mockImplementation(() => {})
|
|
vi.spyOn(logger, 'info').mockImplementation(() => {})
|
|
// Throw so the function stops at process.exit like the real process would,
|
|
// instead of falling through past it in the test.
|
|
exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {
|
|
throw new Error('process.exit')
|
|
}) as never)
|
|
})
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch
|
|
rmSync(screenciDir, { recursive: true, force: true })
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('is a no-op when a real SCREENCI_SECRET is set (no status call, no exit)', async () => {
|
|
const fetchSpy = vi.fn()
|
|
global.fetch = fetchSpy as unknown as typeof fetch
|
|
|
|
const { ensureAnonRecordingAllowedOrExit } = await import('./cli')
|
|
await ensureAnonRecordingAllowedOrExit(
|
|
screenciDir,
|
|
'https://api.example.com',
|
|
'https://app.example.com',
|
|
'sec_real'
|
|
)
|
|
|
|
expect(fetchSpy).not.toHaveBeenCalled()
|
|
expect(exitSpy).not.toHaveBeenCalled()
|
|
// The secret path is fully signed-in; no anonymous Terms notice is printed.
|
|
expect(logger.info).not.toHaveBeenCalledWith(
|
|
expect.stringContaining(SCREENCI_TERMS_URL)
|
|
)
|
|
})
|
|
|
|
it('allows a first-run (server has not seen the token) without exiting, and prints the Terms notice up front', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
json: async () => ({ status: 'not_found' }),
|
|
}) as unknown as typeof fetch
|
|
|
|
const { ensureAnonRecordingAllowedOrExit } = await import('./cli')
|
|
await ensureAnonRecordingAllowedOrExit(
|
|
screenciDir,
|
|
'https://api.example.com',
|
|
'https://app.example.com',
|
|
undefined
|
|
)
|
|
|
|
expect(exitSpy).not.toHaveBeenCalled()
|
|
expect(logger.info).toHaveBeenCalledWith(formatAnonTermsNotice())
|
|
})
|
|
|
|
it('prints the Terms notice up front on a pending trial', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
json: async () => ({ status: 'pending' }),
|
|
}) as unknown as typeof fetch
|
|
|
|
const { ensureAnonRecordingAllowedOrExit } = await import('./cli')
|
|
await ensureAnonRecordingAllowedOrExit(
|
|
screenciDir,
|
|
'https://api.example.com',
|
|
'https://app.example.com',
|
|
undefined
|
|
)
|
|
|
|
expect(exitSpy).not.toHaveBeenCalled()
|
|
expect(logger.info).toHaveBeenCalledWith(formatAnonTermsNotice())
|
|
})
|
|
|
|
it('does not print the Terms notice for a claimed session (already accepted on sign-up)', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
json: async () => ({ status: 'claimed', secret: 'sec_real' }),
|
|
}) as unknown as typeof fetch
|
|
|
|
const { ensureAnonRecordingAllowedOrExit } = await import('./cli')
|
|
await ensureAnonRecordingAllowedOrExit(
|
|
screenciDir,
|
|
'https://api.example.com',
|
|
'https://app.example.com',
|
|
undefined
|
|
)
|
|
|
|
expect(exitSpy).not.toHaveBeenCalled()
|
|
expect(logger.info).not.toHaveBeenCalledWith(
|
|
expect.stringContaining(SCREENCI_TERMS_URL)
|
|
)
|
|
})
|
|
|
|
it('allows recording even when the server reports the legacy used/remaining fields (previews are uncapped)', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
json: async () => ({ status: 'pending', used: true, remaining: 0 }),
|
|
}) as unknown as typeof fetch
|
|
|
|
const { ensureAnonRecordingAllowedOrExit } = await import('./cli')
|
|
await ensureAnonRecordingAllowedOrExit(
|
|
screenciDir,
|
|
'https://api.example.com',
|
|
'https://app.example.com',
|
|
undefined
|
|
)
|
|
|
|
expect(exitSpy).not.toHaveBeenCalled()
|
|
expect(logger.info).toHaveBeenCalledWith(formatAnonTermsNotice())
|
|
})
|
|
|
|
it('blocks and exits before recording when the trial has expired', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
json: async () => ({ status: 'expired' }),
|
|
}) as unknown as typeof fetch
|
|
|
|
const { ensureAnonRecordingAllowedOrExit } = await import('./cli')
|
|
await expect(
|
|
ensureAnonRecordingAllowedOrExit(
|
|
screenciDir,
|
|
'https://api.example.com',
|
|
'https://app.example.com',
|
|
undefined
|
|
)
|
|
).rejects.toThrow('process.exit')
|
|
|
|
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
expect(logger.error).toHaveBeenCalledWith(
|
|
expect.stringContaining('trial has expired')
|
|
)
|
|
})
|
|
})
|