diff --git a/.changeset/five-mirrors-marry.md b/.changeset/five-mirrors-marry.md new file mode 100644 index 00000000..c9b8232f --- /dev/null +++ b/.changeset/five-mirrors-marry.md @@ -0,0 +1,5 @@ +--- +'dotenv-diff': patch +--- + +add gitignore issues to --json diff --git a/.changeset/goofy-stamps-cross.md b/.changeset/goofy-stamps-cross.md new file mode 100644 index 00000000..a98f6cc7 --- /dev/null +++ b/.changeset/goofy-stamps-cross.md @@ -0,0 +1,5 @@ +--- +'dotenv-diff': patch +--- + +fixed gitignore warning bug diff --git a/packages/cli/src/commands/scanUsage.ts b/packages/cli/src/commands/scanUsage.ts index e4a6d6cc..37a66ed9 100644 --- a/packages/cli/src/commands/scanUsage.ts +++ b/packages/cli/src/commands/scanUsage.ts @@ -7,6 +7,7 @@ import type { ComparisonFile, } from '../config/types.js'; import { determineComparisonFile } from '../core/scan/determineComparisonFile.js'; +import { computeExitDecision } from '../core/scan/computeExitDecision.js'; import { printScanResult } from '../services/printScanResult.js'; import { scanJsonOutput } from '../ui/scan/scanJsonOutput.js'; import { printMissingExample } from '../ui/scan/printMissingExample.js'; @@ -15,11 +16,8 @@ import { printComparisonError } from '../ui/scan/printComparisonError.js'; import { skipCommentedUsages } from '../core/scan/skipCommentedUsages.js'; import { frameworkValidator } from '../core/frameworks/frameworkValidator.js'; import { detectSecretsInExample } from '../core/security/exampleSecretDetector.js'; -import { - DEFAULT_EXAMPLE_FILE, - URGENT_EXPIRE_DAYS, - EXPIRE_THRESHOLD_DAYS, -} from '../config/constants.js'; +import { DEFAULT_EXAMPLE_FILE } from '../config/constants.js'; +import { checkGitignoreStatus } from '../services/git.js'; import { promptNoEnvScenario } from './prompts/promptNoEnvScenario.js'; import { printBaselineWritten, @@ -167,50 +165,24 @@ export async function scanUsage(opts: ScanUsageOptions): Promise { // JSON output if (opts.json) { + // The console path runs this same check inside printScanResult. Both paths + // need it, and only one of them ever runs, so it is checked per branch. + const gitignoreIssue = checkGitignoreStatus({ + cwd: opts.cwd, + }); + const jsonOutput = scanJsonOutput( scanResult, comparedAgainst, opts.listAll ?? false, + gitignoreIssue, ); console.log(JSON.stringify(jsonOutput, null, 2)); - // Check for high severity secrets - const hasHighSeveritySecrets = (scanResult.secrets ?? []).some( - (s) => s.severity === 'high', - ); - - // Check for high potential secrets in example warnings - const hasHighSeverityExampleWarnings = ( - scanResult.exampleWarnings ?? [] - ).some((w) => w.severity === 'high'); - - const hasUrgentExpireWarnings = (scanResult.expireWarnings ?? []).some( - (w) => w.daysLeft <= URGENT_EXPIRE_DAYS, - ); - - return { - exitWithError: - scanResult.missing.length > 0 || - hasHighSeveritySecrets || - hasUrgentExpireWarnings || - hasHighSeverityExampleWarnings || - !!( - opts.strict && - (scanResult.unused.length > 0 || - (scanResult.duplicates?.env?.length ?? 0) > 0 || - (scanResult.duplicates?.example?.length ?? 0) > 0 || - (scanResult.secrets?.length ?? 0) > 0 || - (scanResult.frameworkWarnings?.length ?? 0) > 0 || - (scanResult.logged?.length ?? 0) > 0 || - (scanResult.uppercaseWarnings?.length ?? 0) > 0 || - (scanResult.expireWarnings?.filter( - (w) => w.daysLeft <= EXPIRE_THRESHOLD_DAYS, - ).length ?? 0) > 0 || - (scanResult.inconsistentNamingWarnings?.length ?? 0) > 0 || - (scanResult.commentWarnings?.length ?? 0) > 0 || - (scanResult.exampleWarnings?.length ?? 0) > 0) - ), - }; + return computeExitDecision(scanResult, { + strict: opts.strict, + hasGitignoreIssue: gitignoreIssue !== null, + }); } // Console output diff --git a/packages/cli/src/core/scan/computeExitDecision.ts b/packages/cli/src/core/scan/computeExitDecision.ts new file mode 100644 index 00000000..a0218832 --- /dev/null +++ b/packages/cli/src/core/scan/computeExitDecision.ts @@ -0,0 +1,87 @@ +import type { ExitResult, ScanResult } from '../../config/types.js'; +import { + EXPIRE_THRESHOLD_DAYS, + URGENT_EXPIRE_DAYS, +} from '../../config/constants.js'; + +/** + * Inputs that affect the exit decision but are not part of {@link ScanResult}. + */ +export interface ExitDecisionOptions { + /** Whether --strict is enabled, promoting warnings to failures */ + strict?: boolean | undefined; + /** + * Whether a .gitignore issue was detected. Passed in rather than checked here + * because it needs the filesystem, while this function stays pure. + */ + hasGitignoreIssue?: boolean | undefined; +} + +/** + * Determines whether a scan should exit with a non-zero code. + * + * This is the single source of truth shared by every output path, so a finding + * can never fail the build in one format and pass in another. + * + * Two tiers of findings exist: + * - Hard failures always exit non-zero: missing keys, high-severity secrets + * (in code or in the example file), and keys expiring within + * {@link URGENT_EXPIRE_DAYS} days. + * - Strict violations only exit non-zero under --strict: every remaining + * warning category. + * @param scanResult - The scan result to evaluate. + * @param opts - Options affecting the decision (strict mode, .gitignore state). + * @returns Whether the caller should exit with a non-zero code. + */ +export function computeExitDecision( + scanResult: ScanResult, + opts: ExitDecisionOptions = {}, +): ExitResult { + const exitWithError = + hasHardFailure(scanResult) || + (!!opts.strict && hasStrictViolation(scanResult, opts.hasGitignoreIssue)); + + return { exitWithError }; +} + +/** + * Findings severe enough to fail the build regardless of --strict. + * @param scan - The scan result to evaluate. + * @returns True when at least one hard failure was found. + */ +function hasHardFailure(scan: ScanResult): boolean { + return ( + scan.missing.length > 0 || + (scan.secrets ?? []).some((s) => s.severity === 'high') || + (scan.exampleWarnings ?? []).some((w) => w.severity === 'high') || + (scan.expireWarnings ?? []).some((w) => w.daysLeft <= URGENT_EXPIRE_DAYS) + ); +} + +/** + * Warning-level findings that only fail the build under --strict. + * @param scan - The scan result to evaluate. + * @param hasGitignoreIssue - Whether a .gitignore issue was detected. + * @returns True when at least one strict violation was found. + */ +function hasStrictViolation( + scan: ScanResult, + hasGitignoreIssue?: boolean, +): boolean { + return ( + scan.unused.length > 0 || + (scan.duplicates?.env?.length ?? 0) > 0 || + (scan.duplicates?.example?.length ?? 0) > 0 || + (scan.secrets?.length ?? 0) > 0 || + (scan.exampleWarnings?.length ?? 0) > 0 || + (scan.frameworkWarnings?.length ?? 0) > 0 || + (scan.logged?.length ?? 0) > 0 || + (scan.uppercaseWarnings?.length ?? 0) > 0 || + (scan.expireWarnings ?? []).some( + (w) => w.daysLeft <= EXPIRE_THRESHOLD_DAYS, + ) || + (scan.inconsistentNamingWarnings?.length ?? 0) > 0 || + (scan.commentWarnings?.length ?? 0) > 0 || + !!hasGitignoreIssue + ); +} diff --git a/packages/cli/src/services/git.ts b/packages/cli/src/services/git.ts index edf0276c..8cc7eeac 100644 --- a/packages/cli/src/services/git.ts +++ b/packages/cli/src/services/git.ts @@ -24,7 +24,7 @@ interface GitignoreCheckOptions { /** Are we in a git repo? (checks for .git directory in cwd) */ export function isGitRepo(cwd = process.cwd()): boolean { - return fs.existsSync(path.resolve(cwd, GIT_DIR)); + return findGitRoot(cwd) !== null; } /** diff --git a/packages/cli/src/services/printScanResult.ts b/packages/cli/src/services/printScanResult.ts index 89acf16b..7cd689b7 100644 --- a/packages/cli/src/services/printScanResult.ts +++ b/packages/cli/src/services/printScanResult.ts @@ -6,11 +6,7 @@ import type { ExitResult, FixContext, } from '../config/types.js'; -import { - DEFAULT_ENV_FILE, - EXPIRE_THRESHOLD_DAYS, - URGENT_EXPIRE_DAYS, -} from '../config/constants.js'; +import { DEFAULT_ENV_FILE } from '../config/constants.js'; import { printHeader } from '../ui/scan/printHeader.js'; import { printStats } from '../ui/scan/printStats.js'; import { printMissing } from '../ui/scan/printMissing.js'; @@ -24,6 +20,7 @@ import { printExampleWarnings } from '../ui/scan/printExampleWarnings.js'; import { printConsolelogWarning } from '../ui/scan/printConsolelogWarning.js'; import { printUppercaseWarning } from '../ui/scan/printUppercaseWarning.js'; import { computeHealthScore } from '../core/scan/computeHealthScore.js'; +import { computeExitDecision } from '../core/scan/computeExitDecision.js'; import { printHealthScore } from '../ui/scan/printHealthScore.js'; import { printExpireWarnings } from '../ui/scan/printExpireWarnings.js'; import { printCommentWarnings } from '../ui/scan/printCommentWarnings.js'; @@ -43,8 +40,6 @@ export function printScanResult( comparedAgainst: string, fixContext?: FixContext, ): ExitResult { - let exitWithError = false; - // Determine if output should be in JSON format const isJson = opts.json; @@ -60,16 +55,12 @@ export function printScanResult( } // Missing variables (used in code but not in env file) - if ( - printMissing( - scanResult.missing, - scanResult.used, - comparedAgainst, - scanResult.suggestions ?? [], - ) - ) { - exitWithError = true; - } + printMissing( + scanResult.missing, + scanResult.used, + comparedAgainst, + scanResult.suggestions ?? [], + ); if (scanResult.frameworkWarnings) { printFrameworkWarnings(scanResult.frameworkWarnings, opts.strict); @@ -127,32 +118,6 @@ export function printScanResult( if (scanResult.commentWarnings) { printCommentWarnings(scanResult.commentWarnings, opts.strict); } - // Check for high severity secrets - ALWAYS exit with error - const hasHighSeveritySecrets = (scanResult.secrets ?? []).some( - (s) => s.severity === 'high', - ); - - if (hasHighSeveritySecrets) { - exitWithError = true; - } - - // Check for high severity example secrets - ALWAYS exit with error - const hasHighSeverityExampleSecrets = (scanResult.exampleWarnings ?? []).some( - (w) => w.severity === 'high', - ); - - if (hasHighSeverityExampleSecrets) { - exitWithError = true; - } - - const hasUrgentExpireWarnings = (scanResult.expireWarnings ?? []).some( - (w) => w.daysLeft <= URGENT_EXPIRE_DAYS, - ); - - if (hasUrgentExpireWarnings) { - exitWithError = true; - } - // Gitignore check const gitignoreIssue = checkGitignoreStatus({ cwd: opts.cwd, @@ -169,25 +134,10 @@ export function printScanResult( const hasGitignoreIssue = gitignoreIssue !== null; - if (opts.strict) { - const hasStrictViolations = - scanResult.unused.length > 0 || - (scanResult.duplicates?.env?.length ?? 0) > 0 || - (scanResult.duplicates?.example?.length ?? 0) > 0 || - (scanResult.secrets?.length ?? 0) > 0 || - (scanResult.exampleWarnings?.length ?? 0) > 0 || - hasGitignoreIssue || - (scanResult.frameworkWarnings?.length ?? 0) > 0 || - (scanResult.logged?.length ?? 0) > 0 || - (scanResult.uppercaseWarnings?.length ?? 0) > 0 || - (scanResult.expireWarnings?.filter( - (w) => w.daysLeft <= EXPIRE_THRESHOLD_DAYS, - ).length ?? 0) > 0 || - (scanResult.inconsistentNamingWarnings?.length ?? 0) > 0 || - (scanResult.commentWarnings?.length ?? 0) > 0; - - if (hasStrictViolations) exitWithError = true; - } + const { exitWithError } = computeExitDecision(scanResult, { + strict: opts.strict, + hasGitignoreIssue, + }); if (opts.fix && fixContext) { printAutoFix(fixContext, comparedAgainst || DEFAULT_ENV_FILE, isJson); diff --git a/packages/cli/src/ui/scan/scanJsonOutput.ts b/packages/cli/src/ui/scan/scanJsonOutput.ts index 68deae33..0f199734 100644 --- a/packages/cli/src/ui/scan/scanJsonOutput.ts +++ b/packages/cli/src/ui/scan/scanJsonOutput.ts @@ -10,6 +10,7 @@ import type { FrameworkWarning, ExampleSecretWarning, TypoSuggestion, + GitignoreIssue, } from '../../config/types.js'; import { computeHealthScore } from '../../core/scan/computeHealthScore.js'; import { normalizePath } from '../../core/helpers/normalizePath.js'; @@ -50,6 +51,7 @@ interface ScanJsonOutput { env?: Duplicate[]; example?: Duplicate[]; }; + gitignoreIssue?: { reason: GitignoreIssue }; logged?: EnvUsage[]; expireWarnings?: ExpireWarning[]; commentWarnings?: CommentWarning[]; @@ -64,12 +66,15 @@ interface ScanJsonOutput { * Creates a JSON output for the scan results. * @param scanResult - The result of the scan. * @param comparedAgainst - The file being compared against. + * @param listAll - Whether it should list all variables + * @param gitignoreIssue - A detected .gitignore issue, or null when there is none. * @returns The JSON output. */ export function scanJsonOutput( scanResult: ScanResult, comparedAgainst: string, listAll: boolean = false, + gitignoreIssue: { reason: GitignoreIssue } | null = null, ): ScanJsonOutput { const output: ScanJsonOutput = {}; @@ -160,6 +165,10 @@ export function scanJsonOutput( output.duplicates = scanResult.duplicates; } + if (gitignoreIssue) { + output.gitignoreIssue = gitignoreIssue; + } + // Add logged variables if any if (scanResult.logged?.length) { output.logged = scanResult.logged.map((l) => ({ diff --git a/packages/cli/test/unit/commands/scanUsage.test.ts b/packages/cli/test/unit/commands/scanUsage.test.ts index 7d3117f8..088f2961 100644 --- a/packages/cli/test/unit/commands/scanUsage.test.ts +++ b/packages/cli/test/unit/commands/scanUsage.test.ts @@ -23,6 +23,10 @@ vi.mock('../../../src/ui/scan/scanJsonOutput.js', () => ({ scanJsonOutput: vi.fn(() => ({ ok: true })), })); +vi.mock('../../../src/services/git.js', () => ({ + checkGitignoreStatus: vi.fn(() => null), +})); + vi.mock('../../../src/ui/scan/printMissingExample.js', () => ({ printMissingExample: vi.fn(() => false), })); @@ -75,6 +79,8 @@ import { scanUsage } from '../../../src/commands/scanUsage.js'; import { scanCodebase } from '../../../src/services/scanCodebase.js'; import { determineComparisonFile } from '../../../src/core/scan/determineComparisonFile.js'; import { printScanResult } from '../../../src/services/printScanResult.js'; +import { checkGitignoreStatus } from '../../../src/services/git.js'; +import { scanJsonOutput } from '../../../src/ui/scan/scanJsonOutput.js'; import { processComparisonFile } from '../../../src/services/processComparisonFile.js'; import { printMissingExample } from '../../../src/ui/scan/printMissingExample.js'; import { printComparisonError } from '../../../src/ui/scan/printComparisonError.js'; @@ -132,6 +138,7 @@ describe('scanUsage', () => { vi.mocked(scanCodebase).mockResolvedValue({ ...baseScanResult }); vi.mocked(determineComparisonFile).mockResolvedValue({ type: 'none' }); vi.mocked(printScanResult).mockReturnValue({ exitWithError: false }); + vi.mocked(checkGitignoreStatus).mockReturnValue(null); vi.mocked(printMissingExample).mockReturnValue(false); vi.mocked(promptNoEnvScenario).mockResolvedValue({ compareFile: undefined, @@ -155,6 +162,62 @@ describe('scanUsage', () => { expect(result.exitWithError).toBe(true); }); + describe('gitignore in JSON mode', () => { + const issue = { reason: 'not-ignored' as const }; + + it('checks gitignore with cwd and the default env file', async () => { + await scanUsage({ ...baseOpts, json: true }); + + expect(checkGitignoreStatus).toHaveBeenCalledWith({ + cwd: '/root', + }); + }); + + it('passes the issue to the JSON output', async () => { + vi.mocked(checkGitignoreStatus).mockReturnValue(issue); + + await scanUsage({ ...baseOpts, json: true }); + + expect(scanJsonOutput).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + issue, + ); + }); + + it('passes null to the JSON output when there is no issue', async () => { + await scanUsage({ ...baseOpts, json: true }); + + expect(scanJsonOutput).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + null, + ); + }); + + it('fails under --strict when an issue exists', async () => { + vi.mocked(checkGitignoreStatus).mockReturnValue(issue); + + const result = await scanUsage({ ...baseOpts, json: true, strict: true }); + + expect(result.exitWithError).toBe(true); + }); + + it('does not fail without --strict', async () => { + vi.mocked(checkGitignoreStatus).mockReturnValue(issue); + + const result = await scanUsage({ + ...baseOpts, + json: true, + strict: false, + }); + + expect(result.exitWithError).toBe(false); + }); + }); + it('exits when comparison error requests exit', async () => { vi.mocked(determineComparisonFile).mockResolvedValue({ type: 'found', diff --git a/packages/cli/test/unit/core/scan/computeExitDecision.test.ts b/packages/cli/test/unit/core/scan/computeExitDecision.test.ts new file mode 100644 index 00000000..050a8c03 --- /dev/null +++ b/packages/cli/test/unit/core/scan/computeExitDecision.test.ts @@ -0,0 +1,210 @@ +import { describe, it, expect } from 'vitest'; +import { computeExitDecision } from '../../../../src/core/scan/computeExitDecision.js'; +import type { ScanResult } from '../../../../src/config/types.js'; +import { + EXPIRE_THRESHOLD_DAYS, + URGENT_EXPIRE_DAYS, +} from '../../../../src/config/constants.js'; + +const base: ScanResult = { + used: [], + missing: [], + unused: [], + stats: { + filesScanned: 1, + totalUsages: 0, + uniqueVariables: 0, + warningsCount: 0, + duration: 0, + }, + secrets: [], + duplicates: {}, + logged: [], +}; + +/** Builds a scan result with the given overrides applied to the clean base. */ +const scan = (overrides: Partial = {}): ScanResult => ({ + ...base, + ...overrides, +}); + +describe('computeExitDecision', () => { + it('passes on a clean scan', () => { + expect(computeExitDecision(scan()).exitWithError).toBe(false); + expect(computeExitDecision(scan(), { strict: true }).exitWithError).toBe( + false, + ); + }); + + describe('hard failures (independent of --strict)', () => { + it('fails on missing keys', () => { + expect( + computeExitDecision(scan({ missing: ['API_KEY'] })).exitWithError, + ).toBe(true); + }); + + it('fails on a high severity secret but not a medium one', () => { + const secret = (severity: 'high' | 'medium') => + scan({ + secrets: [ + { + file: 'a.ts', + line: 1, + kind: 'pattern' as const, + message: 'm', + snippet: 's', + severity, + }, + ], + }); + + expect(computeExitDecision(secret('high')).exitWithError).toBe(true); + expect(computeExitDecision(secret('medium')).exitWithError).toBe(false); + }); + + it('fails on a high severity example secret but not a low one', () => { + const warning = (severity: 'high' | 'low') => + scan({ + exampleWarnings: [{ key: 'K', value: 'v', reason: 'r', severity }], + }); + + expect(computeExitDecision(warning('high')).exitWithError).toBe(true); + expect(computeExitDecision(warning('low')).exitWithError).toBe(false); + }); + + it('fails on urgently expiring keys only', () => { + const expiring = (daysLeft: number) => + scan({ expireWarnings: [{ key: 'K', date: '2030-01-01', daysLeft }] }); + + expect( + computeExitDecision(expiring(URGENT_EXPIRE_DAYS)).exitWithError, + ).toBe(true); + expect( + computeExitDecision(expiring(URGENT_EXPIRE_DAYS + 1)).exitWithError, + ).toBe(false); + }); + }); + + describe('strict violations', () => { + const cases: Array<[string, Partial]> = [ + ['unused keys', { unused: ['OLD_KEY'] }], + ['duplicate env keys', { duplicates: { env: [{ key: 'K', count: 2 }] } }], + [ + 'duplicate example keys', + { duplicates: { example: [{ key: 'K', count: 2 }] } }, + ], + [ + 'medium severity secrets', + { + secrets: [ + { + file: 'a.ts', + line: 1, + kind: 'pattern', + message: 'm', + snippet: 's', + severity: 'medium', + }, + ], + }, + ], + [ + 'framework warnings', + { + frameworkWarnings: [ + { + variable: 'V', + reason: 'r', + file: 'a.ts', + line: 1, + framework: 'nextjs', + }, + ], + }, + ], + [ + 'logged usages', + { + logged: [ + { + variable: 'V', + file: 'a.ts', + line: 1, + column: 1, + pattern: 'process.env', + context: 'c', + }, + ], + }, + ], + [ + 'uppercase warnings', + { uppercaseWarnings: [{ key: 'k', suggestion: 'K' }] }, + ], + [ + 'expire warnings within the threshold', + { + expireWarnings: [ + { key: 'K', date: '2030-01-01', daysLeft: EXPIRE_THRESHOLD_DAYS }, + ], + }, + ], + [ + 'inconsistent naming warnings', + { + inconsistentNamingWarnings: [ + { key1: 'A_B', key2: 'AB', suggestion: 'A_B' }, + ], + }, + ], + ['comment warnings', { commentWarnings: [{ key: 'K', line: 3 }] }], + ]; + + for (const [name, overrides] of cases) { + it(`fails on ${name} only under --strict`, () => { + expect(computeExitDecision(scan(overrides)).exitWithError).toBe(false); + expect( + computeExitDecision(scan(overrides), { strict: true }).exitWithError, + ).toBe(true); + }); + } + + it('does not fail on expire warnings beyond the strict threshold', () => { + const result = computeExitDecision( + scan({ + expireWarnings: [ + { + key: 'K', + date: '2030-01-01', + daysLeft: EXPIRE_THRESHOLD_DAYS + 1, + }, + ], + }), + { strict: true }, + ); + + expect(result.exitWithError).toBe(false); + }); + }); + + describe('gitignore', () => { + it('fails under --strict when the caller reports an issue', () => { + expect( + computeExitDecision(scan(), { strict: true, hasGitignoreIssue: true }) + .exitWithError, + ).toBe(true); + }); + + it('does not fail without --strict', () => { + expect( + computeExitDecision(scan(), { hasGitignoreIssue: true }).exitWithError, + ).toBe(false); + }); + + it('is ignored when the caller does not report it (JSON output path)', () => { + expect(computeExitDecision(scan(), { strict: true }).exitWithError).toBe( + false, + ); + }); + }); +}); diff --git a/packages/cli/test/unit/services/printScanResult.test.ts b/packages/cli/test/unit/services/printScanResult.test.ts index e947d0ee..3200ede0 100644 --- a/packages/cli/test/unit/services/printScanResult.test.ts +++ b/packages/cli/test/unit/services/printScanResult.test.ts @@ -153,9 +153,11 @@ describe('printScanResult', () => { }); it('returns exitWithError true when missing variables exist', () => { - vi.mocked(printMissing).mockReturnValue(true); - - const result = printScanResult(baseScanResult, baseOpts, '.env'); + const result = printScanResult( + { ...baseScanResult, missing: ['API_KEY'] }, + baseOpts, + '.env', + ); expect(result.exitWithError).toBe(true); }); diff --git a/packages/cli/test/unit/ui/scan/scanJsonOutput.test.ts b/packages/cli/test/unit/ui/scan/scanJsonOutput.test.ts index 459e15ff..c10df5b0 100644 --- a/packages/cli/test/unit/ui/scan/scanJsonOutput.test.ts +++ b/packages/cli/test/unit/ui/scan/scanJsonOutput.test.ts @@ -27,6 +27,25 @@ function makeScanResult(partial: Partial = {}): ScanResult { } describe('scanJsonOutput', () => { + it('includes the gitignore issue when one is passed', () => { + const scanResult = makeScanResult(); + + const result = scanJsonOutput(scanResult, '', false, { + reason: 'not-ignored', + }); + + expect(result.gitignoreIssue).toEqual({ reason: 'not-ignored' }); + }); + + it('omits the gitignore issue when there is none', () => { + const scanResult = makeScanResult(); + + expect( + scanJsonOutput(scanResult, '', false, null).gitignoreIssue, + ).toBeUndefined(); + expect(scanJsonOutput(scanResult, '').gitignoreIssue).toBeUndefined(); + }); + it('includes comparedAgainst when provided', () => { const scanResult = makeScanResult();