From 07a7d4918c08e1ae6b29c15901ccb867b4ad2f74 Mon Sep 17 00:00:00 2001 From: anti Date: Sat, 9 May 2026 04:30:51 -0400 Subject: [PATCH] test(decnet_web): MSW-based test foundation for UI refactor Phase 0 of the decnet_web refactor: stand up an MSW server, fixtures, and a router-aware render helper so the upcoming god-component splits (AttackerDetail first) can land with same-commit test coverage. - msw devDep + setupServer wired into src/test/setup.ts - src/test/server.ts re-exports server, http, HttpResponse, apiUrl() - src/test/fixtures/{attacker,decky,canary,topology}.ts factories - src/test/renderWithRouter.tsx wraps MemoryRouter + ToastProvider - baseline coverage thresholds (0%) in vite.config.ts; raise per PR - coverage/ added to decnet_web/.gitignore Existing Orchestrator/AttackerDetail/ThemeLab tests stay on vi.mock and continue to pass; new tests use MSW. --- decnet_web/.gitignore | 1 + decnet_web/package-lock.json | 510 ++++++++++++++++++++++- decnet_web/package.json | 1 + decnet_web/src/test/fixtures/attacker.ts | 58 +++ decnet_web/src/test/fixtures/canary.ts | 63 +++ decnet_web/src/test/fixtures/decky.ts | 31 ++ decnet_web/src/test/fixtures/index.ts | 9 + decnet_web/src/test/fixtures/topology.ts | 18 + decnet_web/src/test/renderWithRouter.tsx | 34 ++ decnet_web/src/test/server.ts | 13 + decnet_web/src/test/setup.ts | 8 +- decnet_web/vite.config.ts | 7 + 12 files changed, 751 insertions(+), 2 deletions(-) create mode 100644 decnet_web/src/test/fixtures/attacker.ts create mode 100644 decnet_web/src/test/fixtures/canary.ts create mode 100644 decnet_web/src/test/fixtures/decky.ts create mode 100644 decnet_web/src/test/fixtures/index.ts create mode 100644 decnet_web/src/test/fixtures/topology.ts create mode 100644 decnet_web/src/test/renderWithRouter.tsx create mode 100644 decnet_web/src/test/server.ts diff --git a/decnet_web/.gitignore b/decnet_web/.gitignore index a0f88c95..53b1260e 100644 --- a/decnet_web/.gitignore +++ b/decnet_web/.gitignore @@ -10,6 +10,7 @@ lerna-debug.log* node_modules dist dist-ssr +coverage *.local # Editor directories and files diff --git a/decnet_web/package-lock.json b/decnet_web/package-lock.json index 01f357c4..50ea381f 100644 --- a/decnet_web/package-lock.json +++ b/decnet_web/package-lock.json @@ -31,6 +31,7 @@ "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", "jsdom": "^29.1.1", + "msw": "^2.14.5", "typescript": "~6.0.2", "typescript-eslint": "^8.58.0", "vite": "^8.0.4", @@ -768,6 +769,93 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@inquirer/ansi": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", + "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.12.tgz", + "integrity": "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.9.tgz", + "integrity": "sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz", + "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz", + "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -818,6 +906,31 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mswjs/interceptors": { + "version": "0.41.8", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.8.tgz", + "integrity": "sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors/node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", @@ -837,6 +950,31 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz", + "integrity": "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@oxc-project/types": { "version": "0.123.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.123.0.tgz", @@ -1423,6 +1561,23 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/set-cookie-parser": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz", + "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -1940,7 +2095,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -2181,6 +2335,31 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2522,6 +2701,13 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/entities": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", @@ -2851,6 +3037,33 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2990,6 +3203,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3065,6 +3288,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graphql": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.0.tgz", + "integrity": "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3114,6 +3347,24 @@ "node": ">= 0.4" } }, + "node_modules/headers-polyfill": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-5.0.1.tgz", + "integrity": "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/set-cookie-parser": "^2.4.10", + "set-cookie-parser": "^3.0.1" + } + }, + "node_modules/headers-polyfill/node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "dev": true, + "license": "MIT" + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -3227,6 +3478,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3240,6 +3501,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3867,6 +4135,61 @@ "dev": true, "license": "MIT" }, + "node_modules/msw": { + "version": "2.14.5", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.14.5.tgz", + "integrity": "sha512-X6G05oX4x0e+CNI55KMdhMmwHCBKf2iwazGr+iwsdoJ94JA1ED7wSXb6V+lLPdqFkmIlPiGYvayqnaNcOzobDA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^6.0.11", + "@mswjs/interceptors": "^0.41.3", + "@open-draft/deferred-promise": "^3.0.0", + "@types/statuses": "^2.0.6", + "cookie": "^1.1.1", + "graphql": "^16.13.2", + "headers-polyfill": "^5.0.1", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.11.11", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.1", + "type-fest": "^5.5.0", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -3929,6 +4252,13 @@ "node": ">= 0.8.0" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4007,6 +4337,13 @@ "node": ">=8" } }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -4278,6 +4615,16 @@ "redux": "^5.0.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -4304,6 +4651,13 @@ "node": ">=4" } }, + "node_modules/rettime": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.11.tgz", + "integrity": "sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/rolldown": { "version": "1.0.0-rc.13", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.13.tgz", @@ -4431,6 +4785,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/solid-js": { "version": "1.9.12", "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.12.tgz", @@ -4476,6 +4843,16 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", @@ -4483,6 +4860,41 @@ "dev": true, "license": "MIT" }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -4529,6 +4941,19 @@ "dev": true, "license": "MIT" }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -4659,6 +5084,22 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", + "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", @@ -4714,6 +5155,16 @@ "dev": true, "license": "MIT" }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -5045,6 +5496,24 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", @@ -5062,6 +5531,16 @@ "dev": true, "license": "MIT" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -5069,6 +5548,35 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/decnet_web/package.json b/decnet_web/package.json index 0cf415d2..53bbf48e 100644 --- a/decnet_web/package.json +++ b/decnet_web/package.json @@ -36,6 +36,7 @@ "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", "jsdom": "^29.1.1", + "msw": "^2.14.5", "typescript": "~6.0.2", "typescript-eslint": "^8.58.0", "vite": "^8.0.4", diff --git a/decnet_web/src/test/fixtures/attacker.ts b/decnet_web/src/test/fixtures/attacker.ts new file mode 100644 index 00000000..98e34cd4 --- /dev/null +++ b/decnet_web/src/test/fixtures/attacker.ts @@ -0,0 +1,58 @@ +export interface AttackerFixture { + uuid: string; + ip: string; + identity_id: string | null; + first_seen: string; + last_seen: string; + event_count: number; + service_count: number; + decky_count: number; + services: string[]; + deckies: string[]; + traversal_path: string | null; + is_traversal: boolean; + bounty_count: number; + credential_count: number; + fingerprints: unknown[]; + commands: { service: string; decky: string; command: string; timestamp: string }[]; + country_code: string | null; + country_source: string | null; + asn: number | null; + as_name: string | null; + asn_source: string | null; + ptr_record: string | null; + updated_at: string; + behavior: null; + service_activity: { interacted: string[]; scanned: string[] }; + observations: never[]; +} + +export const makeAttacker = (overrides: Partial = {}): AttackerFixture => ({ + uuid: '11111111-1111-1111-1111-111111111111', + ip: '198.51.100.10', + identity_id: null, + first_seen: '2026-05-01T10:00:00Z', + last_seen: '2026-05-09T11:00:00Z', + event_count: 12, + service_count: 2, + decky_count: 1, + services: ['ssh', 'http'], + deckies: ['decoy-01'], + traversal_path: null, + is_traversal: false, + bounty_count: 0, + credential_count: 0, + fingerprints: [], + commands: [], + country_code: 'US', + country_source: 'maxmind', + asn: 64500, + as_name: 'EXAMPLE-AS', + asn_source: 'maxmind', + ptr_record: null, + updated_at: '2026-05-09T11:00:00Z', + behavior: null, + service_activity: { interacted: ['ssh'], scanned: ['http'] }, + observations: [], + ...overrides, +}); diff --git a/decnet_web/src/test/fixtures/canary.ts b/decnet_web/src/test/fixtures/canary.ts new file mode 100644 index 00000000..c33f1ecb --- /dev/null +++ b/decnet_web/src/test/fixtures/canary.ts @@ -0,0 +1,63 @@ +export interface CanaryTokenFixture { + uuid: string; + kind: 'http' | 'dns' | 'aws_passive'; + decky_name: string; + topology_id: string | null; + blob_uuid: string | null; + instrumenter: string | null; + generator: string | null; + placement_path: string; + callback_token: string; + placed_at: string; + last_triggered_at: string | null; + trigger_count: number; + created_by: string; + state: 'planted' | 'revoked' | 'failed'; + last_error: string | null; +} + +export interface CanaryBlobFixture { + uuid: string; + sha256: string; + filename: string; + content_type: string; + size_bytes: number; + uploaded_by: string; + uploaded_at: string; + token_count: number; +} + +export const makeCanaryToken = ( + overrides: Partial = {}, +): CanaryTokenFixture => ({ + uuid: '22222222-2222-2222-2222-222222222222', + kind: 'http', + decky_name: 'decoy-01', + topology_id: null, + blob_uuid: null, + instrumenter: 'git_config', + generator: 'git_config', + placement_path: '/etc/.git/config', + callback_token: 'abc123token', + placed_at: '2026-05-01T10:00:00Z', + last_triggered_at: null, + trigger_count: 0, + created_by: 'admin', + state: 'planted', + last_error: null, + ...overrides, +}); + +export const makeCanaryBlob = ( + overrides: Partial = {}, +): CanaryBlobFixture => ({ + uuid: '33333333-3333-3333-3333-333333333333', + sha256: 'a'.repeat(64), + filename: 'creds.json', + content_type: 'application/json', + size_bytes: 1024, + uploaded_by: 'admin', + uploaded_at: '2026-05-01T10:00:00Z', + token_count: 0, + ...overrides, +}); diff --git a/decnet_web/src/test/fixtures/decky.ts b/decnet_web/src/test/fixtures/decky.ts new file mode 100644 index 00000000..6fbbc1cf --- /dev/null +++ b/decnet_web/src/test/fixtures/decky.ts @@ -0,0 +1,31 @@ +export interface DeckyFixture { + name: string; + ip: string; + services: string[]; + distro: string; + hostname: string; + archetype: string | null; + service_config: Record>; + mutate_interval: number | null; + last_mutated: number; + swarm?: { + host_uuid: string; + host_name: string; + state: string; + last_error: string | null; + last_seen: string | null; + }; +} + +export const makeDecky = (overrides: Partial = {}): DeckyFixture => ({ + name: 'decoy-01', + ip: '10.10.10.10', + services: ['ssh', 'http'], + distro: 'debian-12', + hostname: 'fileserver-01', + archetype: 'workstation', + service_config: {}, + mutate_interval: null, + last_mutated: 0, + ...overrides, +}); diff --git a/decnet_web/src/test/fixtures/index.ts b/decnet_web/src/test/fixtures/index.ts new file mode 100644 index 00000000..742a301a --- /dev/null +++ b/decnet_web/src/test/fixtures/index.ts @@ -0,0 +1,9 @@ +export { makeAttacker, type AttackerFixture } from './attacker'; +export { makeDecky, type DeckyFixture } from './decky'; +export { + makeCanaryToken, + makeCanaryBlob, + type CanaryTokenFixture, + type CanaryBlobFixture, +} from './canary'; +export { makeTopology, type TopologyFixture } from './topology'; diff --git a/decnet_web/src/test/fixtures/topology.ts b/decnet_web/src/test/fixtures/topology.ts new file mode 100644 index 00000000..77dba791 --- /dev/null +++ b/decnet_web/src/test/fixtures/topology.ts @@ -0,0 +1,18 @@ +export interface TopologyFixture { + id: string; + name: string; + mode: string; + target_host_uuid: string | null; + status: string; + version: number; +} + +export const makeTopology = (overrides: Partial = {}): TopologyFixture => ({ + id: '44444444-4444-4444-4444-444444444444', + name: 'corp-net-01', + mode: 'flat', + target_host_uuid: null, + status: 'active', + version: 1, + ...overrides, +}); diff --git a/decnet_web/src/test/renderWithRouter.tsx b/decnet_web/src/test/renderWithRouter.tsx new file mode 100644 index 00000000..cd059f41 --- /dev/null +++ b/decnet_web/src/test/renderWithRouter.tsx @@ -0,0 +1,34 @@ +import type { ReactElement, ReactNode } from 'react'; +import { render, type RenderOptions, type RenderResult } from '@testing-library/react'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { ToastProvider } from '../components/Toasts/ToastProvider'; + +export interface RenderWithRouterOptions extends Omit { + /** Initial URL the MemoryRouter starts at. */ + initialEntries?: string[]; + /** When set, the rendered UI is mounted at this route path so `useParams` resolves. */ + path?: string; +} + +const Wrap = ({ children }: { children: ReactNode }) => ( + {children} +); + +export const renderWithRouter = ( + ui: ReactElement, + { initialEntries = ['/'], path, ...rest }: RenderWithRouterOptions = {}, +): RenderResult => { + const tree = path ? ( + + + + ) : ( + ui + ); + return render( + + {tree} + , + rest, + ); +}; diff --git a/decnet_web/src/test/server.ts b/decnet_web/src/test/server.ts new file mode 100644 index 00000000..507c0ae2 --- /dev/null +++ b/decnet_web/src/test/server.ts @@ -0,0 +1,13 @@ +import { setupServer } from 'msw/node'; +import { http, HttpResponse } from 'msw'; + +export const API_BASE = 'http://localhost:8000/api/v1'; + +export const server = setupServer(); + +export { http, HttpResponse }; + +export const apiUrl = (path: string): string => { + const trimmed = path.startsWith('/') ? path : `/${path}`; + return `${API_BASE}${trimmed}`; +}; diff --git a/decnet_web/src/test/setup.ts b/decnet_web/src/test/setup.ts index 01d18c21..87f14e96 100644 --- a/decnet_web/src/test/setup.ts +++ b/decnet_web/src/test/setup.ts @@ -1,7 +1,13 @@ import '@testing-library/jest-dom/vitest'; -import { afterEach } from 'vitest'; +import { afterAll, afterEach, beforeAll } from 'vitest'; import { cleanup } from '@testing-library/react'; +import { server } from './server'; + +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); afterEach(() => { cleanup(); + server.resetHandlers(); }); + +afterAll(() => server.close()); diff --git a/decnet_web/vite.config.ts b/decnet_web/vite.config.ts index 6fb68fee..4a30a164 100644 --- a/decnet_web/vite.config.ts +++ b/decnet_web/vite.config.ts @@ -14,6 +14,13 @@ export default defineConfig({ reporter: ['text', 'html'], include: ['src/**/*.{ts,tsx}'], exclude: ['src/**/*.d.ts', 'src/test/**', 'src/main.tsx'], + // Baseline floors. Each refactor PR raises these; never lower. + thresholds: { + lines: 0, + functions: 0, + branches: 0, + statements: 0, + }, }, }, server: {