Skip to content

Commit ed15752

Browse files
committed
test(e2e): port scan section of smoke.sh + add JSON-contract + scratch helpers
Phase 1 of the smoke.sh → .mts port. Lands two helpers and the scan domain; the other 10 domains stay in smoke.sh for follow-up commits. New in `helpers/cli-execution.mts`: - `validateSocketJsonContract(stdout, expectedExitCode)` — the TypeScript port of smoke.sh's `validate_json`. Enforces the Socket `--json` contract: `ok:true` ⇒ non-null `data`; `ok:false` ⇒ non-empty `message`; `code` (when present) is a number. - `executeCliInScratch(args, options)` — runs the CLI inside an isolated `os.tmpdir()` cwd with isolated `HOME` / `XDG_CONFIG_HOME` so destructive operations never mutate the developer's real Socket config / credentials. Auto-cleans via `safeDelete()` even on failure. Supports `seedFiles: { 'package.json': '…' }` for fixtures. New file `test/e2e/scan.e2e.test.mts` (39 tests, all skipped unless `RUN_E2E_TESTS=1`): - help / dry-run / no-arg checks for `scan`, `scan create|del|list| view|metadata|report|diff` (no auth needed). - auth-required round-trip: list → pick most recent SBOM id → view + metadata + report + diff, with `--json` payloads validated against the contract. - destructive `scan create .` runs inside `executeCliInScratch` with a seeded `package.json`. - error-path checks against `--org fake_org` for create / view / report / metadata / diff. smoke.sh is unchanged this commit; future commits will port one domain at a time (organization, package, analytics, manifest, repos, plus a long-tail file). When all domains land, smoke.sh and critical-commands.e2e.test.mts both get deleted.
1 parent dd2e76f commit ed15752

2 files changed

Lines changed: 476 additions & 0 deletions

