You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
rely on process.exit(1) being called for D/F grades (cli.ts:95). That exit-code path is completely untested today. So are argument parsing, --json output, --timeout, error handling, and the --version flag.
Coverage today:
Module
Statement coverage
rules.ts
~100%
analyzer.ts
~100%
index.ts
~50% (only analyzeHeaders(object) path; analyze(string) with real fetch is never called)
fetch.ts
0%
cli.ts
0%
Proposed fix
Add a test/cli.test.ts (and optionally test/fetch.test.ts) using Vitest's built-in module mocking — no extra dependencies needed.
Sketch of test/cli.test.ts
import{vi,describe,it,expect,beforeEach,afterEach}from'vitest';// Mock fetch so tests never hit the networkvi.mock('../src/fetch.js',()=>({fetchHeaders: vi.fn(),}));import{fetchHeaders}from'../src/fetch.js';// Helper: run main() with given argv, capture stdout/stderr, and catch process.exitasyncfunctionrunCli(args: string[]): Promise<{stdout: string;stderr: string;exitCode: number}>{constoriginalArgv=process.argv;constoriginalExit=process.exit;conststdoutLines: string[]=[];conststderrLines: string[]=[];letexitCode=0;process.argv=['node','cli.js', ...args];vi.spyOn(process,'exit').mockImplementation((code?: number)=>{exitCode=code??0;thrownewError('process.exit');});vi.spyOn(console,'log').mockImplementation((...a)=>stdoutLines.push(a.join(' ')));vi.spyOn(console,'error').mockImplementation((...a)=>stderrLines.push(a.join(' ')));try{// dynamically import to re-execute main() each testawaitimport('../src/cli.js?t='+Date.now());}catch{/* swallow process.exit throw */}process.argv=originalArgv;process.exit=originalExit;vi.restoreAllMocks();return{stdout: stdoutLines.join('\n'),stderr: stderrLines.join('\n'), exitCode };}// Good-grade headers fixtureconstA_HEADERS={/* full set of strong headers */};// Bad-grade headers fixture (empty → F)constF_HEADERS={};describe('CLI',()=>{it('exits 0 for an A-grade site',async()=>{vi.mocked(fetchHeaders).mockResolvedValue(A_HEADERS);const{ exitCode }=awaitrunCli(['https://example.com']);expect(exitCode).toBe(0);});it('exits 1 for an F-grade site (CI gate)',async()=>{vi.mocked(fetchHeaders).mockResolvedValue(F_HEADERS);const{ exitCode }=awaitrunCli(['https://bad.example.com']);expect(exitCode).toBe(1);});it('--json outputs valid JSON containing grade and score',async()=>{vi.mocked(fetchHeaders).mockResolvedValue(A_HEADERS);const{ stdout }=awaitrunCli(['https://example.com','--json']);constreport=JSON.parse(stdout);expect(report).toHaveProperty('grade');expect(report).toHaveProperty('score');});it('--version prints a semver string',async()=>{const{ stdout, exitCode }=awaitrunCli(['--version']);expect(stdout).toMatch(/^\d+\.\d+\.\d+/);expect(exitCode).toBe(0);});it('--help prints usage and exits 0',async()=>{const{ stdout, exitCode }=awaitrunCli(['--help']);expect(stdout).toContain('Usage');expect(exitCode).toBe(0);});it('missing URL exits 1 with usage message',async()=>{const{ stderr, exitCode }=awaitrunCli([]);expect(exitCode).toBe(1);expect(stderr).toContain('Usage');});it('network error exits 1 with an error message',async()=>{vi.mocked(fetchHeaders).mockRejectedValue(newError('ECONNREFUSED'));const{ stderr, exitCode }=awaitrunCli(['https://unreachable.example.com']);expect(exitCode).toBe(1);expect(stderr).toContain('ECONNREFUSED');});it('--timeout passes the value through to fetchHeaders',async()=>{vi.mocked(fetchHeaders).mockResolvedValue(A_HEADERS);awaitrunCli(['https://example.com','--timeout','3000']);expect(fetchHeaders).toHaveBeenCalledWith('https://example.com',{timeoutMs: 3000});});});
test/fetch.test.ts (network layer)
Use Vitest's vi.stubGlobal('fetch', ...) to mock the global fetch, then verify that:
headers are collected and lower-cased
AbortController fires after timeoutMs elapses (use fake timers)
Response body is cancelled after headers are read
Why this is worth doing
The CI-gate exit code is the Add Dev.to launch post for security-headers CLI tool #1 feature — one line of untested code (if (report.grade === 'D' || report.grade === 'F') process.exit(1)) is what every CI user depends on.
--json correctness matters for downstream tooling — teams pipe the JSON into dashboards, scripts, and SIEM tools; a regression here would be silent.
analyze(string) in index.ts is also uncovered — the end-to-end path from URL → fetch → analyze → report is never exercised, so a future refactor could break the library's primary API silently.
Zero new runtime dependencies — Vitest already has vi.mock, vi.spyOn, and vi.stubGlobal; no msw or other packages needed.
Problem
The project's primary selling point is its use as a CI gate — runs like:
rely on
process.exit(1)being called for D/F grades (cli.ts:95). That exit-code path is completely untested today. So are argument parsing,--jsonoutput,--timeout, error handling, and the--versionflag.Coverage today:
rules.tsanalyzer.tsindex.tsanalyzeHeaders(object)path;analyze(string)with real fetch is never called)fetch.tscli.tsProposed fix
Add a
test/cli.test.ts(and optionallytest/fetch.test.ts) using Vitest's built-in module mocking — no extra dependencies needed.Sketch of
test/cli.test.tstest/fetch.test.ts(network layer)Use Vitest's
vi.stubGlobal('fetch', ...)to mock the globalfetch, then verify that:AbortControllerfires aftertimeoutMselapses (use fake timers)Why this is worth doing
if (report.grade === 'D' || report.grade === 'F') process.exit(1)) is what every CI user depends on.--jsoncorrectness matters for downstream tooling — teams pipe the JSON into dashboards, scripts, and SIEM tools; a regression here would be silent.analyze(string)inindex.tsis also uncovered — the end-to-end path from URL → fetch → analyze → report is never exercised, so a future refactor could break the library's primary API silently.vi.mock,vi.spyOn, andvi.stubGlobal; nomswor other packages needed.