diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..3c0fdf77 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,36 @@ +name: Playwright Tests +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + services: + nats: + image: nats:latest + ports: + - 4222:4222 + steps: + - uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "pnpm" + - name: Install dependencies + run: pnpm install + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index beae74c1..ae46d4d4 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,5 @@ dist # Envrc file .envrc generated/sdk +playwright-report +test-results diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e295a3c..8fee78f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: # Those are not supported by biomejs yet, refer https://biomejs.dev/internals/language-support/ types_or: [html, css, markdown] - repo: https://github.com/biomejs/pre-commit - rev: "v0.1.0" # Use the sha / tag you want to point at + rev: "v0.6.1" # Use the sha / tag you want to point at hooks: - id: biome-check - additional_dependencies: ["@biomejs/biome@1.6.3"] + additional_dependencies: ["@biomejs/biome@1.9.4"] diff --git a/biome.json b/biome.json index 1afbea28..ef98b140 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/1.8.0/schema.json", + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", "organizeImports": { "enabled": true }, diff --git a/moleculer.config.ts b/moleculer.config.ts index ff768b7b..06375bb1 100644 --- a/moleculer.config.ts +++ b/moleculer.config.ts @@ -39,21 +39,23 @@ const config: BrokerOptions = { // Enable/disable logging or use custom logger. More info: https://moleculer.services/docs/0.14/logging.html // Available logger types: "Console", "File", "Pino", "Winston", "Bunyan", "debug", "Log4js", "Datadog" - logger: { - // Note: Change to Console if you want to see the logger output - type: "Pino", - options: { - pino: { - // More info: http://getpino.io/#/docs/api?id=options-object - options: { - timestamp: stdTimeFunctions.isoTime, + logger: [ + { + // Note: Change to Console if you want to see the logger output + type: "Pino", + options: { + pino: { + // More info: http://getpino.io/#/docs/api?id=options-object + options: { + timestamp: stdTimeFunctions.isoTime, + }, }, }, }, - }, - // Default log level for built-in console logger. It can be overwritten in logger options above. - // Available values: trace, debug, info, warn, error, fatal - logLevel: "info", + { + type: "Console", + }, + ], // Define transporter. // More info: https://moleculer.services/docs/0.14/networking.html diff --git a/package.json b/package.json index 1bf4bf06..c75f9f6c 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "start": "npx tsx ./server.ts", "cli": "NODE_OPTIONS='--import tsx' moleculer-connect --env --config moleculer.config.ts --transporter NATS", "ci": "biome ci .", - "test:ci": "vitest --watch", - "test": "vitest --run", - "test:coverage": "vitest --coverage", + "test:ci": "vitest --watch --exclude 'tests/**'", + "test": "vitest --run --exclude 'tests/**'", + "test:coverage": "vitest --coverage --exclude 'tests/**'", "test:ui": "vitest --ui", "build": "tsup --env.NODE_ENV production", "format": "biome format . --write", @@ -30,7 +30,11 @@ "generate:action": "hygen action new --name", "generate:swagger": "npx tsx cli.ts", "generate:sdk": "openapi-ts", - "typecheck": "npm run generate:sdk && tsc -b" + "typecheck": "npm run generate:sdk && tsc -b", + "test:e2e": "NODE_ENV=test playwright test", + "test:e2e:ui": "NODE_ENV=test playwright test --ui", + "test:e2e:debug": "NODE_ENV=test DEBUG=pw:* playwright test --debug", + "test:e2e:watch": "NODE_ENV=test playwright test --watch" }, "dependencies": { "@asteasolutions/zod-to-openapi": "7.3.0", @@ -51,6 +55,7 @@ "@biomejs/biome": "1.9.4", "@flydotio/dockerfile": "0.6.1", "@hey-api/openapi-ts": "0.61.2", + "@playwright/test": "^1.49.1", "@types/express": "5.0.0", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", @@ -67,6 +72,7 @@ "moleculer-connect": "0.2.2", "moleculer-io": "2.2.0", "moleculer-repl": "0.7.4", + "msw": "2.1.0", "npm-run-all2": "7.0.2", "pino-pretty": "13.0.0", "sort-package-json": "2.12.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..a487e187 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: "html", + use: { + baseURL: "http://localhost:4567", + trace: "on-first-retry", + }, + + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + ], + + webServer: { + command: "npm run dev", + url: "http://localhost:4567", + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 348d7467..dd704de2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: '@hey-api/openapi-ts': specifier: 0.61.2 version: 0.61.2(typescript@5.7.3) + '@playwright/test': + specifier: ^1.49.1 + version: 1.49.1 '@types/express': specifier: 5.0.0 version: 5.0.0 @@ -105,6 +108,9 @@ importers: moleculer-repl: specifier: 0.7.4 version: 0.7.4 + msw: + specifier: 2.1.0 + version: 2.1.0(typescript@5.7.3) npm-run-all2: specifier: 7.0.2 version: 7.0.2 @@ -128,7 +134,7 @@ importers: version: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0) vitest: specifier: 2.1.8 - version: 2.1.8(@types/node@22.10.5)(@vitest/ui@2.1.8) + version: 2.1.8(@types/node@22.10.5)(@vitest/ui@2.1.8)(msw@2.1.0(typescript@5.7.3)) wait-on: specifier: 8.0.2 version: 8.0.2(debug@4.4.0) @@ -227,6 +233,15 @@ packages: cpu: [x64] os: [win32] + '@bundled-es-modules/cookie@2.0.1': + resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} + + '@bundled-es-modules/js-levenshtein@2.0.1': + resolution: {integrity: sha512-DERMS3yfbAljKsQc0U2wcqGKUWpdFjwqWuoMugEJlqBnKO180/n+4SR/J8MRDt1AN48X1ovgoD9KrdVXcaa3Rg==} + + '@bundled-es-modules/statuses@1.0.1': + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -726,10 +741,32 @@ packages: '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + '@mswjs/cookies@1.1.1': + resolution: {integrity: sha512-W68qOHEjx1iD+4VjQudlx26CPIoxmIAtK4ZCexU0/UJBG6jYhcuyzKJx+Iw8uhBIGd9eba64XgWVgo20it1qwA==} + engines: {node: '>=18'} + + '@mswjs/interceptors@0.25.16': + resolution: {integrity: sha512-8QC8JyKztvoGAdPgyZy49c9vSHHAZjHagwl4RY9E8carULk8ym3iTaiawrT1YoLF/qb449h48f71XDPgkUSOUg==} + engines: {node: '>=18'} + + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.49.1': + resolution: {integrity: sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==} + engines: {node: '>=18'} + hasBin: true + '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} @@ -947,6 +984,9 @@ packages: '@types/jest@29.5.14': resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/js-levenshtein@1.1.3': + resolution: {integrity: sha512-jd+Q+sD20Qfu9e2aEXogiO3vpOC1PYJOUdyN9gvs4Qrvkg4wF43L5OhqrPeokdv8TL0/mXoYfpkcoGZMNN2pkQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -980,6 +1020,9 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/statuses@2.0.5': + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + '@types/swagger-jsdoc@6.0.4': resolution: {integrity: sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==} @@ -1051,6 +1094,10 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1078,6 +1125,10 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -1130,6 +1181,10 @@ packages: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -1222,10 +1277,17 @@ packages: change-case@3.1.0: resolution: {integrity: sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw==} + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1255,6 +1317,10 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1634,6 +1700,10 @@ packages: ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + fast-copy@3.0.2: resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} @@ -1664,6 +1734,10 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -1721,6 +1795,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1763,6 +1842,10 @@ packages: git-hooks-list@3.1.0: resolution: {integrity: sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true @@ -1822,6 +1905,9 @@ packages: header-case@1.0.1: resolution: {integrity: sha512-i0q9mkOeSuhXw6bGgiQCCBgY/jlZuV/7dZXyZ9c6LcBrqwvT8eT719E9uxE5LiZftdl+z81Ugbg/VvXV4OJOeQ==} + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + helmet@8.0.0: resolution: {integrity: sha512-VyusHLEIIO5mjQPUI1wpOAEu+wl6Q0998jzTxqUYGE45xCIcAxy3MsbEK/yyJUJ3ADeMoB6MornPH6GMWAf+Pw==} engines: {node: '>=18.0.0'} @@ -1873,6 +1959,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inquirer@8.2.6: + resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} + engines: {node: '>=12.0.0'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -1881,14 +1971,26 @@ packages: resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} engines: {node: '>= 10'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -1899,6 +2001,9 @@ packages: is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1989,6 +2094,10 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2295,6 +2404,19 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.1.0: + resolution: {integrity: sha512-Y89vzaL56fUMOLLXZmGHLRlboBQ1AXan4Rz20nJC76pRaoq4F8XQXPwyW9BW9cTY6MTrVnyHY24axSl//LJdOg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.7.x <= 5.3.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -2339,6 +2461,10 @@ packages: encoding: optional: true + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + npm-normalize-package-bin@4.0.0: resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==} engines: {node: ^18.17.0 || >=20.5.0} @@ -2401,6 +2527,13 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -2454,6 +2587,9 @@ packages: path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -2514,6 +2650,16 @@ packages: pkg-types@1.3.0: resolution: {integrity: sha512-kS7yWjVFCkIw9hqdJBoMxDdzEngmkr5FXeWZZfQ6GoYacjVnsW6l2CcYW/0ThD0vF4LPJgVYnrg4d0uuhwYQbg==} + playwright-core@1.49.1: + resolution: {integrity: sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.49.1: + resolution: {integrity: sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==} + engines: {node: '>=18'} + hasBin: true + postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -2605,6 +2751,10 @@ packages: resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + readdirp@4.1.1: resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==} engines: {node: '>= 14.18.0'} @@ -2650,6 +2800,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} @@ -2802,6 +2956,9 @@ packages: std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -2901,6 +3058,9 @@ packages: thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + timers-ext@0.1.8: resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} engines: {node: '>=0.12'} @@ -2937,6 +3097,10 @@ packages: title-case@2.1.1: resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==} + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2995,6 +3159,14 @@ packages: tweetnacl@1.0.3: resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.32.0: + resolution: {integrity: sha512-rfgpoi08xagF3JSdtJlCwMq9DGNDE0IMh3Mkpc1wUypg9vPi786AiqeBBKcqvIkq42azsBM85N490fyZjeUftw==} + engines: {node: '>=16'} + type-flag@3.0.0: resolution: {integrity: sha512-3YaYwMseXCAhBB14RXW5cRQfJQlEknS6i4C8fCfeUdS3ihG9EdccdR9kt3vP73ZdeTGmPb4bZtkDn5XMIn1DLA==} @@ -3201,6 +3373,10 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3353,6 +3529,18 @@ snapshots: '@biomejs/cli-win32-x64@1.9.4': optional: true + '@bundled-es-modules/cookie@2.0.1': + dependencies: + cookie: 0.7.2 + + '@bundled-es-modules/js-levenshtein@2.0.1': + dependencies: + js-levenshtein: 1.1.6 + + '@bundled-es-modules/statuses@1.0.1': + dependencies: + statuses: 2.0.1 + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -3653,9 +3841,33 @@ snapshots: '@jsdevtools/ono@7.1.3': {} + '@mswjs/cookies@1.1.1': {} + + '@mswjs/interceptors@0.25.16': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.49.1': + dependencies: + playwright: 1.49.1 + '@polka/url@1.0.0-next.28': {} '@rollup/plugin-commonjs@28.0.2(rollup@4.30.1)': @@ -3861,6 +4073,8 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 + '@types/js-levenshtein@1.1.3': {} + '@types/json-schema@7.0.15': {} '@types/lodash@4.17.14': {} @@ -3892,6 +4106,8 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/statuses@2.0.5': {} + '@types/swagger-jsdoc@6.0.4': {} '@types/ws@8.5.13': @@ -3911,12 +4127,13 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.8(vite@5.4.11(@types/node@22.10.5))': + '@vitest/mocker@2.1.8(msw@2.1.0(typescript@5.7.3))(vite@5.4.11(@types/node@22.10.5))': dependencies: '@vitest/spy': 2.1.8 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: + msw: 2.1.0(typescript@5.7.3) vite: 5.4.11(@types/node@22.10.5) '@vitest/pretty-format@2.1.8': @@ -3947,7 +4164,7 @@ snapshots: sirv: 3.0.0 tinyglobby: 0.2.10 tinyrainbow: 1.2.0 - vitest: 2.1.8(@types/node@22.10.5)(@vitest/ui@2.1.8) + vitest: 2.1.8(@types/node@22.10.5)(@vitest/ui@2.1.8)(msw@2.1.0(typescript@5.7.3)) '@vitest/utils@2.1.8': dependencies: @@ -3979,6 +4196,10 @@ snapshots: ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -3997,6 +4218,11 @@ snapshots: any-promise@1.3.0: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -4040,6 +4266,8 @@ snapshots: base64id@2.0.0: {} + binary-extensions@2.3.0: {} + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -4185,8 +4413,22 @@ snapshots: upper-case: 1.1.3 upper-case-first: 1.1.2 + chardet@0.7.0: {} + check-error@2.1.1: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chokidar@4.0.3: dependencies: readdirp: 4.1.1 @@ -4217,6 +4459,8 @@ snapshots: cli-spinners@2.9.2: {} + cli-width@3.0.0: {} + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -4657,6 +4901,12 @@ snapshots: dependencies: type: 2.7.3 + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + fast-copy@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -4675,6 +4925,10 @@ snapshots: fflate@0.8.2: {} + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + filelist@1.0.4: dependencies: minimatch: 5.1.6 @@ -4737,6 +4991,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -4785,6 +5042,10 @@ snapshots: git-hooks-list@3.1.0: {} + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + glob@10.4.5: dependencies: foreground-child: 3.3.0 @@ -4850,6 +5111,8 @@ snapshots: no-case: 2.3.2 upper-case: 1.1.3 + headers-polyfill@4.0.3: {} + helmet@8.0.0: {} help-me@5.0.0: {} @@ -4908,16 +5171,44 @@ snapshots: inherits@2.0.4: {} + inquirer@8.2.6: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 6.2.0 + ipaddr.js@1.9.1: {} ipaddr.js@2.2.0: {} + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-core-module@2.16.1: dependencies: hasown: 2.0.2 + is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + is-interactive@1.0.0: {} is-lower-case@1.1.3: @@ -4926,6 +5217,8 @@ snapshots: is-module@1.0.0: {} + is-node-process@1.2.0: {} + is-number@7.0.0: {} is-plain-obj@4.1.0: {} @@ -5029,6 +5322,8 @@ snapshots: joycon@3.1.1: {} + js-levenshtein@1.1.6: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -5314,6 +5609,34 @@ snapshots: ms@2.1.3: {} + msw@2.1.0(typescript@5.7.3): + dependencies: + '@bundled-es-modules/cookie': 2.0.1 + '@bundled-es-modules/js-levenshtein': 2.0.1 + '@bundled-es-modules/statuses': 1.0.1 + '@mswjs/cookies': 1.1.1 + '@mswjs/interceptors': 0.25.16 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/js-levenshtein': 1.1.3 + '@types/statuses': 2.0.5 + chalk: 4.1.2 + chokidar: 3.6.0 + graphql: 16.10.0 + headers-polyfill: 4.0.3 + inquirer: 8.2.6 + is-node-process: 1.2.0 + js-levenshtein: 1.1.6 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + strict-event-emitter: 0.5.1 + type-fest: 4.32.0 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.7.3 + + mute-stream@0.0.8: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -5348,6 +5671,8 @@ snapshots: dependencies: whatwg-url: 5.0.0 + normalize-path@3.0.0: {} + npm-normalize-package-bin@4.0.0: {} npm-run-all2@7.0.2: @@ -5420,6 +5745,10 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 + os-tmpdir@1.0.2: {} + + outvariant@1.4.3: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -5464,6 +5793,8 @@ snapshots: path-to-regexp@3.3.0: {} + path-to-regexp@6.3.0: {} + pathe@1.1.2: {} pathe@2.0.1: {} @@ -5545,6 +5876,14 @@ snapshots: mlly: 1.7.4 pathe: 1.1.2 + playwright-core@1.49.1: {} + + playwright@1.49.1: + dependencies: + playwright-core: 1.49.1 + optionalDependencies: + fsevents: 2.3.2 + postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(yaml@2.7.0): dependencies: lilconfig: 3.1.3 @@ -5633,6 +5972,10 @@ snapshots: process: 0.11.10 string_decoder: 1.3.0 + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + readdirp@4.1.1: {} real-require@0.2.0: {} @@ -5689,6 +6032,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.30.1 fsevents: 2.3.3 + run-async@2.4.1: {} + rxjs@7.8.1: dependencies: tslib: 2.8.1 @@ -5876,6 +6221,8 @@ snapshots: std-env@3.8.0: {} + strict-event-emitter@0.5.1: {} + string-argv@0.3.2: {} string-width@4.2.3: @@ -6010,6 +6357,8 @@ snapshots: dependencies: real-require: 0.2.0 + through@2.3.8: {} + timers-ext@0.1.8: dependencies: es5-ext: 0.10.64 @@ -6042,6 +6391,10 @@ snapshots: no-case: 2.3.2 upper-case: 1.1.3 + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -6100,6 +6453,10 @@ snapshots: tweetnacl@1.0.3: {} + type-fest@0.21.3: {} + + type-fest@4.32.0: {} + type-flag@3.0.0: {} type-is@1.6.18: @@ -6185,10 +6542,10 @@ snapshots: optionalDependencies: vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0) - vitest@2.1.8(@types/node@22.10.5)(@vitest/ui@2.1.8): + vitest@2.1.8(@types/node@22.10.5)(@vitest/ui@2.1.8)(msw@2.1.0(typescript@5.7.3)): dependencies: '@vitest/expect': 2.1.8 - '@vitest/mocker': 2.1.8(vite@5.4.11(@types/node@22.10.5)) + '@vitest/mocker': 2.1.8(msw@2.1.0(typescript@5.7.3))(vite@5.4.11(@types/node@22.10.5)) '@vitest/pretty-format': 2.1.8 '@vitest/runner': 2.1.8 '@vitest/snapshot': 2.1.8 @@ -6265,6 +6622,12 @@ snapshots: wordwrap@1.0.0: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 diff --git a/server.ts b/server.ts index e05a7f78..0bc327fd 100644 --- a/server.ts +++ b/server.ts @@ -53,3 +53,10 @@ broker .catch((err) => { broker.logger.error(err); }); + +// Run mock server if development or test +if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") { + import("./tests/mocks/server").then((module) => { + module.server.listen({ onUnhandledRequest: "warn" }); + }); +} diff --git a/services/weather.service.ts b/services/weather.service.ts new file mode 100644 index 00000000..cb0a3dae --- /dev/null +++ b/services/weather.service.ts @@ -0,0 +1,79 @@ +import type { Context, Service, ServiceSchema } from "moleculer"; + +export type WeatherData = { + current: { + temperature_2m: number; + relative_humidity_2m: number; + rain: number; + weather_code: number; + }; +}; + +export type GetWeatherParams = { + latitude: string; + longitude: string; +}; + +type ServiceSettings = { + weatherApiUrl: string; +}; + +type ServiceMethods = { + fetchWeatherData(params: GetWeatherParams): Promise; +}; + +type ServiceThis = Service & ServiceMethods; + +const weatherService: ServiceSchema = { + name: "weather", + + settings: { + weatherApiUrl: "https://api.open-meteo.com/v1/forecast", + }, + + dependencies: [], + + actions: { + getCurrentWeather: { + rest: "GET /", + params: { + latitude: { type: "string", min: 6, max: 25 }, + longitude: { type: "string", min: 6, max: 25 }, + }, + async handler(this: ServiceThis, ctx: Context): Promise { + return this.fetchWeatherData(ctx.params); + }, + }, + }, + + events: {}, + + methods: { + async fetchWeatherData(params: GetWeatherParams): Promise { + const queryParams = new URLSearchParams({ + latitude: params.latitude.toString(), + longitude: params.longitude.toString(), + current: "temperature_2m,relative_humidity_2m,rain,weather_code", + }); + const response = await fetch(`${this.settings.weatherApiUrl}?${queryParams}`); + if (!response.ok) { + throw new Error(`Weather API error: ${response.statusText}`); + } + return response.json(); + }, + }, + + created() { + this.logger.info(`The ${this.name} service created.`); + }, + + async started() { + this.logger.info(`The ${this.name} service started.`); + }, + + async stopped() { + this.logger.info(`The ${this.name} service stopped.`); + }, +}; + +export default weatherService; diff --git a/test/unit/services/weather.spec.ts b/test/unit/services/weather.spec.ts new file mode 100644 index 00000000..7723152b --- /dev/null +++ b/test/unit/services/weather.spec.ts @@ -0,0 +1,38 @@ +import { Errors, ServiceBroker, type ServiceSchema } from "moleculer"; +import TestService, { + type GetWeatherParams, + type WeatherData, +} from "../../../services/weather.service"; +import "../../../tests/mocks/server"; + +const { ValidationError } = Errors; + +describe("Test 'weather' service", () => { + const broker = new ServiceBroker({ logger: false }); + broker.createService(TestService as unknown as ServiceSchema); + + beforeAll(async () => broker.start()); + afterAll(async () => broker.stop()); + + describe("Test 'weather.getCurrentWeather' action", () => { + it("should return weather data for valid coordinates", async () => { + const response = await broker.call( + "weather.getCurrentWeather", + { + latitude: "46.9481", + longitude: "7.4474", + }, + ); + expect(response.current).toBeDefined(); + }); + + it("should reject with ValidationError when params are missing", async () => { + expect.assertions(1); + try { + await broker.call("weather.getCurrentWeather"); + } catch (error: unknown) { + expect(error).toBeInstanceOf(ValidationError); + } + }); + }); +}); diff --git a/tests/example.spec.ts b/tests/example.spec.ts new file mode 100644 index 00000000..5605ffd6 --- /dev/null +++ b/tests/example.spec.ts @@ -0,0 +1,139 @@ +import { type APIRequestContext, expect, test } from "@playwright/test"; + +// Test group for health check endpoint +test.describe("Health Check API", () => { + test("should return success response with CPU info", async ({ + request, + }: { request: APIRequestContext }) => { + const response = await request.get("/api/health/check"); + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + const body = await response.json(); + + // CPU checks + expect(body.cpu).toBeDefined(); + expect(typeof body.cpu.load1).toBe("number"); + expect(typeof body.cpu.load5).toBe("number"); + expect(typeof body.cpu.load15).toBe("number"); + expect(body.cpu.cores).toBeGreaterThan(0); + expect(body.cpu.utilization).toBeGreaterThanOrEqual(0); + }); + + test("should include memory information", async ({ + request, + }: { request: APIRequestContext }) => { + const response = await request.get("/api/health/check"); + const body = await response.json(); + + expect(body.mem).toBeDefined(); + expect(body.mem.free).toBeGreaterThan(0); + expect(body.mem.total).toBeGreaterThan(0); + expect(body.mem.percent).toBeGreaterThanOrEqual(0); + expect(body.mem.percent).toBeLessThanOrEqual(100); + }); + + test("should include detailed OS information", async ({ + request, + }: { request: APIRequestContext }) => { + const response = await request.get("/api/health/check"); + const body = await response.json(); + + expect(body.os).toBeDefined(); + expect(body.os.uptime).toBeGreaterThan(0); + expect(body.os.type).toBe("Darwin"); + expect(body.os.release).toBeDefined(); + expect(body.os.hostname).toBeDefined(); + expect(body.os.arch).toBe("arm64"); + expect(body.os.platform).toBe("darwin"); + + // OS User info + expect(body.os.user).toBeDefined(); + expect(body.os.user.uid).toBeGreaterThan(0); + expect(body.os.user.gid).toBeGreaterThan(0); + expect(body.os.user.username).toBeDefined(); + expect(body.os.user.homedir).toMatch(/^\/Users\//); + expect(body.os.user.shell).toBeDefined(); + }); + + test("should include detailed process information", async ({ + request, + }: { request: APIRequestContext }) => { + const response = await request.get("/api/health/check"); + const body = await response.json(); + + expect(body.process).toBeDefined(); + expect(body.process.pid).toBeGreaterThan(0); + expect(body.process.uptime).toBeGreaterThan(0); + + // Process memory + expect(body.process.memory).toBeDefined(); + expect(body.process.memory.rss).toBeGreaterThan(0); + expect(body.process.memory.heapTotal).toBeGreaterThan(0); + expect(body.process.memory.heapUsed).toBeGreaterThan(0); + expect(body.process.memory.external).toBeGreaterThanOrEqual(0); + expect(body.process.memory.arrayBuffers).toBeGreaterThanOrEqual(0); + + // Process argv + expect(body.process.argv).toBeInstanceOf(Array); + expect(body.process.argv.length).toBeGreaterThan(0); + }); + + test("should include client information", async ({ + request, + }: { request: APIRequestContext }) => { + const response = await request.get("/api/health/check"); + const body = await response.json(); + + expect(body.client).toBeDefined(); + expect(body.client.type).toBe("nodejs"); + expect(body.client.version).toBeDefined(); + expect(body.client.langVersion).toMatch(/^v\d+\.\d+\.\d+$/); + }); + + test("should include network information", async ({ + request, + }: { request: APIRequestContext }) => { + const response = await request.get("/api/health/check"); + const body = await response.json(); + + expect(body.net).toBeDefined(); + expect(body.net.ip).toBeInstanceOf(Array); + expect(body.net.ip.length).toBeGreaterThan(0); + // Check if IPs are valid + body.net.ip.forEach((ip: string) => { + expect(ip).toMatch(/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/); + }); + }); + + test("should include properly formatted time information", async ({ + request, + }: { request: APIRequestContext }) => { + const response = await request.get("/api/health/check"); + const body = await response.json(); + expect(body.time).toBeDefined(); + expect(typeof body.time.now).toBe("number"); + expect(body.time.iso).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + expect(body.time.utc).toMatch( + /^[A-Za-z]{3}, \d{2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/, + ); + + // Verify time consistency + const now = new Date(body.time.now); + const iso = new Date(body.time.iso); + const utc = new Date(body.time.utc); + + expect(Math.abs(now.getTime() - iso.getTime())).toBeLessThan(1000); + expect(Math.abs(now.getTime() - utc.getTime())).toBeLessThan(1000); + }); + + test("should respond within acceptable time limit", async ({ + request, + }: { request: APIRequestContext }) => { + const startTime = Date.now(); + await request.get("/api/health/check"); + const endTime = Date.now(); + const responseTime = endTime - startTime; + + expect(responseTime).toBeLessThan(1000); // Response should be under 1 second + }); +}); diff --git a/tests/mocks/handlers.ts b/tests/mocks/handlers.ts new file mode 100644 index 00000000..b98f74b7 --- /dev/null +++ b/tests/mocks/handlers.ts @@ -0,0 +1,27 @@ +import { http, HttpResponse } from "msw"; + +const mockWeatherData = { + latitude: 46.94, + longitude: 7.44, + current: { + temperature_2m: 20.5, + relative_humidity_2m: 65, + rain: 0, + weather_code: 1, + }, +}; + +export const handlers = [ + // Mock weather API + http.get("**/api.open-meteo.com/v1/forecast", ({ request }) => { + const url = new URL(request.url); + const latitude = url.searchParams.get("latitude"); + const longitude = url.searchParams.get("longitude"); + + if (latitude === "46.9481" && longitude === "7.4474") { + return HttpResponse.json(mockWeatherData); + } + + return new HttpResponse("Invalid coordinates", { status: 422 }); + }), +]; diff --git a/tests/mocks/server.ts b/tests/mocks/server.ts new file mode 100644 index 00000000..bb7d1cdf --- /dev/null +++ b/tests/mocks/server.ts @@ -0,0 +1,8 @@ +import { setupServer } from "msw/node"; +import { handlers } from "./handlers"; + +export const server = setupServer(...handlers); + +server.events.on("request:start", ({ request }) => { + console.log("MSW intercepted:", request.method, request.url); +}); diff --git a/tests/weather.spec.ts b/tests/weather.spec.ts new file mode 100644 index 00000000..f0b3355f --- /dev/null +++ b/tests/weather.spec.ts @@ -0,0 +1,28 @@ +import { expect, test } from "@playwright/test"; + +test.describe("Weather API", () => { + const mockWeatherData = { + latitude: 46.94, + longitude: 7.44, + current: { + temperature_2m: 20.5, + relative_humidity_2m: 65, + rain: 0, + weather_code: 1, + }, + }; + + test("should fetch weather data successfully", async ({ request }) => { + const response = await request.get("/api/weather?latitude=46.9481&longitude=7.4474"); + expect(response.ok()).toBeTruthy(); + + const data = await response.json(); + expect(data).toEqual(mockWeatherData); + }); + + test("should validate required parameters", async ({ request }) => { + const response = await request.get("/api/weather"); + expect(response.ok()).toBeFalsy(); + expect(response.status()).toBe(422); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index f7ce7d6a..d72e9de9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -90,6 +90,12 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["services/**/*.ts", "addons/**/*.ts", "examples/**/*.ts", "test/**/*.spec.ts"], + "include": [ + "services/**/*.ts", + "addons/**/*.ts", + "examples/**/*.ts", + "test/**/*.spec.ts", + "tests/weather.spec.ts" + ], "files": ["server.ts", "moleculer.config.ts", "logger.ts", "cli.ts"] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..0b7d991b --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + // Support jest globals + globals: true, + exclude: [ + "**/node_modules/**", + "**/dist/**", + "**/tests/**", + "**/.{idea,git,cache,output,temp}/**", + ], + }, +});