File tree

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
/**
2+
* @file E2E tests for the `socket scan` command family. Ported from
3+
* `packages/cli/test/smoke.sh`'s scan section. Exercises help, dry-run,
4+
* list/view/metadata/report/diff, and `--json` contract conformance.
5+
*
6+
* Destructive ops (creating real scans via `scan create .`) run inside an
7+
* isolated scratch HOME via `executeCliInScratch` so they never mutate the
8+
* developer's real Socket account state.
9+
*
10+
* Gated on `RUN_E2E_TESTS=1`. Auth-required tests additionally require a
11+
* Socket API token to be present.
12+
*/
13+
14+
import { beforeAll, describe, expect, it } from 'vitest'
15+
16+
import { ENV } from '../../src/constants/env.mts'
17+
import { getDefaultApiToken } from '../../src/util/socket/sdk.mts'
18+
import {
19+
executeCliCommand,
20+
executeCliInScratch,
21+
validateSocketJsonContract,
22+
} from '../helpers/cli-execution.mts'
23+
24+
const RUN = ENV.RUN_E2E_TESTS
25+
26+
describe('socket scan (e2e)', () => {
27+
let hasAuth = false
28+
29+
beforeAll(async () => {
30+
if (RUN) {
31+
hasAuth = !!(await getDefaultApiToken())
32+
}
33+
})
34+
35+
describe('help and dry-run (no auth required)', () => {
36+
it.skipIf(!RUN)('exits 2 with no subcommand (prints help)', async () => {
37+
const result = await executeCliCommand(['scan'])
38+
expect(result.code).toBe(2)
39+
})
40+
41+
it.skipIf(!RUN)('scan --help exits 0', async () => {
42+
const result = await executeCliCommand(['scan', '--help'])
43+
expect(result.code).toBe(0)
44+
expect(result.stdout).toContain('scan')
45+
})
46+
47+
it.skipIf(!RUN)('scan --dry-run exits 0', async () => {
48+
const result = await executeCliCommand(['scan', '--dry-run'])
49+
expect(result.code).toBe(0)
50+
})
51+
52+
it.skipIf(!RUN)('scan create --help exits 0', async () => {
53+
const result = await executeCliCommand(['scan', 'create', '--help'])
54+
expect(result.code).toBe(0)
55+
})
56+
57+
it.skipIf(!RUN)('scan create --dry-run (no target) exits 2', async () => {
58+
const result = await executeCliCommand(['scan', 'create', '--dry-run'])
59+
expect(result.code).toBe(2)
60+
})
61+
62+
it.skipIf(!RUN)('scan del --help exits 0', async () => {
63+
const result = await executeCliCommand(['scan', 'del', '--help'])
64+
expect(result.code).toBe(0)
65+
})
66+
67+
it.skipIf(!RUN)('scan del --dry-run exits 2', async () => {
68+
const result = await executeCliCommand(['scan', 'del', '--dry-run'])
69+
expect(result.code).toBe(2)
70+
})
71+
72+
it.skipIf(!RUN)('scan list --help exits 0', async () => {
73+
const result = await executeCliCommand(['scan', 'list', '--help'])
74+
expect(result.code).toBe(0)
75+
})
76+
77+
it.skipIf(!RUN)('scan list --dry-run exits 0', async () => {
78+
const result = await executeCliCommand(['scan', 'list', '--dry-run'])
79+
expect(result.code).toBe(0)
80+
})
81+
82+
it.skipIf(!RUN)('scan view --help exits 0', async () => {
83+
const result = await executeCliCommand(['scan', 'view', '--help'])
84+
expect(result.code).toBe(0)
85+
})
86+
87+
it.skipIf(!RUN)('scan view (no id) exits 2', async () => {
88+
const result = await executeCliCommand(['scan', 'view'])
89+
expect(result.code).toBe(2)
90+
})
91+
92+
it.skipIf(!RUN)('scan view --dry-run (no id) exits 2', async () => {
93+
const result = await executeCliCommand(['scan', 'view', '--dry-run'])
94+
expect(result.code).toBe(2)
95+
})
96+
97+
it.skipIf(!RUN)('scan metadata --help exits 0', async () => {
98+
const result = await executeCliCommand(['scan', 'metadata', '--help'])
99+
expect(result.code).toBe(0)
100+
})
101+
102+
it.skipIf(!RUN)('scan metadata --dry-run (no id) exits 2', async () => {
103+
const result = await executeCliCommand(['scan', 'metadata', '--dry-run'])
104+
expect(result.code).toBe(2)
105+
})
106+
107+
it.skipIf(!RUN)('scan report --help exits 0', async () => {
108+
const result = await executeCliCommand(['scan', 'report', '--help'])
109+
expect(result.code).toBe(0)
110+
})
111+
112+
it.skipIf(!RUN)('scan report --dry-run (no id) exits 2', async () => {
113+
const result = await executeCliCommand(['scan', 'report', '--dry-run'])
114+
expect(result.code).toBe(2)
115+
})
116+
117+
it.skipIf(!RUN)('scan diff --help exits 0', async () => {
118+
const result = await executeCliCommand(['scan', 'diff', '--help'])
119+
expect(result.code).toBe(0)
120+
})
121+
122+
it.skipIf(!RUN)('scan diff --dry-run (no ids) exits 2', async () => {
123+
const result = await executeCliCommand(['scan', 'diff', '--dry-run'])
124+
expect(result.code).toBe(2)
125+
})
126+
})
127+
128+
describe('list / view / metadata / report / diff (auth required)', () => {
129+
let sbomId: string | undefined
130+
let secondSbomId: string | undefined
131+
132+
beforeAll(async () => {
133+
if (!RUN || !hasAuth) {
134+
return
135+
}
136+
// Resolve the two most-recent scan IDs for the configured default org.
137+
// These feed the per-id checks below.
138+
const result = await executeCliCommand(['scan', 'list', '--json'])
139+
if (result.code === 0) {
140+
try {
141+
const payload = JSON.parse(result.stdout) as {
142+
data?: { results?: Array<{ id?: string }> }
143+
}
144+
const results = payload.data?.results
145+
if (Array.isArray(results)) {
146+
sbomId = results[0]?.id
147+
secondSbomId = results[1]?.id
148+
}
149+
} catch {
150+
// Fall through — per-id tests will skip themselves below.
151+
}
152+
}
153+
})
154+
155+
it.skipIf(!RUN || !hasAuth)('scan list exits 0', async () => {
156+
const result = await executeCliCommand(['scan', 'list'])
157+
expect(result.code).toBe(0)
158+
})
159+
160+
it.skipIf(!RUN || !hasAuth)('scan list --json conforms to contract', async () => {
161+
const result = await executeCliCommand(['scan', 'list', '--json'])
162+
expect(result.code).toBe(0)
163+
validateSocketJsonContract(result.stdout, 0)
164+
})
165+
166+
it.skipIf(!RUN || !hasAuth)('scan list --markdown exits 0', async () => {
167+
const result = await executeCliCommand(['scan', 'list', '--markdown'])
168+
expect(result.code).toBe(0)
169+
})
170+
171+
it.skipIf(!RUN || !hasAuth)('scan view <id> exits 0', async () => {
172+
if (!sbomId) {
173+
return
174+
}
175+
const result = await executeCliCommand(['scan', 'view', sbomId])
176+
expect(result.code).toBe(0)
177+
})
178+
179+
it.skipIf(!RUN || !hasAuth)('scan view <id> --json conforms to contract', async () => {
180+
if (!sbomId) {
181+
return
182+
}
183+
const result = await executeCliCommand(['scan', 'view', sbomId, '--json'])
184+
expect(result.code).toBe(0)
185+
validateSocketJsonContract(result.stdout, 0)
186+
})
187+
188+
it.skipIf(!RUN || !hasAuth)('scan view <id> --markdown exits 0', async () => {
189+
if (!sbomId) {
190+
return
191+
}
192+
const result = await executeCliCommand(['scan', 'view', sbomId, '--markdown'])
193+
expect(result.code).toBe(0)
194+
})
195+
196+
it.skipIf(!RUN || !hasAuth)('scan metadata <id> exits 0', async () => {
197+
if (!sbomId) {
198+
return
199+
}
200+
const result = await executeCliCommand(['scan', 'metadata', sbomId])
201+
expect(result.code).toBe(0)
202+
})
203+
204+
it.skipIf(!RUN || !hasAuth)('scan metadata <id> --json conforms to contract', async () => {
205+
if (!sbomId) {
206+
return
207+
}
208+
const result = await executeCliCommand(['scan', 'metadata', sbomId, '--json'])
209+
expect(result.code).toBe(0)
210+
validateSocketJsonContract(result.stdout, 0)
211+
})
212+
213+
it.skipIf(!RUN || !hasAuth)('scan metadata <id> --markdown exits 0', async () => {
214+
if (!sbomId) {
215+
return
216+
}
217+
const result = await executeCliCommand(['scan', 'metadata', sbomId, '--markdown'])
218+
expect(result.code).toBe(0)
219+
})
220+
221+
it.skipIf(!RUN || !hasAuth)('scan report <id> exits 0', async () => {
222+
if (!sbomId) {
223+
return
224+
}
225+
const result = await executeCliCommand(['scan', 'report', sbomId])
226+
expect(result.code).toBe(0)
227+
})
228+
229+
it.skipIf(!RUN || !hasAuth)('scan report <id> --json conforms to contract', async () => {
230+
if (!sbomId) {
231+
return
232+
}
233+
const result = await executeCliCommand(['scan', 'report', sbomId, '--json'])
234+
expect(result.code).toBe(0)
235+
validateSocketJsonContract(result.stdout, 0)
236+
})
237+
238+
it.skipIf(!RUN || !hasAuth)('scan report <id> --markdown exits 0', async () => {
239+
if (!sbomId) {
240+
return
241+
}
242+
const result = await executeCliCommand(['scan', 'report', sbomId, '--markdown'])
243+
expect(result.code).toBe(0)
244+
})
245+
246+
it.skipIf(!RUN || !hasAuth)('scan diff <id1> <id2> exits 0', async () => {
247+
if (!sbomId || !secondSbomId) {
248+
return
249+
}
250+
const result = await executeCliCommand(['scan', 'diff', sbomId, secondSbomId])
251+
expect(result.code).toBe(0)
252+
})
253+
254+
it.skipIf(!RUN || !hasAuth)('scan diff --json conforms to contract', async () => {
255+
if (!sbomId || !secondSbomId) {
256+
return
257+
}
258+
const result = await executeCliCommand(['scan', 'diff', sbomId, secondSbomId, '--json'])
259+
expect(result.code).toBe(0)
260+
validateSocketJsonContract(result.stdout, 0)
261+
})
262+
263+
it.skipIf(!RUN || !hasAuth)('scan diff --markdown exits 0', async () => {
264+
if (!sbomId || !secondSbomId) {
265+
return
266+
}
267+
const result = await executeCliCommand(['scan', 'diff', sbomId, secondSbomId, '--markdown'])
268+
expect(result.code).toBe(0)
269+
})
270+
})
271+
272+
describe('scan create (auth required, scratch-isolated)', () => {
273+
it.skipIf(!RUN || !hasAuth)(
274+
'scan create . exits 0 with --json contract',
275+
async () => {
276+
const result = await executeCliInScratch(['scan', 'create', '.', '--json'], {
277+
seedFiles: {
278+
'package.json': JSON.stringify({ name: 'socket-cli-e2e-scan', version: '0.0.0' }),
279+
},
280+
})
281+
expect(result.code).toBe(0)
282+
validateSocketJsonContract(result.stdout, 0)
283+
},
284+
)
285+
})
286+
287+
describe('error paths — non-existent org', () => {
288+
it.skipIf(!RUN || !hasAuth)(
289+
'scan create --org fake_org exits 1',
290+
async () => {
291+
const result = await executeCliCommand(['scan', 'create', '.', '--org', 'fake_org', '--json'])
292+
expect(result.code).toBe(1)
293+
validateSocketJsonContract(result.stdout, 1)
294+
},
295+
)
296+
297+
it.skipIf(!RUN || !hasAuth)('scan view --org fake_org exits 1', async () => {
298+
const result = await executeCliCommand(['scan', 'view', 'placeholder', '--org', 'fake_org', '--json'])
299+
expect(result.code).toBe(1)
300+
validateSocketJsonContract(result.stdout, 1)
301+
})
302+
303+
it.skipIf(!RUN || !hasAuth)('scan report --org fake_org exits 1', async () => {
304+
const result = await executeCliCommand(['scan', 'report', 'placeholder', '--org', 'fake_org', '--json'])
305+
expect(result.code).toBe(1)
306+
validateSocketJsonContract(result.stdout, 1)
307+
})
308+
309+
it.skipIf(!RUN || !hasAuth)('scan metadata --org fake_org exits 1', async () => {
310+
const result = await executeCliCommand(['scan', 'metadata', 'placeholder', '--org', 'fake_org', '--json'])
311+
expect(result.code).toBe(1)
312+
validateSocketJsonContract(result.stdout, 1)
313+
})
314+
315+
it.skipIf(!RUN || !hasAuth)('scan diff --org fake_org exits 1', async () => {
316+
const result = await executeCliCommand([
317+
'scan', 'diff', 'placeholder', 'placeholder', '--org', 'fake_org', '--json',
318+
])
319+
expect(result.code).toBe(1)
320+
validateSocketJsonContract(result.stdout, 1)
321+
})
322+
})
323+
})

0 commit comments

Comments
 (0)