specproof 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -27
- package/components/CoverageProof.tsx +2 -2
- package/next.config.js +3 -0
- package/package.json +26 -10
- package/scripts/cli.ts +113 -0
- package/scripts/generate-proof.ts +100 -17
- package/.github/workflows/release.yml +0 -49
- package/.github/workflows/test-unit.yml +0 -69
- package/CHANGELOG.md +0 -47
- package/CLAUDE.md +0 -82
- package/app/proof-contract.test.ts +0 -95
- package/app/proof.generated.json +0 -288
- package/bun.lock +0 -991
- package/eslint.config.mjs +0 -43
- package/example/api/openapi.json +0 -88
- package/example/tests/auth.test.ts +0 -25
- package/example/tests/client.ts +0 -53
- package/example/tests/tasks.test.ts +0 -83
- package/lib/api-test-coverage.test.ts +0 -423
- package/marketing/index.html +0 -1083
- package/marketing/package.json +0 -10
- package/marketing/server.ts +0 -27
- package/public/banner.png +0 -0
- package/public/icon.png +0 -0
- package/public/logo.png +0 -0
- package/vitest.config.ts +0 -19
package/CLAUDE.md
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## What this is
|
|
6
|
-
|
|
7
|
-
SpecProof: a standalone Next.js app that renders an audit view of any repo's API test coverage: every OpenAPI operation and response status, cross-examined against the target repo's test assertions. Clicking a stamped verdict shows the actual test snippet that proves (or fails to prove) coverage.
|
|
8
|
-
|
|
9
|
-
This repo does not contain a real API or its tests — it audits a target repo from the outside, the way OpenAPI tooling works when installed into a repo. Nothing repo-specific is baked in. It does carry a hand-written demo target in `example/` (see "Example fixture" below), which is what gets audited when no `SPECPROOF_REPO` is set.
|
|
10
|
-
|
|
11
|
-
## Architecture: generate → checked-in artifact → contract-test
|
|
12
|
-
|
|
13
|
-
1. **Sources of truth**, read from the target repo (`SPECPROOF_REPO` env var; defaults to the current working directory):
|
|
14
|
-
- The OpenAPI spec — auto-discovered as the shallowest `openapi*.json` / `swagger*.json` in the tree, or set explicitly with `SPECPROOF_SPEC` (relative to the repo root).
|
|
15
|
-
- The repo's `*.test.ts` / `*.test.tsx` / `*.test.js` files — parsed via regex for `describe("METHOD /path")` blocks and `.status).toBe(NNN)` / `.status).toEqual(NNN)` assertions.
|
|
16
|
-
2. **Analyzer** (`lib/api-test-coverage.ts`): joins the two sources into a `CoverageReport` (tags → operations → statuses, each status with assertion counts and extracted `it()` snippets). Operations are joined on the describe title's method + path; `{param}`, `[param]`, and `:param` segments are treated as equivalent. When several test files describe the same operation, the one with the most `it()` blocks wins.
|
|
17
|
-
3. **Generator** (`scripts/generate-proof.ts`): calls the analyzer, writes the result to `app/proof.generated.json`. This is checked in, deterministic, and regenerated automatically before `dev`/`build`. When no target spec is found it leaves the existing artifact untouched (exit 0), so the app always builds.
|
|
18
|
-
4. **Consumer** (`app/page.tsx` → `components/CoverageProof.tsx`): renders the checked-in artifact only. No target checkout is needed to build or deploy the app itself. An empty proof renders an empty-state hint.
|
|
19
|
-
5. **Drift guard** (`app/proof-contract.test.ts`): fails when the artifact is stale relative to the spec/tests, when the proof's audited operations don't exactly match the spec's operations, or when a quoted snippet no longer points at a real `it()`/`test()` line. This suite self-skips (via `describe.skipIf`) when no target spec is resolvable — in this repo one always is (the example fixture), so `bun run test:unit` always exercises the full analyzer here.
|
|
20
|
-
|
|
21
|
-
## Example fixture (`example/`)
|
|
22
|
-
|
|
23
|
-
`example/` is a hand-written demo target: `example/api/openapi.json` (the fictional "TaskFlow" API, 7 operations) plus `example/tests/*.test.ts` written in exactly the `describe("METHOD /path")` / `.status).toBe(NNN)` shape the parser expects. Because spec auto-discovery finds the shallowest `openapi*.json` under the target root (cwd by default), running `dev` / `generate:proof` / `test:unit` from this repo audits the example automatically — no `SPECPROOF_REPO` needed. When the package is installed into another repo, the example sits under `node_modules/`, which the scanner excludes, so it never pollutes a real audit.
|
|
24
|
-
|
|
25
|
-
- The example files are **fixture data parsed as text**, never executed or compiled: they are excluded from vitest (`vitest.config.ts`), tsc (`tsconfig.json`), and eslint (`eslint.config.mjs`). They only need to look like a prettier-formatted test suite.
|
|
26
|
-
- The spec and tests are **deliberately out of step** so the demo shows every verdict state: fully proven operations (`POST /auth/login`, `GET /tasks`, `GET /tasks/{taskId}`), partial coverage (`POST /tasks`, `PATCH /tasks/{taskId}`), untested operations (`DELETE /tasks/{taskId}`, `GET /projects`), and one status the tests assert but the spec omits (422 on `POST /tasks`). Preserve that mix when editing.
|
|
27
|
-
- Editing anything in `example/` makes the checked-in proof stale and fails the contract tests: run `bun run generate:proof` and commit the regenerated `app/proof.generated.json`.
|
|
28
|
-
|
|
29
|
-
Because of this split, most day-to-day work happens in exactly one of two places:
|
|
30
|
-
- Changing what's audited/how coverage is computed → `lib/api-test-coverage.ts` (+ regenerate the proof).
|
|
31
|
-
- Changing how the proof is displayed → `components/CoverageProof.tsx` / `app/proof.css`, reading the existing `app/proof.generated.json` as fixture data — no target checkout needed.
|
|
32
|
-
|
|
33
|
-
## Commands
|
|
34
|
-
|
|
35
|
-
Requires [Bun](https://bun.sh).
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
bun install
|
|
39
|
-
bun run dev # generate:proof + next dev (port 3001)
|
|
40
|
-
bun run build # generate:proof + next build
|
|
41
|
-
bun run start # serve the production build (port 3001)
|
|
42
|
-
bun run generate:proof # regenerate app/proof.generated.json only
|
|
43
|
-
bun run test:unit # contract tests (vitest run; audits example/ unless SPECPROOF_REPO is set)
|
|
44
|
-
bun run test:unit:watch # vitest watch mode
|
|
45
|
-
bun run lint # eslint + tsc --noEmit
|
|
46
|
-
bun run lint:es # eslint only
|
|
47
|
-
bun run lint:ts # tsc --noEmit only
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
To run a single test file: `bunx vitest run app/proof-contract.test.ts` (contract tests) or `bunx vitest run lib/api-test-coverage.test.ts` (analyzer unit tests). The unit tests' inline fixtures deliberately use operations that don't exist in the example spec (`/widgets`, `/gadgets`) — the analyzer parses this repo's own `*.test.ts` files as raw text when auditing `example/`, and colliding paths would pollute the generated proof.
|
|
51
|
-
|
|
52
|
-
To audit a real repo instead of the bundled example:
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
SPECPROOF_REPO=/path/to/target-repo bun run generate:proof
|
|
56
|
-
# optionally, if the spec has a nonstandard name/location:
|
|
57
|
-
SPECPROOF_REPO=/path/to/target-repo SPECPROOF_SPEC=docs/api-spec.json bun run generate:proof
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Updating the proof
|
|
61
|
-
|
|
62
|
-
When a target repo's test or OpenAPI spec changes, the checked-in `app/proof.generated.json` goes stale and the contract tests fail. Fix:
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
SPECPROOF_REPO=/path/to/target-repo bun run generate:proof
|
|
66
|
-
git add app/proof.generated.json && git commit
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## CI & Releasing
|
|
70
|
-
|
|
71
|
-
`main` is protected by a repo ruleset (PR required, merge queue, no direct pushes). The flow: open a PR → the CI workflow (`.github/workflows/test-unit.yml`, jobs `Build` / `Lint` / `Test`) runs on the PR and again in the merge queue → all three checks must pass → clicking merge queues the PR and GitHub merges it (squash).
|
|
72
|
-
|
|
73
|
-
Every merge to `main` then triggers `.github/workflows/release.yml`, which publishes to npm when `package.json`'s version isn't already on the registry (needs the `NPM_TOKEN` repo secret). To release: bump `version` in `package.json`, open a PR, and merge. There is no manual release trigger.
|
|
74
|
-
|
|
75
|
-
## Notes on the parsing approach
|
|
76
|
-
|
|
77
|
-
`lib/api-test-coverage.ts` parses test files with regexes rather than a real TS/AST parser, relying on prettier-consistent formatting conventions in the audited repo:
|
|
78
|
-
- `describe("METHOD /path")` titles are matched for the HTTP method prefix (`GET|POST|PUT|DELETE|PATCH`) followed by the operation path.
|
|
79
|
-
- `it()`/`test()` blocks are extracted by matching the opening call, then scanning for the next `});` at the same indentation level — not a brace-matching parser, so it depends on consistent formatting in the audited repo.
|
|
80
|
-
- Snippet source is dedented before being stored/rendered.
|
|
81
|
-
|
|
82
|
-
If this parsing logic needs updating, check it against real test files in a target repo — the contract test's "every quoted snippet points at a real it()/test() block" check is what catches regressions here.
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
resolveSpecPath,
|
|
7
|
-
TARGET_REPO_ROOT,
|
|
8
|
-
type CoverageReport,
|
|
9
|
-
} from "@/lib/api-test-coverage";
|
|
10
|
-
import { buildProof, GENERATED_PROOF_PATH } from "@/scripts/generate-proof";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Contract tests that keep the rendered proof in lockstep with the target
|
|
14
|
-
* repo it audits. Three drift modes are covered:
|
|
15
|
-
*
|
|
16
|
-
* 1. The OpenAPI spec gains/loses an operation without the proof following
|
|
17
|
-
* (spec <-> proof, both directions).
|
|
18
|
-
* 2. A quoted test snippet no longer points at a real it()/test() block in
|
|
19
|
-
* its test file — the evidence the UI shows must be re-readable from the
|
|
20
|
-
* source it cites.
|
|
21
|
-
* 3. The spec or a test file changed without regenerating
|
|
22
|
-
* proof.generated.json.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
function loadCheckedInProof(): CoverageReport {
|
|
26
|
-
return JSON.parse(fs.readFileSync(GENERATED_PROOF_PATH, "utf8"));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// The drift guards compare the artifact against the audited target repo
|
|
30
|
-
// (SPECPROOF_REPO, or the current directory). Without an OpenAPI spec to audit
|
|
31
|
-
// there is nothing to compare against, so the suite self-skips.
|
|
32
|
-
const specPath = resolveSpecPath();
|
|
33
|
-
if (!specPath) {
|
|
34
|
-
console.warn(
|
|
35
|
-
`proof-contract: skipping — no OpenAPI spec found under ${TARGET_REPO_ROOT} (set SPECPROOF_REPO to the repo to audit, or SPECPROOF_SPEC to the spec file)`,
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
describe.skipIf(!specPath)("SpecProof contract", () => {
|
|
40
|
-
it("audits every operation in the OpenAPI spec, and nothing else", () => {
|
|
41
|
-
const spec = JSON.parse(fs.readFileSync(specPath!, "utf8")) as {
|
|
42
|
-
paths: Record<string, Record<string, unknown>>;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const documented = Object.entries(spec.paths)
|
|
46
|
-
.flatMap(([opPath, methods]) =>
|
|
47
|
-
Object.keys(methods).map((method) => `${method} ${opPath}`),
|
|
48
|
-
)
|
|
49
|
-
.sort();
|
|
50
|
-
|
|
51
|
-
const audited = loadCheckedInProof()
|
|
52
|
-
.tags.flatMap((tag) => tag.operations)
|
|
53
|
-
.map((op) => `${op.method} ${op.specPath}`)
|
|
54
|
-
.sort();
|
|
55
|
-
|
|
56
|
-
expect(documented.length).toBeGreaterThan(0);
|
|
57
|
-
expect(audited).toEqual(documented);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("every quoted snippet points at a real it()/test() block in its test file", () => {
|
|
61
|
-
const sources = new Map<string, string[]>();
|
|
62
|
-
const readLines = (testFile: string) => {
|
|
63
|
-
const cached = sources.get(testFile);
|
|
64
|
-
if (cached) return cached;
|
|
65
|
-
const lines = fs
|
|
66
|
-
.readFileSync(path.join(TARGET_REPO_ROOT, testFile), "utf8")
|
|
67
|
-
.split("\n");
|
|
68
|
-
sources.set(testFile, lines);
|
|
69
|
-
return lines;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
for (const tag of loadCheckedInProof().tags) {
|
|
73
|
-
for (const op of tag.operations) {
|
|
74
|
-
if (!op.testFile) continue;
|
|
75
|
-
for (const status of op.statuses) {
|
|
76
|
-
for (const snippet of status.snippets) {
|
|
77
|
-
const line = readLines(op.testFile)[snippet.startLine - 1] ?? "";
|
|
78
|
-
expect(
|
|
79
|
-
/(?:it|test)\(/.test(line),
|
|
80
|
-
`${op.method} ${op.specPath} ${status.code}: snippet "${snippet.title}" cites ${op.testFile}:${snippet.startLine}, but that line is not an it()/test() block`,
|
|
81
|
-
).toBe(true);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("proof.generated.json is up to date with the spec and tests", () => {
|
|
89
|
-
const checkedIn = loadCheckedInProof();
|
|
90
|
-
// If this fails, the OpenAPI spec or a test file changed without
|
|
91
|
-
// regenerating the proof: run `bun run generate:proof` and commit the
|
|
92
|
-
// result.
|
|
93
|
-
expect(checkedIn).toEqual(buildProof());
|
|
94
|
-
});
|
|
95
|
-
});
|
package/app/proof.generated.json
DELETED
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"tags": [
|
|
3
|
-
{
|
|
4
|
-
"tag": "Auth",
|
|
5
|
-
"description": "Session and token management",
|
|
6
|
-
"operations": [
|
|
7
|
-
{
|
|
8
|
-
"method": "post",
|
|
9
|
-
"specPath": "/auth/login",
|
|
10
|
-
"summary": "Exchange credentials for a session token",
|
|
11
|
-
"testFile": "example/tests/auth.test.ts",
|
|
12
|
-
"testCount": 2,
|
|
13
|
-
"statuses": [
|
|
14
|
-
{
|
|
15
|
-
"code": "200",
|
|
16
|
-
"description": "Session token issued",
|
|
17
|
-
"documented": true,
|
|
18
|
-
"assertions": 1,
|
|
19
|
-
"snippets": [
|
|
20
|
-
{
|
|
21
|
-
"title": "returns a session token for valid credentials",
|
|
22
|
-
"source": "it(\"returns a session token for valid credentials\", async () => {\n const res = await api.post(\"/auth/login\", {\n email: \"ada@example.com\",\n password: \"correct horse battery staple\",\n });\n\n expect(res.status).toBe(200);\n expect(res.body.token).toMatch(/^tk_/);\n});",
|
|
23
|
-
"startLine": 6
|
|
24
|
-
}
|
|
25
|
-
]
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
"code": "401",
|
|
29
|
-
"description": "Invalid credentials",
|
|
30
|
-
"documented": true,
|
|
31
|
-
"assertions": 1,
|
|
32
|
-
"snippets": [
|
|
33
|
-
{
|
|
34
|
-
"title": "rejects invalid credentials",
|
|
35
|
-
"source": "it(\"rejects invalid credentials\", async () => {\n const res = await api.post(\"/auth/login\", {\n email: \"ada@example.com\",\n password: \"not-the-password\",\n });\n\n expect(res.status).toBe(401);\n expect(res.body.token).toBeUndefined();\n});",
|
|
36
|
-
"startLine": 16
|
|
37
|
-
}
|
|
38
|
-
]
|
|
39
|
-
}
|
|
40
|
-
],
|
|
41
|
-
"coveredCount": 2,
|
|
42
|
-
"gapCount": 0
|
|
43
|
-
}
|
|
44
|
-
],
|
|
45
|
-
"coveredCount": 2,
|
|
46
|
-
"totalCount": 2
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"tag": "Tasks",
|
|
50
|
-
"description": "Create and manage tasks",
|
|
51
|
-
"operations": [
|
|
52
|
-
{
|
|
53
|
-
"method": "get",
|
|
54
|
-
"specPath": "/tasks",
|
|
55
|
-
"summary": "List tasks for the authenticated user",
|
|
56
|
-
"testFile": "example/tests/tasks.test.ts",
|
|
57
|
-
"testCount": 2,
|
|
58
|
-
"statuses": [
|
|
59
|
-
{
|
|
60
|
-
"code": "200",
|
|
61
|
-
"description": "Task list",
|
|
62
|
-
"documented": true,
|
|
63
|
-
"assertions": 1,
|
|
64
|
-
"snippets": [
|
|
65
|
-
{
|
|
66
|
-
"title": "lists tasks for the authenticated user",
|
|
67
|
-
"source": "it(\"lists tasks for the authenticated user\", async () => {\n const res = await api.get(\"/tasks\");\n\n expect(res.status).toBe(200);\n expect(Array.isArray(res.body.tasks)).toBe(true);\n});",
|
|
68
|
-
"startLine": 14
|
|
69
|
-
}
|
|
70
|
-
]
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
"code": "401",
|
|
74
|
-
"description": "Missing or invalid bearer token",
|
|
75
|
-
"documented": true,
|
|
76
|
-
"assertions": 1,
|
|
77
|
-
"snippets": [
|
|
78
|
-
{
|
|
79
|
-
"title": "rejects requests without a bearer token",
|
|
80
|
-
"source": "it(\"rejects requests without a bearer token\", async () => {\n const res = await api.get(\"/tasks\", { auth: false });\n\n expect(res.status).toBe(401);\n});",
|
|
81
|
-
"startLine": 21
|
|
82
|
-
}
|
|
83
|
-
]
|
|
84
|
-
}
|
|
85
|
-
],
|
|
86
|
-
"coveredCount": 2,
|
|
87
|
-
"gapCount": 0
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
"method": "post",
|
|
91
|
-
"specPath": "/tasks",
|
|
92
|
-
"summary": "Create a task",
|
|
93
|
-
"testFile": "example/tests/tasks.test.ts",
|
|
94
|
-
"testCount": 3,
|
|
95
|
-
"statuses": [
|
|
96
|
-
{
|
|
97
|
-
"code": "201",
|
|
98
|
-
"description": "Task created",
|
|
99
|
-
"documented": true,
|
|
100
|
-
"assertions": 1,
|
|
101
|
-
"snippets": [
|
|
102
|
-
{
|
|
103
|
-
"title": "creates a task and returns it",
|
|
104
|
-
"source": "it(\"creates a task and returns it\", async () => {\n const res = await api.post(\"/tasks\", {\n title: \"Ship the Q3 roadmap\",\n projectId: \"proj_1\",\n });\n\n expect(res.status).toBe(201);\n expect(res.body.id).toBeDefined();\n expect(res.body.title).toBe(\"Ship the Q3 roadmap\");\n});",
|
|
105
|
-
"startLine": 29
|
|
106
|
-
}
|
|
107
|
-
]
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
"code": "400",
|
|
111
|
-
"description": "Malformed request body",
|
|
112
|
-
"documented": true,
|
|
113
|
-
"assertions": 1,
|
|
114
|
-
"snippets": [
|
|
115
|
-
{
|
|
116
|
-
"title": "rejects a task without a title",
|
|
117
|
-
"source": "it(\"rejects a task without a title\", async () => {\n const res = await api.post(\"/tasks\", { projectId: \"proj_1\" });\n\n expect(res.status).toBe(400);\n expect(res.body.error).toBe(\"title is required\");\n});",
|
|
118
|
-
"startLine": 40
|
|
119
|
-
}
|
|
120
|
-
]
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
"code": "401",
|
|
124
|
-
"description": "Missing or invalid bearer token",
|
|
125
|
-
"documented": true,
|
|
126
|
-
"assertions": 0,
|
|
127
|
-
"snippets": []
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
"code": "422",
|
|
131
|
-
"description": "",
|
|
132
|
-
"documented": false,
|
|
133
|
-
"assertions": 1,
|
|
134
|
-
"snippets": [
|
|
135
|
-
{
|
|
136
|
-
"title": "rejects a due date in the past",
|
|
137
|
-
"source": "it(\"rejects a due date in the past\", async () => {\n const res = await api.post(\"/tasks\", {\n title: \"Time travel\",\n dueDate: \"1999-12-31\",\n });\n\n expect(res.status).toBe(422);\n});",
|
|
138
|
-
"startLine": 47
|
|
139
|
-
}
|
|
140
|
-
]
|
|
141
|
-
}
|
|
142
|
-
],
|
|
143
|
-
"coveredCount": 2,
|
|
144
|
-
"gapCount": 1
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
"method": "get",
|
|
148
|
-
"specPath": "/tasks/{taskId}",
|
|
149
|
-
"summary": "Fetch a single task",
|
|
150
|
-
"testFile": "example/tests/tasks.test.ts",
|
|
151
|
-
"testCount": 2,
|
|
152
|
-
"statuses": [
|
|
153
|
-
{
|
|
154
|
-
"code": "200",
|
|
155
|
-
"description": "The task",
|
|
156
|
-
"documented": true,
|
|
157
|
-
"assertions": 1,
|
|
158
|
-
"snippets": [
|
|
159
|
-
{
|
|
160
|
-
"title": "returns a task by id",
|
|
161
|
-
"source": "it(\"returns a task by id\", async () => {\n const created = await api.post(\"/tasks\", { title: \"Write release notes\" });\n const res = await api.get(`/tasks/${created.body.id}`);\n\n expect(res.status).toBe(200);\n expect(res.body.id).toBe(created.body.id);\n});",
|
|
162
|
-
"startLine": 58
|
|
163
|
-
}
|
|
164
|
-
]
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
"code": "404",
|
|
168
|
-
"description": "No task with this id",
|
|
169
|
-
"documented": true,
|
|
170
|
-
"assertions": 1,
|
|
171
|
-
"snippets": [
|
|
172
|
-
{
|
|
173
|
-
"title": "returns 404 for an unknown task id",
|
|
174
|
-
"source": "it(\"returns 404 for an unknown task id\", async () => {\n const res = await api.get(\"/tasks/task_does_not_exist\");\n\n expect(res.status).toBe(404);\n});",
|
|
175
|
-
"startLine": 66
|
|
176
|
-
}
|
|
177
|
-
]
|
|
178
|
-
}
|
|
179
|
-
],
|
|
180
|
-
"coveredCount": 2,
|
|
181
|
-
"gapCount": 0
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
"method": "patch",
|
|
185
|
-
"specPath": "/tasks/{taskId}",
|
|
186
|
-
"summary": "Update a task's fields",
|
|
187
|
-
"testFile": "example/tests/tasks.test.ts",
|
|
188
|
-
"testCount": 1,
|
|
189
|
-
"statuses": [
|
|
190
|
-
{
|
|
191
|
-
"code": "200",
|
|
192
|
-
"description": "Updated task",
|
|
193
|
-
"documented": true,
|
|
194
|
-
"assertions": 1,
|
|
195
|
-
"snippets": [
|
|
196
|
-
{
|
|
197
|
-
"title": "updates the fields of a task",
|
|
198
|
-
"source": "it(\"updates the fields of a task\", async () => {\n const created = await api.post(\"/tasks\", { title: \"Refine the backlog\" });\n const res = await api.patch(`/tasks/${created.body.id}`, {\n title: \"Refine and prioritize the backlog\",\n });\n\n expect(res.status).toBe(200);\n expect(res.body.title).toBe(\"Refine and prioritize the backlog\");\n});",
|
|
199
|
-
"startLine": 74
|
|
200
|
-
}
|
|
201
|
-
]
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
"code": "404",
|
|
205
|
-
"description": "No task with this id",
|
|
206
|
-
"documented": true,
|
|
207
|
-
"assertions": 0,
|
|
208
|
-
"snippets": []
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
"code": "409",
|
|
212
|
-
"description": "Task was modified concurrently",
|
|
213
|
-
"documented": true,
|
|
214
|
-
"assertions": 0,
|
|
215
|
-
"snippets": []
|
|
216
|
-
}
|
|
217
|
-
],
|
|
218
|
-
"coveredCount": 1,
|
|
219
|
-
"gapCount": 2
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
"method": "delete",
|
|
223
|
-
"specPath": "/tasks/{taskId}",
|
|
224
|
-
"summary": "Delete a task",
|
|
225
|
-
"testFile": null,
|
|
226
|
-
"testCount": 0,
|
|
227
|
-
"statuses": [
|
|
228
|
-
{
|
|
229
|
-
"code": "204",
|
|
230
|
-
"description": "Task deleted",
|
|
231
|
-
"documented": true,
|
|
232
|
-
"assertions": 0,
|
|
233
|
-
"snippets": []
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
"code": "404",
|
|
237
|
-
"description": "No task with this id",
|
|
238
|
-
"documented": true,
|
|
239
|
-
"assertions": 0,
|
|
240
|
-
"snippets": []
|
|
241
|
-
}
|
|
242
|
-
],
|
|
243
|
-
"coveredCount": 0,
|
|
244
|
-
"gapCount": 2
|
|
245
|
-
}
|
|
246
|
-
],
|
|
247
|
-
"coveredCount": 7,
|
|
248
|
-
"totalCount": 12
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
"tag": "Projects",
|
|
252
|
-
"description": "Group tasks into projects",
|
|
253
|
-
"operations": [
|
|
254
|
-
{
|
|
255
|
-
"method": "get",
|
|
256
|
-
"specPath": "/projects",
|
|
257
|
-
"summary": "List projects",
|
|
258
|
-
"testFile": null,
|
|
259
|
-
"testCount": 0,
|
|
260
|
-
"statuses": [
|
|
261
|
-
{
|
|
262
|
-
"code": "200",
|
|
263
|
-
"description": "Project list",
|
|
264
|
-
"documented": true,
|
|
265
|
-
"assertions": 0,
|
|
266
|
-
"snippets": []
|
|
267
|
-
},
|
|
268
|
-
{
|
|
269
|
-
"code": "401",
|
|
270
|
-
"description": "Missing or invalid bearer token",
|
|
271
|
-
"documented": true,
|
|
272
|
-
"assertions": 0,
|
|
273
|
-
"snippets": []
|
|
274
|
-
}
|
|
275
|
-
],
|
|
276
|
-
"coveredCount": 0,
|
|
277
|
-
"gapCount": 2
|
|
278
|
-
}
|
|
279
|
-
],
|
|
280
|
-
"coveredCount": 0,
|
|
281
|
-
"totalCount": 2
|
|
282
|
-
}
|
|
283
|
-
],
|
|
284
|
-
"operationCount": 7,
|
|
285
|
-
"coveredCount": 9,
|
|
286
|
-
"totalCount": 16,
|
|
287
|
-
"untestedOperations": 2
|
|
288
|
-
}
|