From 80d382337dbb06d3679ea6a893c32ff311c359d1 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 14 Jan 2025 14:14:44 +0100 Subject: [PATCH 001/189] git: Ignore *all* `node_modules` folders --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 46551a152e3..48a11c3ba54 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ /tmp # dependencies -/node_modules +node_modules/ /bower_components package-lock.json yarn.lock From e884806196d0028ac9857160c5f202fe63771519 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 14 Jan 2025 14:44:41 +0100 Subject: [PATCH 002/189] Add basic `@crates-io/msw` package --- .github/workflows/ci.yml | 2 + packages/crates-io-msw/index.js | 1 + packages/crates-io-msw/package.json | 27 + packages/crates-io-msw/vitest.config.js | 11 + packages/crates-io-msw/vitest.setup.js | 10 + pnpm-lock.yaml | 947 +++++++++++++++++++++++- pnpm-workspace.yaml | 2 + 7 files changed, 999 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/index.js create mode 100644 packages/crates-io-msw/package.json create mode 100644 packages/crates-io-msw/vitest.config.js create mode 100644 packages/crates-io-msw/vitest.setup.js create mode 100644 pnpm-workspace.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef84af32f57..e59649c7c37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -233,6 +233,8 @@ jobs: - run: pnpm install + - run: pnpm --filter "@crates-io/msw" test + - if: github.repository == 'rust-lang/crates.io' run: pnpm percy exec --parallel -- pnpm test-coverage diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js new file mode 100644 index 00000000000..0f3710c6e1c --- /dev/null +++ b/packages/crates-io-msw/index.js @@ -0,0 +1 @@ +export const handlers = []; diff --git a/packages/crates-io-msw/package.json b/packages/crates-io-msw/package.json new file mode 100644 index 00000000000..649e3100e08 --- /dev/null +++ b/packages/crates-io-msw/package.json @@ -0,0 +1,27 @@ +{ + "name": "@crates-io/msw", + "version": "0.0.0", + "type": "module", + "private": true, + "main": "index.js", + "homepage": "https://github.com/rust-lang/crates.io#readme", + "bugs": { + "url": "https://github.com/rust-lang/crates.io/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rust-lang/crates.io.git" + }, + "license": "(MIT OR Apache-2.0)", + "author": "", + "scripts": { + "test": "vitest" + }, + "dependencies": { + "msw": "^2.7.0" + }, + "devDependencies": { + "happy-dom": "16.5.3", + "vitest": "3.0.1" + } +} diff --git a/packages/crates-io-msw/vitest.config.js b/packages/crates-io-msw/vitest.config.js new file mode 100644 index 00000000000..98c648f95d5 --- /dev/null +++ b/packages/crates-io-msw/vitest.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + setupFiles: 'vitest.setup.js', + + // The default Node.js environment does not support using relative paths + // with `msw`, so we use `happy-dom` instead. + environment: 'happy-dom', + }, +}); diff --git a/packages/crates-io-msw/vitest.setup.js b/packages/crates-io-msw/vitest.setup.js new file mode 100644 index 00000000000..19644d22f8a --- /dev/null +++ b/packages/crates-io-msw/vitest.setup.js @@ -0,0 +1,10 @@ +import { setupServer } from 'msw/node'; +import { afterAll, afterEach, beforeAll } from 'vitest'; + +import { handlers } from './index.js'; + +const server = setupServer(...handlers); + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9bda5fa65b1..b49c551e471 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -302,6 +302,19 @@ importers: specifier: 5.97.1 version: 5.97.1 + packages/crates-io-msw: + dependencies: + msw: + specifier: ^2.7.0 + version: 2.7.0(@types/node@22.10.10)(typescript@5.7.3) + devDependencies: + happy-dom: + specifier: 16.5.3 + version: 16.5.3 + vitest: + specifier: 3.0.1 + version: 3.0.1(@types/node@22.10.10)(happy-dom@16.5.3)(jsdom@25.0.1)(msw@2.7.0(@types/node@22.10.10)(typescript@5.7.3))(terser@5.37.0) + packages: '@ampproject/remapping@2.3.0': @@ -891,6 +904,15 @@ packages: '@braintree/sanitize-url@7.1.1': resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==} + '@bundled-es-modules/cookie@2.0.1': + resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} + + '@bundled-es-modules/statuses@1.0.1': + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + + '@bundled-es-modules/tough-cookie@0.1.6': + resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + '@chevrotain/cst-dts-gen@11.0.3': resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} @@ -1364,6 +1386,144 @@ packages: '@embroider/core': ^3.4.20 webpack: ^5.0.0 + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.4.1': resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1527,10 +1687,26 @@ packages: '@iconify/utils@2.2.1': resolution: {integrity: sha512-0/7J7hk4PqXmxo5PDBDxmnecw5PxklZJfNjIVG9FM0mEfVrvfudS22rYWsqVk6gR3UJ/mSYS90X4R3znXnqfNA==} + '@inquirer/confirm@5.1.3': + resolution: {integrity: sha512-fuF9laMmHoOgWapF9h9hv6opA5WvmGFHsTYGCmuFxcghIhEhb3dN0CdQR4BUMqa2H506NCj8cGX4jwMsE4t6dA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + '@inquirer/core@10.1.4': + resolution: {integrity: sha512-5y4/PUJVnRb4bwWY67KLdebWOhOc7xj5IP2J80oWXa64mVag24rwQ1VAdnj7/eDY/odhguW0zQ1Mp1pj6fO/2w==} + engines: {node: '>=18'} + '@inquirer/figures@1.0.9': resolution: {integrity: sha512-BXvGj0ehzrngHTPTDqUoDT3NXL8U0RxUk2zJm2A66RhCEIWdtU1v6GuUqNAgArW4PQ9CinqIWyHdQgdwOj06zQ==} engines: {node: '>=18'} + '@inquirer/type@3.0.2': + resolution: {integrity: sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1588,6 +1764,10 @@ packages: resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==} engines: {node: '>=4'} + '@mswjs/interceptors@0.37.5': + resolution: {integrity: sha512-AAwRb5vXFcY4L+FvZ7LZusDuZ0vEe0Zm8ohn1FM6/X7A3bj4mqmkAcGRWuvC2JwSygNwHAAmMnAI73vPHeqsHA==} + engines: {node: '>=18'} + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} @@ -1607,6 +1787,15 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@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==} + '@percy/cli-app@1.30.6': resolution: {integrity: sha512-IjsWqXcjjXBPErU87Zrvui2k8nogz6trXVUpiGVkPGlFXeGjQXzB95wUECRDqvDTRRrisW1p/XVJi62dUAOX2A==} engines: {node: '>=14'} @@ -1707,6 +1896,101 @@ packages: resolution: {integrity: sha512-eGjkyHSufkHyZ66WpygWnslcRePB0U1lJg1dF3rgWqTChpregYoDyNGDzK7l9Gk+CHVgGZZS5aWp7uKKVmAAEg==} engines: {node: '>=18.12'} + '@rollup/rollup-android-arm-eabi@4.30.1': + resolution: {integrity: sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.30.1': + resolution: {integrity: sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.30.1': + resolution: {integrity: sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.30.1': + resolution: {integrity: sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.30.1': + resolution: {integrity: sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.30.1': + resolution: {integrity: sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.30.1': + resolution: {integrity: sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.30.1': + resolution: {integrity: sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.30.1': + resolution: {integrity: sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.30.1': + resolution: {integrity: sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.30.1': + resolution: {integrity: sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.30.1': + resolution: {integrity: sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.30.1': + resolution: {integrity: sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.30.1': + resolution: {integrity: sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.30.1': + resolution: {integrity: sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.30.1': + resolution: {integrity: sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.30.1': + resolution: {integrity: sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.30.1': + resolution: {integrity: sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.30.1': + resolution: {integrity: sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==} + cpu: [x64] + os: [win32] + '@scalvert/ember-setup-middleware-reporter@0.1.1': resolution: {integrity: sha512-C5DHU6YlKaISB5utGQ+jpsMB57ZtY0uZ8UkD29j855BjqG6eJ98lhA2h/BoJbyPw89RKLP1EEXroy9+5JPoyVw==} engines: {node: 12.* || >= 14} @@ -1780,6 +2064,9 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/cors@2.8.17': resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} @@ -2071,12 +2358,18 @@ packages: '@types/sizzle@2.3.9': resolution: {integrity: sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==} + '@types/statuses@2.0.5': + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + '@types/supports-color@8.1.3': resolution: {integrity: sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==} '@types/symlink-or-copy@1.2.2': resolution: {integrity: sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -2089,6 +2382,35 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@vitest/expect@3.0.1': + resolution: {integrity: sha512-oPrXe8dwvQdzUxQFWwibY97/smQ6k8iPVeSf09KEvU1yWzu40G6naHExY0lUgjnTPWMRGQOJnhMBb8lBu48feg==} + + '@vitest/mocker@3.0.1': + resolution: {integrity: sha512-5letLsVdFhReCPws/SNwyekBCyi4w2IusycV4T7eVdt2mfellS2yKDrEmnE5KPCHr0Ez5xCZVJbJws3ckuNNgQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.0.1': + resolution: {integrity: sha512-FnyGQ9eFJ/Dnqg3jCvq9O6noXtxbZhOlSvNLZsCGJxhsGiZ5LDepmsTCizRfyGJt4Q6pJmZtx7rO/qqr9R9gDA==} + + '@vitest/runner@3.0.1': + resolution: {integrity: sha512-LfVbbYOduTVx8PnYFGH98jpgubHBefIppbPQJBSlgjnRRlaX/KR6J46htECUHpf+ElJZ4xxssAfEz/Cb2iIMYA==} + + '@vitest/snapshot@3.0.1': + resolution: {integrity: sha512-ZYV+iw2lGyc4QY2xt61b7Y3NJhSAO7UWcYWMcV0UnMrkXa8hXtfZES6WAk4g7Jr3p4qJm1P0cgDcOFyY5me+Ug==} + + '@vitest/spy@3.0.1': + resolution: {integrity: sha512-HnGJB3JFflnlka4u7aD0CfqrEtX3FgNaZAar18/KIhfo0r/WADn9PhBfiqAmNw4R/xaRcLzLPFXDwEQV1vHlJA==} + + '@vitest/utils@3.0.1': + resolution: {integrity: sha512-i+Gm61rfIeSitPUsu4ZcWqucfb18ShAanRpOG6KlXfd1j6JVK5XxO2Z6lEmfjMnAQRIvvLtJ3JByzDTv347e8w==} + '@warp-drive/build-config@0.0.0-beta.7': resolution: {integrity: sha512-EHBWwNTv62OA9C24VEEeU04A2JNkMYiJjkA/cXnuQeM0/HSYyki4vtHtCjFXGG397KUpS0bkFBzzfXivHof9yA==} engines: {node: '>= 18.20.4'} @@ -2456,6 +2778,10 @@ packages: assert@1.5.1: resolution: {integrity: sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + assign-symbols@1.0.0: resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} engines: {node: '>=0.10.0'} @@ -2951,6 +3277,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + cacache@12.0.4: resolution: {integrity: sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==} @@ -3007,6 +3337,10 @@ packages: resolution: {integrity: sha512-INsuF4GyiFLk8C91FPokbKTc/rwHqV4JnfatVZ6GPhguP1qmkRWX2dp5tepYboYdPpGWisLVLI+KsXoXFPRSMg==} hasBin: true + chai@5.1.2: + resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} + engines: {node: '>=12'} + chalk@1.1.3: resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} engines: {node: '>=0.10.0'} @@ -3033,6 +3367,10 @@ packages: resolution: {integrity: sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==} engines: {pnpm: '>=8'} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} @@ -3437,6 +3775,10 @@ packages: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.0.2: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} @@ -3843,6 +4185,10 @@ packages: decorator-transforms@2.3.0: resolution: {integrity: sha512-jo8c1ss9yFPudHuYYcrJ9jpkDZIoi+lOGvt+Uyp9B+dz32i50icRMx9Bfa8hEt7TnX1FyKWKkjV+cUdT/ep2kA==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -4491,6 +4837,11 @@ packages: es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -4651,6 +5002,9 @@ packages: estree-walker@0.6.1: resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -4707,6 +5061,10 @@ packages: resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} engines: {node: '>=0.10.0'} + expect-type@1.1.0: + resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} + engines: {node: '>=12.0.0'} + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} @@ -5198,6 +5556,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql@16.10.0: + resolution: {integrity: sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + growly@1.3.0: resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} @@ -5213,6 +5575,10 @@ packages: engines: {node: '>=0.4.7'} hasBin: true + happy-dom@16.5.3: + resolution: {integrity: sha512-7zGnyROZuntn+5X84MQ535qiQ3ccm45uHl+Q7EFAcPP0NhkbrfPitqprz0GgszX91/QqsZKQ7nTYkyObCTDUjg==} + engines: {node: '>=18.0.0'} + has-ansi@2.0.0: resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} engines: {node: '>=0.10.0'} @@ -5288,6 +5654,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + heimdalljs-fs-monitor@1.1.1: resolution: {integrity: sha512-BHB8oOXLRlrIaON0MqJSEjGVPDyqt2Y6gu+w2PaEZjrCxeVtZG7etEZp7M4ZQ80HNvnr66KIQ2lot2qdeG8HgQ==} @@ -5630,6 +5999,9 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -6105,6 +6477,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.1.2: + resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -6123,6 +6498,9 @@ packages: magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} @@ -6374,6 +6752,16 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.7.0: + resolution: {integrity: sha512-BIodwZ19RWfCbYTxWTUfTXc+sg4OwjCAgxU1ZsgmggX/7S3LdUifsbUPJs61j0rWb19CZRGY5if77duhc0uXzw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + mustache@4.2.0: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true @@ -6388,6 +6776,10 @@ packages: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + nan@2.22.0: resolution: {integrity: sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==} @@ -6623,6 +7015,9 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -6826,6 +7221,10 @@ packages: pathe@2.0.2: resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==} + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + pbkdf2@3.1.2: resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} engines: {node: '>=0.12'} @@ -7202,7 +7601,6 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qs@6.13.0: @@ -7486,6 +7884,11 @@ packages: resolution: {integrity: sha512-I18GBqP0qJoJC1K1osYjreqA8VAKovxuI3I81RSk0Dmr4TgloI0tAULjZaox8OsJ+n7XRrhH6i0G2By/pj1LCA==} hasBin: true + rollup@4.30.1: + resolution: {integrity: sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} @@ -7703,6 +8106,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -7847,6 +8253,9 @@ packages: resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stagehand@1.0.1: resolution: {integrity: sha512-GqXBq2SPWv9hTXDFKS8WrKK1aISB0aKGHZzH+uD4ShAgs+Fz20ZfoerLOm8U+f62iRWLrw6nimOY/uYuTcVhvg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -7863,6 +8272,9 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + stream-browserify@2.0.2: resolution: {integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==} @@ -7875,6 +8287,9 @@ packages: stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-template@0.2.1: resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==} @@ -8107,9 +8522,24 @@ packages: tiny-lr@2.0.0: resolution: {integrity: sha512-f6nh0VMRvhGx4KCeK1lQ/jaL0Zdb5WdR+Jk8q9OSUQnaSDxAEGH1fgqLZ+cMl5EW3F2MGnCsalBO1IsnnogW1Q==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinypool@1.0.2: + resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + tldts-core@6.1.75: resolution: {integrity: sha512-AOvV5YYIAFFBfransBzSTyztkc3IMfz5Eq3YluaRiEu55nn43Fzaufx70UqEKYr8BoLCach4q8g/bg6e5+/aFw==} @@ -8240,6 +8670,10 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} + type-fest@4.32.0: + resolution: {integrity: sha512-rfgpoi08xagF3JSdtJlCwMq9DGNDE0IMh3Mkpc1wUypg9vPi786AiqeBBKcqvIkq42azsBM85N490fyZjeUftw==} + engines: {node: '>=16'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -8435,6 +8869,67 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-node@3.0.1: + resolution: {integrity: sha512-PoH9mCNsSZQXl3gdymM5IE4WR0k0WbnFd89nAyyDvltF2jVGdFcI8vpB1PBdKTcjAR7kkYiHSlIO68X/UT8Q1A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@5.4.11: + resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@3.0.1: + resolution: {integrity: sha512-SWKoSAkxtFHqt8biR3eN53dzmeWkigEpyipqfblcsoAghVvoFMpxQEj0gc7AajMi6Ra49fjcTN6v4AxklmS4aQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.0.1 + '@vitest/ui': 3.0.1 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} @@ -8573,6 +9068,10 @@ packages: whatwg-mimetype@2.3.0: resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} @@ -8616,6 +9115,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} @@ -9541,6 +10045,19 @@ snapshots: '@braintree/sanitize-url@7.1.1': {} + '@bundled-es-modules/cookie@2.0.1': + dependencies: + cookie: 0.7.2 + + '@bundled-es-modules/statuses@1.0.1': + dependencies: + statuses: 2.0.1 + + '@bundled-es-modules/tough-cookie@0.1.6': + dependencies: + '@types/tough-cookie': 4.0.5 + tough-cookie: 4.1.4 + '@chevrotain/cst-dts-gen@11.0.3': dependencies: '@chevrotain/gast': 11.0.3 @@ -10201,6 +10718,75 @@ snapshots: - canvas - utf-8-validate + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + '@eslint-community/eslint-utils@4.4.1(eslint@9.19.0)': dependencies: eslint: 9.19.0 @@ -10485,8 +11071,32 @@ snapshots: transitivePeerDependencies: - supports-color + '@inquirer/confirm@5.1.3(@types/node@22.10.10)': + dependencies: + '@inquirer/core': 10.1.4(@types/node@22.10.10) + '@inquirer/type': 3.0.2(@types/node@22.10.10) + '@types/node': 22.10.10 + + '@inquirer/core@10.1.4(@types/node@22.10.10)': + dependencies: + '@inquirer/figures': 1.0.9 + '@inquirer/type': 3.0.2(@types/node@22.10.10) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + transitivePeerDependencies: + - '@types/node' + '@inquirer/figures@1.0.9': {} + '@inquirer/type@3.0.2(@types/node@22.10.10)': + dependencies: + '@types/node': 22.10.10 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -10559,6 +11169,15 @@ snapshots: call-me-maybe: 1.0.2 glob-to-regexp: 0.3.0 + '@mswjs/interceptors@0.37.5': + 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 + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': dependencies: eslint-scope: 5.1.1 @@ -10577,6 +11196,15 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.18.0 + '@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': {} + '@percy/cli-app@1.30.6(typescript@5.7.3)': dependencies: '@percy/cli-command': 1.30.6(typescript@5.7.3) @@ -10753,6 +11381,63 @@ snapshots: '@pnpm/error': 6.0.3 find-up: 5.0.0 + '@rollup/rollup-android-arm-eabi@4.30.1': + optional: true + + '@rollup/rollup-android-arm64@4.30.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.30.1': + optional: true + + '@rollup/rollup-darwin-x64@4.30.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.30.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.30.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.30.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.30.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.30.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.30.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.30.1': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.30.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.30.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.30.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.30.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.30.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.30.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.30.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.30.1': + optional: true + '@scalvert/ember-setup-middleware-reporter@0.1.1': dependencies: '@types/fs-extra': 9.0.13 @@ -10846,6 +11531,8 @@ snapshots: dependencies: '@types/node': 22.10.10 + '@types/cookie@0.6.0': {} + '@types/cors@2.8.17': dependencies: '@types/node': 22.10.10 @@ -11283,10 +11970,14 @@ snapshots: '@types/sizzle@2.3.9': {} + '@types/statuses@2.0.5': {} + '@types/supports-color@8.1.3': {} '@types/symlink-or-copy@1.2.2': {} + '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': optional: true @@ -11301,6 +11992,47 @@ snapshots: '@types/node': 22.10.10 optional: true + '@vitest/expect@3.0.1': + dependencies: + '@vitest/spy': 3.0.1 + '@vitest/utils': 3.0.1 + chai: 5.1.2 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.0.1(msw@2.7.0(@types/node@22.10.10)(typescript@5.7.3))(vite@5.4.11(@types/node@22.10.10)(terser@5.37.0))': + dependencies: + '@vitest/spy': 3.0.1 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + msw: 2.7.0(@types/node@22.10.10)(typescript@5.7.3) + vite: 5.4.11(@types/node@22.10.10)(terser@5.37.0) + + '@vitest/pretty-format@3.0.1': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.0.1': + dependencies: + '@vitest/utils': 3.0.1 + pathe: 2.0.2 + + '@vitest/snapshot@3.0.1': + dependencies: + '@vitest/pretty-format': 3.0.1 + magic-string: 0.30.17 + pathe: 2.0.2 + + '@vitest/spy@3.0.1': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@3.0.1': + dependencies: + '@vitest/pretty-format': 3.0.1 + loupe: 3.1.2 + tinyrainbow: 2.0.0 + '@warp-drive/build-config@0.0.0-beta.7': dependencies: '@embroider/addon-shim': 1.9.0 @@ -11716,6 +12448,8 @@ snapshots: object.assign: 4.1.7 util: 0.10.4 + assertion-error@2.0.1: {} + assign-symbols@1.0.0: {} ast-types@0.13.3: {} @@ -12629,6 +13363,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + cacache@12.0.4: dependencies: bluebird: 3.7.2 @@ -12715,6 +13451,14 @@ snapshots: ansicolors: 0.2.1 redeyed: 1.0.1 + chai@5.1.2: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.2 + pathval: 2.0.0 + chalk@1.1.3: dependencies: ansi-styles: 2.2.1 @@ -12746,6 +13490,8 @@ snapshots: dependencies: '@kurkle/color': 0.3.4 + check-error@2.1.1: {} + cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 @@ -13026,6 +13772,8 @@ snapshots: cookie@0.7.1: {} + cookie@0.7.2: {} + cookie@1.0.2: {} copy-concurrently@1.0.5: @@ -13489,6 +14237,8 @@ snapshots: transitivePeerDependencies: - '@babel/core' + deep-eql@5.0.2: {} + deep-is@0.1.4: {} default-require-extensions@3.0.1: @@ -14870,6 +15620,32 @@ snapshots: es6-error@4.1.1: {} + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -15053,6 +15829,10 @@ snapshots: estree-walker@0.6.1: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.6 + esutils@2.0.3: {} etag@1.8.1: {} @@ -15146,6 +15926,8 @@ snapshots: dependencies: homedir-polyfill: 1.0.3 + expect-type@1.1.0: {} + express@4.21.2: dependencies: accepts: 1.3.8 @@ -15831,6 +16613,8 @@ snapshots: graceful-fs@4.2.11: {} + graphql@16.10.0: {} + growly@1.3.0: {} gzip-size@6.0.0: @@ -15848,6 +16632,11 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 + happy-dom@16.5.3: + dependencies: + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + has-ansi@2.0.0: dependencies: ansi-regex: 2.1.1 @@ -15929,6 +16718,8 @@ snapshots: dependencies: function-bind: 1.1.2 + headers-polyfill@4.0.3: {} + heimdalljs-fs-monitor@1.1.1: dependencies: callsites: 3.1.0 @@ -16309,6 +17100,8 @@ snapshots: is-map@2.0.3: {} + is-node-process@1.2.0: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.3 @@ -16820,6 +17613,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.1.2: {} + lower-case@2.0.2: dependencies: tslib: 2.8.1 @@ -16840,6 +17635,10 @@ snapshots: dependencies: sourcemap-codec: 1.4.8 + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + make-dir@2.1.0: dependencies: pify: 4.0.1 @@ -17130,6 +17929,31 @@ snapshots: ms@2.1.3: {} + msw@2.7.0(@types/node@22.10.10)(typescript@5.7.3): + dependencies: + '@bundled-es-modules/cookie': 2.0.1 + '@bundled-es-modules/statuses': 1.0.1 + '@bundled-es-modules/tough-cookie': 0.1.6 + '@inquirer/confirm': 5.1.3(@types/node@22.10.10) + '@mswjs/interceptors': 0.37.5 + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.5 + graphql: 16.10.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + strict-event-emitter: 0.5.1 + type-fest: 4.32.0 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.7.3 + transitivePeerDependencies: + - '@types/node' + mustache@4.2.0: {} mute-stream@0.0.7: {} @@ -17138,6 +17962,8 @@ snapshots: mute-stream@1.0.0: {} + mute-stream@2.0.0: {} + nan@2.22.0: optional: true @@ -17454,6 +18280,8 @@ snapshots: os-tmpdir@1.0.2: {} + outvariant@1.4.3: {} + own-keys@1.0.1: dependencies: get-intrinsic: 1.2.7 @@ -17621,6 +18449,8 @@ snapshots: pathe@2.0.2: {} + pathval@2.0.0: {} + pbkdf2@3.1.2: dependencies: create-hash: 1.2.0 @@ -18362,6 +19192,31 @@ snapshots: signal-exit: 3.0.7 sourcemap-codec: 1.4.8 + rollup@4.30.1: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.30.1 + '@rollup/rollup-android-arm64': 4.30.1 + '@rollup/rollup-darwin-arm64': 4.30.1 + '@rollup/rollup-darwin-x64': 4.30.1 + '@rollup/rollup-freebsd-arm64': 4.30.1 + '@rollup/rollup-freebsd-x64': 4.30.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.30.1 + '@rollup/rollup-linux-arm-musleabihf': 4.30.1 + '@rollup/rollup-linux-arm64-gnu': 4.30.1 + '@rollup/rollup-linux-arm64-musl': 4.30.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.30.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.30.1 + '@rollup/rollup-linux-riscv64-gnu': 4.30.1 + '@rollup/rollup-linux-s390x-gnu': 4.30.1 + '@rollup/rollup-linux-x64-gnu': 4.30.1 + '@rollup/rollup-linux-x64-musl': 4.30.1 + '@rollup/rollup-win32-arm64-msvc': 4.30.1 + '@rollup/rollup-win32-ia32-msvc': 4.30.1 + '@rollup/rollup-win32-x64-msvc': 4.30.1 + fsevents: 2.3.3 + roughjs@4.6.6: dependencies: hachure-fill: 0.5.2 @@ -18628,6 +19483,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -18806,6 +19663,8 @@ snapshots: stable@0.1.8: {} + stackback@0.0.2: {} + stagehand@1.0.1: dependencies: debug: 4.4.0(supports-color@8.1.1) @@ -18821,6 +19680,8 @@ snapshots: statuses@2.0.1: {} + std-env@3.8.0: {} + stream-browserify@2.0.2: dependencies: inherits: 2.0.4 @@ -18841,6 +19702,8 @@ snapshots: stream-shift@1.0.3: {} + strict-event-emitter@0.5.1: {} + string-template@0.2.1: {} string-width@2.1.1: @@ -19202,8 +20065,16 @@ snapshots: transitivePeerDependencies: - supports-color + tinybench@2.9.0: {} + tinyexec@0.3.2: {} + tinypool@1.0.2: {} + + tinyrainbow@2.0.0: {} + + tinyspy@3.0.2: {} + tldts-core@6.1.75: {} tldts@6.1.75: @@ -19332,6 +20203,8 @@ snapshots: type-fest@0.8.1: {} + type-fest@4.32.0: {} + type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -19525,6 +20398,71 @@ snapshots: vary@1.1.2: {} + vite-node@3.0.1(@types/node@22.10.10)(terser@5.37.0): + dependencies: + cac: 6.7.14 + debug: 4.4.0(supports-color@8.1.1) + es-module-lexer: 1.6.0 + pathe: 2.0.2 + vite: 5.4.11(@types/node@22.10.10)(terser@5.37.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.11(@types/node@22.10.10)(terser@5.37.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.1 + rollup: 4.30.1 + optionalDependencies: + '@types/node': 22.10.10 + fsevents: 2.3.3 + terser: 5.37.0 + + vitest@3.0.1(@types/node@22.10.10)(happy-dom@16.5.3)(jsdom@25.0.1)(msw@2.7.0(@types/node@22.10.10)(typescript@5.7.3))(terser@5.37.0): + dependencies: + '@vitest/expect': 3.0.1 + '@vitest/mocker': 3.0.1(msw@2.7.0(@types/node@22.10.10)(typescript@5.7.3))(vite@5.4.11(@types/node@22.10.10)(terser@5.37.0)) + '@vitest/pretty-format': 3.0.1 + '@vitest/runner': 3.0.1 + '@vitest/snapshot': 3.0.1 + '@vitest/spy': 3.0.1 + '@vitest/utils': 3.0.1 + chai: 5.1.2 + debug: 4.4.0(supports-color@8.1.1) + expect-type: 1.1.0 + magic-string: 0.30.17 + pathe: 2.0.2 + std-env: 3.8.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 2.0.0 + vite: 5.4.11(@types/node@22.10.10)(terser@5.37.0) + vite-node: 3.0.1(@types/node@22.10.10)(terser@5.37.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.10.10 + happy-dom: 16.5.3 + jsdom: 25.0.1(supports-color@8.1.1) + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vm-browserify@1.1.2: {} vscode-jsonrpc@8.2.0: {} @@ -19716,6 +20654,8 @@ snapshots: whatwg-mimetype@2.3.0: {} + whatwg-mimetype@3.0.0: {} + whatwg-mimetype@4.0.0: {} whatwg-url@14.1.0: @@ -19784,6 +20724,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wide-align@1.1.5: dependencies: string-width: 4.2.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000000..18ec407efca --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'packages/*' From 5f94902d36f5cf7742eb67e3d78cdb1eefa3655a Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 14 Jan 2025 14:44:58 +0100 Subject: [PATCH 003/189] msw: Add `@mswjs/data` mock database --- packages/crates-io-msw/index.js | 4 ++ packages/crates-io-msw/package.json | 1 + packages/crates-io-msw/vitest.setup.js | 4 +- pnpm-lock.yaml | 75 ++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 1 deletion(-) diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 0f3710c6e1c..1802e977229 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1 +1,5 @@ +import { factory } from '@mswjs/data'; + export const handlers = []; + +export const db = factory({}); diff --git a/packages/crates-io-msw/package.json b/packages/crates-io-msw/package.json index 649e3100e08..71680203762 100644 --- a/packages/crates-io-msw/package.json +++ b/packages/crates-io-msw/package.json @@ -18,6 +18,7 @@ "test": "vitest" }, "dependencies": { + "@mswjs/data": "^0.16.2", "msw": "^2.7.0" }, "devDependencies": { diff --git a/packages/crates-io-msw/vitest.setup.js b/packages/crates-io-msw/vitest.setup.js index 19644d22f8a..a06393ce007 100644 --- a/packages/crates-io-msw/vitest.setup.js +++ b/packages/crates-io-msw/vitest.setup.js @@ -1,10 +1,12 @@ +import { drop } from '@mswjs/data'; import { setupServer } from 'msw/node'; import { afterAll, afterEach, beforeAll } from 'vitest'; -import { handlers } from './index.js'; +import { db, handlers } from './index.js'; const server = setupServer(...handlers); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); +afterEach(() => drop(db)); afterAll(() => server.close()); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b49c551e471..d70212c2c7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -304,6 +304,9 @@ importers: packages/crates-io-msw: dependencies: + '@mswjs/data': + specifier: ^0.16.2 + version: 0.16.2(@types/node@22.10.10)(typescript@5.7.3) msw: specifier: ^2.7.0 version: 2.7.0(@types/node@22.10.10)(typescript@5.7.3) @@ -1764,6 +1767,9 @@ packages: resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==} engines: {node: '>=4'} + '@mswjs/data@0.16.2': + resolution: {integrity: sha512-/C0d/PBcJyQJokUhcjO4HiZPc67hzllKlRtD1XELygl2t991/ATAAQJVcStn4YtVALsNodruzOHT0JIvgr0hnA==} + '@mswjs/interceptors@0.37.5': resolution: {integrity: sha512-AAwRb5vXFcY4L+FvZ7LZusDuZ0vEe0Zm8ohn1FM6/X7A3bj4mqmkAcGRWuvC2JwSygNwHAAmMnAI73vPHeqsHA==} engines: {node: '>=18'} @@ -2313,6 +2319,12 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/lodash@4.17.14': + resolution: {integrity: sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==} + + '@types/md5@2.3.5': + resolution: {integrity: sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} @@ -2331,6 +2343,9 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/pluralize@0.0.29': + resolution: {integrity: sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA==} + '@types/q@1.5.8': resolution: {integrity: sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==} @@ -2373,6 +2388,9 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/uuid@8.3.4': + resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -3360,6 +3378,9 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + charm@1.0.2: resolution: {integrity: sha512-wqW3VdPnlSWT4eRiYX+hcs+C6ViBPUWk1qTCd+37qw9kEm/a5n2qcyQDMBWvSYKN/ctqZzeXNQaeBjOetJJUkw==} @@ -3844,6 +3865,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + crypto-browserify@3.12.1: resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==} engines: {node: '>= 0.10'} @@ -4121,6 +4145,10 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} @@ -6562,6 +6590,9 @@ packages: md5.js@1.3.5: resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + mdn-data@1.1.4: resolution: {integrity: sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==} @@ -11169,6 +11200,28 @@ snapshots: call-me-maybe: 1.0.2 glob-to-regexp: 0.3.0 + '@mswjs/data@0.16.2(@types/node@22.10.10)(typescript@5.7.3)': + dependencies: + '@types/lodash': 4.17.14 + '@types/md5': 2.3.5 + '@types/pluralize': 0.0.29 + '@types/uuid': 8.3.4 + date-fns: 2.30.0 + debug: 4.4.0(supports-color@8.1.1) + graphql: 16.10.0 + lodash: 4.17.21 + md5: 2.3.0 + outvariant: 1.4.3 + pluralize: 8.0.0 + strict-event-emitter: 0.5.1 + uuid: 8.3.2 + optionalDependencies: + msw: 2.7.0(@types/node@22.10.10)(typescript@5.7.3) + transitivePeerDependencies: + - '@types/node' + - supports-color + - typescript + '@mswjs/interceptors@0.37.5': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -11928,6 +11981,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/lodash@4.17.14': {} + + '@types/md5@2.3.5': {} + '@types/mime@1.3.5': {} '@types/minimatch@3.0.5': {} @@ -11942,6 +11999,8 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/pluralize@0.0.29': {} + '@types/q@1.5.8': {} '@types/qs@6.9.18': {} @@ -11981,6 +12040,8 @@ snapshots: '@types/trusted-types@2.0.7': optional: true + '@types/uuid@8.3.4': {} + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.33': @@ -13482,6 +13543,8 @@ snapshots: chardet@0.7.0: {} + charenc@0.0.2: {} + charm@1.0.2: dependencies: inherits: 2.0.4 @@ -13859,6 +13922,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypt@0.0.2: {} + crypto-browserify@3.12.1: dependencies: browserify-cipher: 1.0.1 @@ -14189,6 +14254,10 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.26.7 + date-fns@3.6.0: {} date-fns@4.1.0: {} @@ -17705,6 +17774,12 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 + md5@2.3.0: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + mdn-data@1.1.4: {} mdn-data@2.0.14: {} From 62238a9420150801dbbc8e38283cb6b3d16c8b4d Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 15 Jan 2025 12:59:54 +0100 Subject: [PATCH 004/189] msw: Extract custom `factory()` fn --- packages/crates-io-msw/index.js | 2 +- packages/crates-io-msw/utils/factory.d.ts | 3 +++ packages/crates-io-msw/utils/factory.js | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/utils/factory.d.ts create mode 100644 packages/crates-io-msw/utils/factory.js diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 1802e977229..f18198673ec 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,4 +1,4 @@ -import { factory } from '@mswjs/data'; +import { factory } from './utils/factory.js'; export const handlers = []; diff --git a/packages/crates-io-msw/utils/factory.d.ts b/packages/crates-io-msw/utils/factory.d.ts new file mode 100644 index 00000000000..47c1d537ab8 --- /dev/null +++ b/packages/crates-io-msw/utils/factory.d.ts @@ -0,0 +1,3 @@ +import { FactoryAPI, ModelDictionary } from '@mswjs/data/lib/glossary'; + +export declare function factory(dictionary: Dictionary): FactoryAPI; diff --git a/packages/crates-io-msw/utils/factory.js b/packages/crates-io-msw/utils/factory.js new file mode 100644 index 00000000000..ebffab9bc38 --- /dev/null +++ b/packages/crates-io-msw/utils/factory.js @@ -0,0 +1,6 @@ +import { factory as mswFactory } from '@mswjs/data'; + +export function factory(models) { + // Create a new MSW database instance with the given models. + return mswFactory(models); +} From 7b4ca78569ad6a56aa15919ad50a01fdff881813 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 15 Jan 2025 13:20:56 +0100 Subject: [PATCH 005/189] msw: Add support for `preCreate()` fns --- packages/crates-io-msw/utils/factory.js | 33 ++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/crates-io-msw/utils/factory.js b/packages/crates-io-msw/utils/factory.js index ebffab9bc38..f6e550a9b81 100644 --- a/packages/crates-io-msw/utils/factory.js +++ b/packages/crates-io-msw/utils/factory.js @@ -1,6 +1,37 @@ import { factory as mswFactory } from '@mswjs/data'; +/** + * This function creates a new MSW database instance with the given models. + * + * This is a custom factory function that extends the default MSW factory + * by adding support for a `preCreate()` function that is executed before + * creating a new model and has access to the model attributes. + */ export function factory(models) { + // Extract `preCreate()` functions from the model definitions + // and store them in a separate Map. + let preCreateFns = new Map(); + for (let [modelName, modelDef] of Object.entries(models)) { + if (modelDef.preCreate) { + preCreateFns.set(modelName, modelDef.preCreate); + delete modelDef.preCreate; + } + } + // Create a new MSW database instance with the given models. - return mswFactory(models); + let db = mswFactory(models); + + // Override the `create()` method of each model to apply + // the `preCreate()` function before creating a new model. + for (let [key, preCreate] of preCreateFns.entries()) { + let modelApi = db[key]; + + modelApi.mswCreate = modelApi.create; + modelApi.create = function (attrs = {}) { + preCreate(attrs); + return modelApi.mswCreate(attrs); + }; + } + + return db; } From 47f28400420bfeeb82e0243ae5c7b53f8e9041cc Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 15 Jan 2025 13:21:36 +0100 Subject: [PATCH 006/189] msw: Add support for `counter` values to the models --- packages/crates-io-msw/utils/factory.js | 10 +++++++--- packages/crates-io-msw/vitest.setup.js | 7 +++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/crates-io-msw/utils/factory.js b/packages/crates-io-msw/utils/factory.js index f6e550a9b81..e26d86e1986 100644 --- a/packages/crates-io-msw/utils/factory.js +++ b/packages/crates-io-msw/utils/factory.js @@ -4,8 +4,9 @@ import { factory as mswFactory } from '@mswjs/data'; * This function creates a new MSW database instance with the given models. * * This is a custom factory function that extends the default MSW factory - * by adding support for a `preCreate()` function that is executed before - * creating a new model and has access to the model attributes. + * by adding a `counter` property to each model and support for a `preCreate()` + * function that is executed before creating a new model and has access to the + * model attributes and the current sequence number. */ export function factory(models) { // Extract `preCreate()` functions from the model definitions @@ -26,9 +27,12 @@ export function factory(models) { for (let [key, preCreate] of preCreateFns.entries()) { let modelApi = db[key]; + // Add a counter to each model. + modelApi.counter = 0; + modelApi.mswCreate = modelApi.create; modelApi.create = function (attrs = {}) { - preCreate(attrs); + preCreate(attrs, ++modelApi.counter); return modelApi.mswCreate(attrs); }; } diff --git a/packages/crates-io-msw/vitest.setup.js b/packages/crates-io-msw/vitest.setup.js index a06393ce007..4476e1dfbf9 100644 --- a/packages/crates-io-msw/vitest.setup.js +++ b/packages/crates-io-msw/vitest.setup.js @@ -9,4 +9,11 @@ const server = setupServer(...handlers); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterEach(() => drop(db)); +afterEach(() => { + Object.values(db).forEach(model => { + if (model.counter) { + model.counter = 0; + } + }); +}); afterAll(() => server.close()); From 42e20071611026198288bea7a3c207545f422c46 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 16 Jan 2025 14:02:27 +0100 Subject: [PATCH 007/189] msw: Extract `db.reset()` fn --- packages/crates-io-msw/utils/factory.d.ts | 6 +++++- packages/crates-io-msw/utils/factory.js | 12 ++++++++++++ packages/crates-io-msw/vitest.setup.js | 10 +--------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/crates-io-msw/utils/factory.d.ts b/packages/crates-io-msw/utils/factory.d.ts index 47c1d537ab8..c8de6302ede 100644 --- a/packages/crates-io-msw/utils/factory.d.ts +++ b/packages/crates-io-msw/utils/factory.d.ts @@ -1,3 +1,7 @@ -import { FactoryAPI, ModelDictionary } from '@mswjs/data/lib/glossary'; +import { FactoryAPI as mswFactoryApi, ModelDictionary } from '@mswjs/data/lib/glossary'; + +export declare type FactoryAPI = mswFactoryApi & { + reset(): void; +}; export declare function factory(dictionary: Dictionary): FactoryAPI; diff --git a/packages/crates-io-msw/utils/factory.js b/packages/crates-io-msw/utils/factory.js index e26d86e1986..1c7fb416591 100644 --- a/packages/crates-io-msw/utils/factory.js +++ b/packages/crates-io-msw/utils/factory.js @@ -37,5 +37,17 @@ export function factory(models) { }; } + db.reset = function () { + for (let model of Object.values(db)) { + if (model.deleteMany) { + model.deleteMany({ where: {} }); + } + + if (model.counter) { + model.counter = 0; + } + } + }; + return db; } diff --git a/packages/crates-io-msw/vitest.setup.js b/packages/crates-io-msw/vitest.setup.js index 4476e1dfbf9..c40a943a5e7 100644 --- a/packages/crates-io-msw/vitest.setup.js +++ b/packages/crates-io-msw/vitest.setup.js @@ -1,4 +1,3 @@ -import { drop } from '@mswjs/data'; import { setupServer } from 'msw/node'; import { afterAll, afterEach, beforeAll } from 'vitest'; @@ -8,12 +7,5 @@ const server = setupServer(...handlers); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); -afterEach(() => drop(db)); -afterEach(() => { - Object.values(db).forEach(model => { - if (model.counter) { - model.counter = 0; - } - }); -}); +afterEach(() => db.reset()); afterAll(() => server.close()); From 15dbb4bc0be79fdd832157887e99140f446c334e Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 15 Jan 2025 12:28:17 +0100 Subject: [PATCH 008/189] msw: Import `dasherize()` fn from `@ember/string` In the current form `@ember/string` can't easily be used as a dependency. Since we only need the `dasherize()` fn it's easiest to just "vendor" the four line function. --- packages/crates-io-msw/utils/strings.js | 6 ++++++ packages/crates-io-msw/utils/strings.test.js | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 packages/crates-io-msw/utils/strings.js create mode 100644 packages/crates-io-msw/utils/strings.test.js diff --git a/packages/crates-io-msw/utils/strings.js b/packages/crates-io-msw/utils/strings.js new file mode 100644 index 00000000000..637a6129bf7 --- /dev/null +++ b/packages/crates-io-msw/utils/strings.js @@ -0,0 +1,6 @@ +export function dasherize(str) { + return str + .replace(/([a-z\d])([A-Z])/g, '$1_$2') + .toLowerCase() + .replace(/[ _]/g, '-'); +} diff --git a/packages/crates-io-msw/utils/strings.test.js b/packages/crates-io-msw/utils/strings.test.js new file mode 100644 index 00000000000..daf414b06ec --- /dev/null +++ b/packages/crates-io-msw/utils/strings.test.js @@ -0,0 +1,20 @@ +import { describe, test } from 'vitest'; + +import { dasherize } from './strings.js'; + +describe('dasherize', () => { + function assert(input, expected) { + test(input, ({ expect }) => { + expect(dasherize(input)).toBe(expected); + }); + } + + assert('my favorite items', 'my-favorite-items'); + assert('css-class-name', 'css-class-name'); + assert('action_name', 'action-name'); + assert('innerHTML', 'inner-html'); + assert('toString', 'to-string'); + assert('PrivateDocs/OwnerInvoice', 'private-docs/owner-invoice'); + assert('privateDocs/ownerInvoice', 'private-docs/owner-invoice'); + assert('private_docs/owner_invoice', 'private-docs/owner-invoice'); +}); From bc236e9edfd081f04228580846cb4a31d88b7816 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 20 Jan 2025 14:50:43 +0100 Subject: [PATCH 009/189] msw: Implement `category` model --- packages/crates-io-msw/index.js | 5 ++- packages/crates-io-msw/models/category.js | 23 ++++++++++++ .../crates-io-msw/models/category.test.js | 35 +++++++++++++++++++ packages/crates-io-msw/utils/defaults.js | 5 +++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/models/category.js create mode 100644 packages/crates-io-msw/models/category.test.js create mode 100644 packages/crates-io-msw/utils/defaults.js diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index f18198673ec..81836e5e59c 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,5 +1,8 @@ +import category from './models/category.js'; import { factory } from './utils/factory.js'; export const handlers = []; -export const db = factory({}); +export const db = factory({ + category, +}); diff --git a/packages/crates-io-msw/models/category.js b/packages/crates-io-msw/models/category.js new file mode 100644 index 00000000000..c88e9b6d23d --- /dev/null +++ b/packages/crates-io-msw/models/category.js @@ -0,0 +1,23 @@ +import { nullable, primaryKey } from '@mswjs/data'; + +import { applyDefault } from '../utils/defaults.js'; +import { dasherize } from '../utils/strings.js'; + +export default { + id: primaryKey(String), + + category: String, + slug: String, + description: String, + created_at: String, + crates_cnt: nullable(Number), + + preCreate(attrs, counter) { + applyDefault(attrs, 'category', () => `Category ${counter}`); + applyDefault(attrs, 'slug', () => dasherize(attrs.category)); + applyDefault(attrs, 'id', () => attrs.slug); + applyDefault(attrs, 'description', () => `This is the description for the category called "${attrs.category}"`); + applyDefault(attrs, 'created_at', () => '2010-06-16T21:30:45Z'); + applyDefault(attrs, 'crates_cnt', () => null); + }, +}; diff --git a/packages/crates-io-msw/models/category.test.js b/packages/crates-io-msw/models/category.test.js new file mode 100644 index 00000000000..92bb05d517b --- /dev/null +++ b/packages/crates-io-msw/models/category.test.js @@ -0,0 +1,35 @@ +import { test } from 'vitest'; + +import { db } from '../index.js'; + +test('default are applied', ({ expect }) => { + let category = db.category.create(); + expect(category).toMatchInlineSnapshot(` + { + "category": "Category 1", + "crates_cnt": null, + "created_at": "2010-06-16T21:30:45Z", + "description": "This is the description for the category called "Category 1"", + "id": "category-1", + "slug": "category-1", + Symbol(type): "category", + Symbol(primaryKey): "id", + } + `); +}); + +test('name can be set', ({ expect }) => { + let category = db.category.create({ category: 'Network programming' }); + expect(category).toMatchInlineSnapshot(` + { + "category": "Network programming", + "crates_cnt": null, + "created_at": "2010-06-16T21:30:45Z", + "description": "This is the description for the category called "Network programming"", + "id": "network-programming", + "slug": "network-programming", + Symbol(type): "category", + Symbol(primaryKey): "id", + } + `); +}); diff --git a/packages/crates-io-msw/utils/defaults.js b/packages/crates-io-msw/utils/defaults.js new file mode 100644 index 00000000000..cb0fa52aeb9 --- /dev/null +++ b/packages/crates-io-msw/utils/defaults.js @@ -0,0 +1,5 @@ +export function applyDefault(attrs, key, fn) { + if (!(key in attrs)) { + attrs[key] = fn(); + } +} From ccb4cef918b85a7a8b069a906a9f57b358a7a596 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 20 Jan 2025 14:58:04 +0100 Subject: [PATCH 010/189] msw: Implement `keyword` model --- packages/crates-io-msw/index.js | 2 ++ packages/crates-io-msw/models/keyword.js | 14 ++++++++++ packages/crates-io-msw/models/keyword.test.js | 27 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 packages/crates-io-msw/models/keyword.js create mode 100644 packages/crates-io-msw/models/keyword.test.js diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 81836e5e59c..5271c27650b 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,8 +1,10 @@ import category from './models/category.js'; +import keyword from './models/keyword.js'; import { factory } from './utils/factory.js'; export const handlers = []; export const db = factory({ category, + keyword, }); diff --git a/packages/crates-io-msw/models/keyword.js b/packages/crates-io-msw/models/keyword.js new file mode 100644 index 00000000000..d6e914823a0 --- /dev/null +++ b/packages/crates-io-msw/models/keyword.js @@ -0,0 +1,14 @@ +import { primaryKey } from '@mswjs/data'; + +import { applyDefault } from '../utils/defaults.js'; + +export default { + id: primaryKey(String), + + keyword: String, + + preCreate(attrs, counter) { + applyDefault(attrs, 'keyword', () => `keyword-${counter}`); + applyDefault(attrs, 'id', () => attrs.keyword); + }, +}; diff --git a/packages/crates-io-msw/models/keyword.test.js b/packages/crates-io-msw/models/keyword.test.js new file mode 100644 index 00000000000..59e32a48b8e --- /dev/null +++ b/packages/crates-io-msw/models/keyword.test.js @@ -0,0 +1,27 @@ +import { test } from 'vitest'; + +import { db } from '../index.js'; + +test('default are applied', ({ expect }) => { + let keyword = db.keyword.create(); + expect(keyword).toMatchInlineSnapshot(` + { + "id": "keyword-1", + "keyword": "keyword-1", + Symbol(type): "keyword", + Symbol(primaryKey): "id", + } + `); +}); + +test('name can be set', ({ expect }) => { + let keyword = db.keyword.create({ keyword: 'gamedev' }); + expect(keyword).toMatchInlineSnapshot(` + { + "id": "gamedev", + "keyword": "gamedev", + Symbol(type): "keyword", + Symbol(primaryKey): "id", + } + `); +}); From 88944551ce842a21f41589a246419a7ca18158be Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 20 Jan 2025 15:17:16 +0100 Subject: [PATCH 011/189] msw: Implement `crate` model --- packages/crates-io-msw/index.js | 2 + packages/crates-io-msw/models/crate.js | 35 +++++++++ packages/crates-io-msw/models/crate.test.js | 84 +++++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 packages/crates-io-msw/models/crate.js create mode 100644 packages/crates-io-msw/models/crate.test.js diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 5271c27650b..2241339b757 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,4 +1,5 @@ import category from './models/category.js'; +import crate from './models/crate.js'; import keyword from './models/keyword.js'; import { factory } from './utils/factory.js'; @@ -6,5 +7,6 @@ export const handlers = []; export const db = factory({ category, + crate, keyword, }); diff --git a/packages/crates-io-msw/models/crate.js b/packages/crates-io-msw/models/crate.js new file mode 100644 index 00000000000..ca253732260 --- /dev/null +++ b/packages/crates-io-msw/models/crate.js @@ -0,0 +1,35 @@ +import { manyOf, nullable, primaryKey } from '@mswjs/data'; + +import { applyDefault } from '../utils/defaults.js'; + +export default { + id: primaryKey(Number), + + name: String, + description: String, + downloads: Number, + recent_downloads: Number, + documentation: nullable(String), + homepage: nullable(String), + repository: nullable(String), + created_at: String, + updated_at: String, + badges: Array, + _extra_downloads: Array, + + categories: manyOf('category'), + keywords: manyOf('keyword'), + + preCreate(attrs, counter) { + applyDefault(attrs, 'id', () => counter); + applyDefault(attrs, 'name', () => `crate-${attrs.id}`); + applyDefault(attrs, 'description', () => `This is the description for the crate called "${attrs.name}"`); + applyDefault(attrs, 'downloads', () => (((attrs.id + 13) * 42) % 13) * 12_345); + applyDefault(attrs, 'recent_downloads', () => (((attrs.id + 7) * 31) % 13) * 321); + applyDefault(attrs, 'documentation', () => null); + applyDefault(attrs, 'homepage', () => null); + applyDefault(attrs, 'repository', () => null); + applyDefault(attrs, 'created_at', () => '2010-06-16T21:30:45Z'); + applyDefault(attrs, 'updated_at', () => '2017-02-24T12:34:56Z'); + }, +}; diff --git a/packages/crates-io-msw/models/crate.test.js b/packages/crates-io-msw/models/crate.test.js new file mode 100644 index 00000000000..7a40dc09f3f --- /dev/null +++ b/packages/crates-io-msw/models/crate.test.js @@ -0,0 +1,84 @@ +import { test } from 'vitest'; + +import { db } from '../index.js'; + +test('default are applied', ({ expect }) => { + let crate = db.crate.create(); + expect(crate).toMatchInlineSnapshot(` + { + "_extra_downloads": [], + "badges": [], + "categories": [], + "created_at": "2010-06-16T21:30:45Z", + "description": "This is the description for the crate called "crate-1"", + "documentation": null, + "downloads": 37035, + "homepage": null, + "id": 1, + "keywords": [], + "name": "crate-1", + "recent_downloads": 321, + "repository": null, + "updated_at": "2017-02-24T12:34:56Z", + Symbol(type): "crate", + Symbol(primaryKey): "id", + } + `); +}); + +test('attributes can be set', ({ expect }) => { + let category = db.category.create(); + let keyword1 = db.keyword.create(); + let keyword2 = db.keyword.create(); + + let crate = db.crate.create({ + name: 'crates-io', + categories: [category], + keywords: [keyword1, keyword2], + }); + + expect(crate).toMatchInlineSnapshot(` + { + "_extra_downloads": [], + "badges": [], + "categories": [ + { + "category": "Category 1", + "crates_cnt": null, + "created_at": "2010-06-16T21:30:45Z", + "description": "This is the description for the category called "Category 1"", + "id": "category-1", + "slug": "category-1", + Symbol(type): "category", + Symbol(primaryKey): "id", + }, + ], + "created_at": "2010-06-16T21:30:45Z", + "description": "This is the description for the crate called "crates-io"", + "documentation": null, + "downloads": 37035, + "homepage": null, + "id": 1, + "keywords": [ + { + "id": "keyword-1", + "keyword": "keyword-1", + Symbol(type): "keyword", + Symbol(primaryKey): "id", + }, + { + "id": "keyword-2", + "keyword": "keyword-2", + Symbol(type): "keyword", + Symbol(primaryKey): "id", + }, + ], + "name": "crates-io", + "recent_downloads": 321, + "repository": null, + "updated_at": "2017-02-24T12:34:56Z", + Symbol(type): "crate", + Symbol(primaryKey): "id", + } + `); +}); From 61a9a03344c0f15ffde56c3e2ad8bed7f12341d3 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 15 Jan 2025 12:33:55 +0100 Subject: [PATCH 012/189] msw: Implement `user` model --- packages/crates-io-msw/index.js | 2 + packages/crates-io-msw/models/user.js | 33 ++++++++++++++++ packages/crates-io-msw/models/user.test.js | 45 ++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 packages/crates-io-msw/models/user.js create mode 100644 packages/crates-io-msw/models/user.test.js diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 2241339b757..3f1d65278f9 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,6 +1,7 @@ import category from './models/category.js'; import crate from './models/crate.js'; import keyword from './models/keyword.js'; +import user from './models/user.js'; import { factory } from './utils/factory.js'; export const handlers = []; @@ -9,4 +10,5 @@ export const db = factory({ category, crate, keyword, + user, }); diff --git a/packages/crates-io-msw/models/user.js b/packages/crates-io-msw/models/user.js new file mode 100644 index 00000000000..2d3ce6f22f4 --- /dev/null +++ b/packages/crates-io-msw/models/user.js @@ -0,0 +1,33 @@ +import { manyOf, nullable, primaryKey } from '@mswjs/data'; + +import { applyDefault } from '../utils/defaults.js'; +import { dasherize } from '../utils/strings.js'; + +export default { + id: primaryKey(Number), + + name: nullable(String), + login: String, + url: String, + avatar: String, + email: nullable(String), + emailVerificationToken: nullable(String), + emailVerified: Boolean, + isAdmin: Boolean, + publishNotifications: Boolean, + + followedCrates: manyOf('crate'), + + preCreate(attrs, counter) { + applyDefault(attrs, 'id', () => counter); + applyDefault(attrs, 'name', () => `User ${attrs.id}`); + applyDefault(attrs, 'login', () => (attrs.name ? dasherize(attrs.name) : `user-${attrs.id}`)); + applyDefault(attrs, 'email', () => `${attrs.login}@crates.io`); + applyDefault(attrs, 'url', () => `https://github.com/${attrs.login}`); + applyDefault(attrs, 'avatar', () => 'https://avatars1.githubusercontent.com/u/14631425?v=4'); + applyDefault(attrs, 'emailVerificationToken', () => null); + applyDefault(attrs, 'emailVerified', () => Boolean(attrs.email && !attrs.emailVerificationToken)); + applyDefault(attrs, 'isAdmin', () => false); + applyDefault(attrs, 'publishNotifications', () => true); + }, +}; diff --git a/packages/crates-io-msw/models/user.test.js b/packages/crates-io-msw/models/user.test.js new file mode 100644 index 00000000000..e3db559e569 --- /dev/null +++ b/packages/crates-io-msw/models/user.test.js @@ -0,0 +1,45 @@ +import { test } from 'vitest'; + +import { db } from '../index.js'; + +test('default are applied', ({ expect }) => { + let user = db.user.create(); + expect(user).toMatchInlineSnapshot(` + { + "avatar": "https://avatars1.githubusercontent.com/u/14631425?v=4", + "email": "user-1@crates.io", + "emailVerificationToken": null, + "emailVerified": true, + "followedCrates": [], + "id": 1, + "isAdmin": false, + "login": "user-1", + "name": "User 1", + "publishNotifications": true, + "url": "https://github.com/user-1", + Symbol(type): "user", + Symbol(primaryKey): "id", + } + `); +}); + +test('name can be set', ({ expect }) => { + let user = db.user.create({ name: 'John Doe' }); + expect(user).toMatchInlineSnapshot(` + { + "avatar": "https://avatars1.githubusercontent.com/u/14631425?v=4", + "email": "john-doe@crates.io", + "emailVerificationToken": null, + "emailVerified": true, + "followedCrates": [], + "id": 1, + "isAdmin": false, + "login": "john-doe", + "name": "John Doe", + "publishNotifications": true, + "url": "https://github.com/john-doe", + Symbol(type): "user", + Symbol(primaryKey): "id", + } + `); +}); From 31ddd22739614d0eebd198a62285353f550dff5b Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 15 Jan 2025 15:46:02 +0100 Subject: [PATCH 013/189] msw: Implement `mswSession` model --- packages/crates-io-msw/index.js | 2 ++ packages/crates-io-msw/models/msw-session.js | 25 ++++++++++++++ .../crates-io-msw/models/msw-session.test.js | 34 +++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 packages/crates-io-msw/models/msw-session.js create mode 100644 packages/crates-io-msw/models/msw-session.test.js diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 3f1d65278f9..33a460a9bf9 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,6 +1,7 @@ import category from './models/category.js'; import crate from './models/crate.js'; import keyword from './models/keyword.js'; +import mswSession from './models/msw-session.js'; import user from './models/user.js'; import { factory } from './utils/factory.js'; @@ -10,5 +11,6 @@ export const db = factory({ category, crate, keyword, + mswSession, user, }); diff --git a/packages/crates-io-msw/models/msw-session.js b/packages/crates-io-msw/models/msw-session.js new file mode 100644 index 00000000000..f66f614b9d5 --- /dev/null +++ b/packages/crates-io-msw/models/msw-session.js @@ -0,0 +1,25 @@ +import { oneOf, primaryKey } from '@mswjs/data'; + +import { applyDefault } from '../utils/defaults.js'; + +/** + * This is a MSW-only model, that is used to keep track of the current + * session and the associated `user` model, because in route handlers we don't + * have access to the cookie data that the actual API is using for these things. + * + * This mock implementation means that there can only ever exist one + * session at a time. + */ +export default { + id: primaryKey(Number), + + user: oneOf('user'), + + preCreate(attrs, counter) { + applyDefault(attrs, 'id', () => counter); + + if (!attrs.user) { + throw new Error('Missing `user` relationship'); + } + }, +}; diff --git a/packages/crates-io-msw/models/msw-session.test.js b/packages/crates-io-msw/models/msw-session.test.js new file mode 100644 index 00000000000..5a6874566c9 --- /dev/null +++ b/packages/crates-io-msw/models/msw-session.test.js @@ -0,0 +1,34 @@ +import { test } from 'vitest'; + +import { db } from '../index.js'; + +test('throws if `user` is not set', ({ expect }) => { + expect(() => db.mswSession.create()).toThrowErrorMatchingInlineSnapshot(`[Error: Missing \`user\` relationship]`); +}); + +test('happy path', ({ expect }) => { + let user = db.user.create(); + let session = db.mswSession.create({ user }); + expect(session).toMatchInlineSnapshot(` + { + "id": 1, + "user": { + "avatar": "https://avatars1.githubusercontent.com/u/14631425?v=4", + "email": "user-1@crates.io", + "emailVerificationToken": null, + "emailVerified": true, + "followedCrates": [], + "id": 1, + "isAdmin": false, + "login": "user-1", + "name": "User 1", + "publishNotifications": true, + "url": "https://github.com/user-1", + Symbol(type): "user", + Symbol(primaryKey): "id", + }, + Symbol(type): "mswSession", + Symbol(primaryKey): "id", + } + `); +}); From e1237f79c38cf20f217123101ef272a7104d7a07 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 17 Jan 2025 11:16:42 +0100 Subject: [PATCH 014/189] msw: Implement `apiToken` model --- packages/crates-io-msw/index.js | 2 + packages/crates-io-msw/models/api-token.js | 39 ++++++++++++++++ .../crates-io-msw/models/api-token.test.js | 44 +++++++++++++++++++ packages/crates-io-msw/utils/random.js | 12 +++++ 4 files changed, 97 insertions(+) create mode 100644 packages/crates-io-msw/models/api-token.js create mode 100644 packages/crates-io-msw/models/api-token.test.js create mode 100644 packages/crates-io-msw/utils/random.js diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 33a460a9bf9..3850b2a2361 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,3 +1,4 @@ +import apiToken from './models/api-token.js'; import category from './models/category.js'; import crate from './models/crate.js'; import keyword from './models/keyword.js'; @@ -8,6 +9,7 @@ import { factory } from './utils/factory.js'; export const handlers = []; export const db = factory({ + apiToken, category, crate, keyword, diff --git a/packages/crates-io-msw/models/api-token.js b/packages/crates-io-msw/models/api-token.js new file mode 100644 index 00000000000..1d3bd2b4ac5 --- /dev/null +++ b/packages/crates-io-msw/models/api-token.js @@ -0,0 +1,39 @@ +import { nullable, oneOf, primaryKey } from '@mswjs/data'; + +import { applyDefault } from '../utils/defaults.js'; +import { seededRandom } from '../utils/random.js'; + +export default { + id: primaryKey(Number), + + crateScopes: nullable(Array), + createdAt: String, + endpointScopes: nullable(Array), + expiredAt: nullable(String), + lastUsedAt: nullable(String), + name: String, + token: String, + revoked: Boolean, + + user: oneOf('user'), + + preCreate(attrs, counter) { + applyDefault(attrs, 'id', () => counter); + applyDefault(attrs, 'crateScopes', () => null); + applyDefault(attrs, 'createdAt', () => '2017-11-19T17:59:22Z'); + applyDefault(attrs, 'endpointScopes', () => null); + applyDefault(attrs, 'expiredAt', () => null); + applyDefault(attrs, 'lastUsedAt', () => null); + applyDefault(attrs, 'name', () => `API Token ${attrs.id}`); + applyDefault(attrs, 'token', () => generateToken(counter)); + applyDefault(attrs, 'revoked', () => false); + + if (!attrs.user) { + throw new Error('Missing `user` relationship on `api-token`'); + } + }, +}; + +function generateToken(seed) { + return seededRandom(seed).toString().slice(2); +} diff --git a/packages/crates-io-msw/models/api-token.test.js b/packages/crates-io-msw/models/api-token.test.js new file mode 100644 index 00000000000..135b78352b9 --- /dev/null +++ b/packages/crates-io-msw/models/api-token.test.js @@ -0,0 +1,44 @@ +import { test } from 'vitest'; + +import { db } from '../index.js'; + +test('throws if `user` is not set', ({ expect }) => { + expect(() => db.apiToken.create()).toThrowErrorMatchingInlineSnapshot( + `[Error: Missing \`user\` relationship on \`api-token\`]`, + ); +}); + +test('happy path', ({ expect }) => { + let user = db.user.create(); + let session = db.apiToken.create({ user }); + expect(session).toMatchInlineSnapshot(` + { + "crateScopes": null, + "createdAt": "2017-11-19T17:59:22Z", + "endpointScopes": null, + "expiredAt": null, + "id": 1, + "lastUsedAt": null, + "name": "API Token 1", + "revoked": false, + "token": "6270739405881613", + "user": { + "avatar": "https://avatars1.githubusercontent.com/u/14631425?v=4", + "email": "user-1@crates.io", + "emailVerificationToken": null, + "emailVerified": true, + "followedCrates": [], + "id": 1, + "isAdmin": false, + "login": "user-1", + "name": "User 1", + "publishNotifications": true, + "url": "https://github.com/user-1", + Symbol(type): "user", + Symbol(primaryKey): "id", + }, + Symbol(type): "apiToken", + Symbol(primaryKey): "id", + } + `); +}); diff --git a/packages/crates-io-msw/utils/random.js b/packages/crates-io-msw/utils/random.js new file mode 100644 index 00000000000..176096483c5 --- /dev/null +++ b/packages/crates-io-msw/utils/random.js @@ -0,0 +1,12 @@ +export function seededRandom(seed) { + return mulberry32(seed)(); +} + +function mulberry32(a) { + return function () { + let t = (a += 0x6d_2b_79_f5); + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4_294_967_296; + }; +} From a5f32811da2b8987daaa2d21a18bbe04f1e140f7 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 20 Jan 2025 15:09:45 +0100 Subject: [PATCH 015/189] msw: Implement `team` model --- packages/crates-io-msw/index.js | 2 ++ packages/crates-io-msw/models/team.js | 24 +++++++++++++++ packages/crates-io-msw/models/team.test.js | 35 ++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 packages/crates-io-msw/models/team.js create mode 100644 packages/crates-io-msw/models/team.test.js diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 3850b2a2361..0b20f946ba0 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -3,6 +3,7 @@ import category from './models/category.js'; import crate from './models/crate.js'; import keyword from './models/keyword.js'; import mswSession from './models/msw-session.js'; +import team from './models/team.js'; import user from './models/user.js'; import { factory } from './utils/factory.js'; @@ -14,5 +15,6 @@ export const db = factory({ crate, keyword, mswSession, + team, user, }); diff --git a/packages/crates-io-msw/models/team.js b/packages/crates-io-msw/models/team.js new file mode 100644 index 00000000000..87df22a6f80 --- /dev/null +++ b/packages/crates-io-msw/models/team.js @@ -0,0 +1,24 @@ +import { primaryKey } from '@mswjs/data'; + +import { applyDefault } from '../utils/defaults.js'; + +const ORGS = ['rust-lang', 'emberjs', 'rust-random', 'georust', 'actix']; + +export default { + id: primaryKey(Number), + + name: String, + org: String, + login: String, + url: String, + avatar: String, + + preCreate(attrs, counter) { + applyDefault(attrs, 'id', () => counter); + applyDefault(attrs, 'name', () => `team-${attrs.id}`); + applyDefault(attrs, 'org', () => ORGS[(attrs.id - 1) % ORGS.length]); + applyDefault(attrs, 'login', () => `github:${attrs.org}:${attrs.name}`); + applyDefault(attrs, 'url', () => `https://github.com/${attrs.org}`); + applyDefault(attrs, 'avatar', () => 'https://avatars1.githubusercontent.com/u/14631425?v=4'); + }, +}; diff --git a/packages/crates-io-msw/models/team.test.js b/packages/crates-io-msw/models/team.test.js new file mode 100644 index 00000000000..c3aecafd0f1 --- /dev/null +++ b/packages/crates-io-msw/models/team.test.js @@ -0,0 +1,35 @@ +import { test } from 'vitest'; + +import { db } from '../index.js'; + +test('default are applied', ({ expect }) => { + let team = db.team.create(); + expect(team).toMatchInlineSnapshot(` + { + "avatar": "https://avatars1.githubusercontent.com/u/14631425?v=4", + "id": 1, + "login": "github:rust-lang:team-1", + "name": "team-1", + "org": "rust-lang", + "url": "https://github.com/rust-lang", + Symbol(type): "team", + Symbol(primaryKey): "id", + } + `); +}); + +test('attributes can be set', ({ expect }) => { + let team = db.team.create({ name: 'axum', org: 'tokio-rs' }); + expect(team).toMatchInlineSnapshot(` + { + "avatar": "https://avatars1.githubusercontent.com/u/14631425?v=4", + "id": 1, + "login": "github:tokio-rs:axum", + "name": "axum", + "org": "tokio-rs", + "url": "https://github.com/tokio-rs", + Symbol(type): "team", + Symbol(primaryKey): "id", + } + `); +}); From 36b0bb08341766533daf8326062a6b0d0fe45dd1 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 20 Jan 2025 15:30:46 +0100 Subject: [PATCH 016/189] msw: Implement `version` model --- packages/crates-io-msw/index.js | 2 + packages/crates-io-msw/models/version.js | 42 +++++++++++++++ packages/crates-io-msw/models/version.test.js | 51 +++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 packages/crates-io-msw/models/version.js create mode 100644 packages/crates-io-msw/models/version.test.js diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 0b20f946ba0..687259eb28a 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -5,6 +5,7 @@ import keyword from './models/keyword.js'; import mswSession from './models/msw-session.js'; import team from './models/team.js'; import user from './models/user.js'; +import version from './models/version.js'; import { factory } from './utils/factory.js'; export const handlers = []; @@ -17,4 +18,5 @@ export const db = factory({ mswSession, team, user, + version, }); diff --git a/packages/crates-io-msw/models/version.js b/packages/crates-io-msw/models/version.js new file mode 100644 index 00000000000..90b367e774f --- /dev/null +++ b/packages/crates-io-msw/models/version.js @@ -0,0 +1,42 @@ +import { nullable, oneOf, primaryKey } from '@mswjs/data'; + +import { applyDefault } from '../utils/defaults.js'; + +const LICENSES = ['MIT/Apache-2.0', 'MIT', 'Apache-2.0']; + +export default { + id: primaryKey(Number), + + num: String, + created_at: String, + updated_at: String, + yanked: Boolean, + yank_message: nullable(String), + license: String, + downloads: Number, + features: Object, + crate_size: Number, + readme: nullable(String), + rust_version: nullable(String), + + crate: oneOf('crate'), + publishedBy: nullable(oneOf('user')), + + preCreate(attrs, counter) { + applyDefault(attrs, 'id', () => counter); + applyDefault(attrs, 'num', () => `1.0.${attrs.id - 1}`); + applyDefault(attrs, 'created_at', () => '2010-06-16T21:30:45Z'); + applyDefault(attrs, 'updated_at', () => '2017-02-24T12:34:56Z'); + applyDefault(attrs, 'yanked', () => false); + applyDefault(attrs, 'yank_message', () => null); + applyDefault(attrs, 'license', () => LICENSES[attrs.id % LICENSES.length]); + applyDefault(attrs, 'downloads', () => (((attrs.id + 13) * 42) % 13) * 1234); + applyDefault(attrs, 'crate_size', () => (((attrs.id + 13) * 42) % 13) * 54_321); + applyDefault(attrs, 'readme', () => null); + applyDefault(attrs, 'rust_version', () => null); + + if (!attrs.crate) { + throw new Error(`Missing \`crate\` relationship on \`version:${attrs.num}\``); + } + }, +}; diff --git a/packages/crates-io-msw/models/version.test.js b/packages/crates-io-msw/models/version.test.js new file mode 100644 index 00000000000..e5d6e3c002f --- /dev/null +++ b/packages/crates-io-msw/models/version.test.js @@ -0,0 +1,51 @@ +import { test } from 'vitest'; + +import { db } from '../index.js'; + +test('throws if `crate` is not set', ({ expect }) => { + expect(() => db.version.create()).toThrowErrorMatchingInlineSnapshot( + `[Error: Missing \`crate\` relationship on \`version:1.0.0\`]`, + ); +}); + +test('happy path', ({ expect }) => { + let crate = db.crate.create(); + let version = db.version.create({ crate }); + expect(version).toMatchInlineSnapshot(` + { + "crate": { + "_extra_downloads": [], + "badges": [], + "categories": [], + "created_at": "2010-06-16T21:30:45Z", + "description": "This is the description for the crate called "crate-1"", + "documentation": null, + "downloads": 37035, + "homepage": null, + "id": 1, + "keywords": [], + "name": "crate-1", + "recent_downloads": 321, + "repository": null, + "updated_at": "2017-02-24T12:34:56Z", + Symbol(type): "crate", + Symbol(primaryKey): "id", + }, + "crate_size": 162963, + "created_at": "2010-06-16T21:30:45Z", + "downloads": 3702, + "features": {}, + "id": 1, + "license": "MIT", + "num": "1.0.0", + "publishedBy": null, + "readme": null, + "rust_version": null, + "updated_at": "2017-02-24T12:34:56Z", + "yank_message": null, + "yanked": false, + Symbol(type): "version", + Symbol(primaryKey): "id", + } + `); +}); From 1a468d5141410872b0d92816681529d62e605067 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 20 Jan 2025 15:39:45 +0100 Subject: [PATCH 017/189] msw: Implement `versionDownload` model --- packages/crates-io-msw/index.js | 2 + .../crates-io-msw/models/version-download.js | 22 +++++++ .../models/version-download.test.js | 59 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 packages/crates-io-msw/models/version-download.js create mode 100644 packages/crates-io-msw/models/version-download.test.js diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 687259eb28a..a1891037a80 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -5,6 +5,7 @@ import keyword from './models/keyword.js'; import mswSession from './models/msw-session.js'; import team from './models/team.js'; import user from './models/user.js'; +import versionDownload from './models/version-download.js'; import version from './models/version.js'; import { factory } from './utils/factory.js'; @@ -18,5 +19,6 @@ export const db = factory({ mswSession, team, user, + versionDownload, version, }); diff --git a/packages/crates-io-msw/models/version-download.js b/packages/crates-io-msw/models/version-download.js new file mode 100644 index 00000000000..cf2ec077806 --- /dev/null +++ b/packages/crates-io-msw/models/version-download.js @@ -0,0 +1,22 @@ +import { oneOf, primaryKey } from '@mswjs/data'; + +import { applyDefault } from '../utils/defaults.js'; + +export default { + id: primaryKey(Number), + + date: String, + downloads: Number, + + version: oneOf('version'), + + preCreate(attrs, counter) { + applyDefault(attrs, 'id', () => counter); + applyDefault(attrs, 'date', () => '2019-05-21'); + applyDefault(attrs, 'downloads', () => (((attrs.id + 13) * 42) % 13) * 2345); + + if (!attrs.version) { + throw new Error('Missing `version` relationship on `version-download`'); + } + }, +}; diff --git a/packages/crates-io-msw/models/version-download.test.js b/packages/crates-io-msw/models/version-download.test.js new file mode 100644 index 00000000000..76a495470ef --- /dev/null +++ b/packages/crates-io-msw/models/version-download.test.js @@ -0,0 +1,59 @@ +import { test } from 'vitest'; + +import { db } from '../index.js'; + +test('throws if `version` is not set', ({ expect }) => { + expect(() => db.versionDownload.create()).toThrowErrorMatchingInlineSnapshot( + `[Error: Missing \`version\` relationship on \`version-download\`]`, + ); +}); + +test('happy path', ({ expect }) => { + let crate = db.crate.create(); + let version = db.version.create({ crate }); + let versionDownload = db.versionDownload.create({ version }); + expect(versionDownload).toMatchInlineSnapshot(` + { + "date": "2019-05-21", + "downloads": 7035, + "id": 1, + "version": { + "crate": { + "_extra_downloads": [], + "badges": [], + "categories": [], + "created_at": "2010-06-16T21:30:45Z", + "description": "This is the description for the crate called "crate-1"", + "documentation": null, + "downloads": 37035, + "homepage": null, + "id": 1, + "keywords": [], + "name": "crate-1", + "recent_downloads": 321, + "repository": null, + "updated_at": "2017-02-24T12:34:56Z", + Symbol(type): "crate", + Symbol(primaryKey): "id", + }, + "crate_size": 162963, + "created_at": "2010-06-16T21:30:45Z", + "downloads": 3702, + "features": {}, + "id": 1, + "license": "MIT", + "num": "1.0.0", + "publishedBy": null, + "readme": null, + "rust_version": null, + "updated_at": "2017-02-24T12:34:56Z", + "yank_message": null, + "yanked": false, + Symbol(type): "version", + Symbol(primaryKey): "id", + }, + Symbol(type): "versionDownload", + Symbol(primaryKey): "id", + } + `); +}); From c7d595fd79cb69501c891bcf04e30955bb747233 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 20 Jan 2025 15:47:19 +0100 Subject: [PATCH 018/189] msw: Implement `crateOwnerInvitation` model --- packages/crates-io-msw/index.js | 2 + .../models/crate-owner-invitation.js | 32 +++++++ .../models/crate-owner-invitation.test.js | 92 +++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 packages/crates-io-msw/models/crate-owner-invitation.js create mode 100644 packages/crates-io-msw/models/crate-owner-invitation.test.js diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index a1891037a80..9b73bb60acc 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,5 +1,6 @@ import apiToken from './models/api-token.js'; import category from './models/category.js'; +import crateOwnerInvitation from './models/crate-owner-invitation.js'; import crate from './models/crate.js'; import keyword from './models/keyword.js'; import mswSession from './models/msw-session.js'; @@ -14,6 +15,7 @@ export const handlers = []; export const db = factory({ apiToken, category, + crateOwnerInvitation, crate, keyword, mswSession, diff --git a/packages/crates-io-msw/models/crate-owner-invitation.js b/packages/crates-io-msw/models/crate-owner-invitation.js new file mode 100644 index 00000000000..d05ea3e2f9a --- /dev/null +++ b/packages/crates-io-msw/models/crate-owner-invitation.js @@ -0,0 +1,32 @@ +import { oneOf, primaryKey } from '@mswjs/data'; + +import { applyDefault } from '../utils/defaults.js'; + +export default { + id: primaryKey(Number), + + createdAt: String, + expiresAt: String, + token: String, + + crate: oneOf('crate'), + invitee: oneOf('user'), + inviter: oneOf('user'), + + preCreate(attrs, counter) { + applyDefault(attrs, 'id', () => counter); + applyDefault(attrs, 'createdAt', () => '2016-12-24T12:34:56Z'); + applyDefault(attrs, 'expiresAt', () => '2017-01-24T12:34:56Z'); + applyDefault(attrs, 'token', () => `secret-token-${attrs.id}`); + + if (!attrs.crate) { + throw new Error(`Missing \`crate\` relationship on \`crate-owner-invitation\``); + } + if (!attrs.invitee) { + throw new Error(`Missing \`invitee\` relationship on \`crate-owner-invitation\``); + } + if (!attrs.inviter) { + throw new Error(`Missing \`inviter\` relationship on \`crate-owner-invitation\``); + } + }, +}; diff --git a/packages/crates-io-msw/models/crate-owner-invitation.test.js b/packages/crates-io-msw/models/crate-owner-invitation.test.js new file mode 100644 index 00000000000..32c10b97d52 --- /dev/null +++ b/packages/crates-io-msw/models/crate-owner-invitation.test.js @@ -0,0 +1,92 @@ +import { test } from 'vitest'; + +import { db } from '../index.js'; + +test('throws if `crate` is not set', ({ expect }) => { + let inviter = db.user.create(); + let invitee = db.user.create(); + expect(() => db.crateOwnerInvitation.create({ inviter, invitee })).toThrowErrorMatchingInlineSnapshot( + `[Error: Missing \`crate\` relationship on \`crate-owner-invitation\`]`, + ); +}); + +test('throws if `inviter` is not set', ({ expect }) => { + let crate = db.crate.create(); + let invitee = db.user.create(); + expect(() => db.crateOwnerInvitation.create({ crate, invitee })).toThrowErrorMatchingInlineSnapshot( + `[Error: Missing \`inviter\` relationship on \`crate-owner-invitation\`]`, + ); +}); + +test('throws if `invitee` is not set', ({ expect }) => { + let crate = db.crate.create(); + let inviter = db.user.create(); + expect(() => db.crateOwnerInvitation.create({ crate, inviter })).toThrowErrorMatchingInlineSnapshot( + `[Error: Missing \`invitee\` relationship on \`crate-owner-invitation\`]`, + ); +}); + +test('happy path', ({ expect }) => { + let crate = db.crate.create(); + let inviter = db.user.create(); + let invitee = db.user.create(); + let invite = db.crateOwnerInvitation.create({ crate, inviter, invitee }); + expect(invite).toMatchInlineSnapshot(` + { + "crate": { + "_extra_downloads": [], + "badges": [], + "categories": [], + "created_at": "2010-06-16T21:30:45Z", + "description": "This is the description for the crate called "crate-1"", + "documentation": null, + "downloads": 37035, + "homepage": null, + "id": 1, + "keywords": [], + "name": "crate-1", + "recent_downloads": 321, + "repository": null, + "updated_at": "2017-02-24T12:34:56Z", + Symbol(type): "crate", + Symbol(primaryKey): "id", + }, + "createdAt": "2016-12-24T12:34:56Z", + "expiresAt": "2017-01-24T12:34:56Z", + "id": 1, + "invitee": { + "avatar": "https://avatars1.githubusercontent.com/u/14631425?v=4", + "email": "user-2@crates.io", + "emailVerificationToken": null, + "emailVerified": true, + "followedCrates": [], + "id": 2, + "isAdmin": false, + "login": "user-2", + "name": "User 2", + "publishNotifications": true, + "url": "https://github.com/user-2", + Symbol(type): "user", + Symbol(primaryKey): "id", + }, + "inviter": { + "avatar": "https://avatars1.githubusercontent.com/u/14631425?v=4", + "email": "user-1@crates.io", + "emailVerificationToken": null, + "emailVerified": true, + "followedCrates": [], + "id": 1, + "isAdmin": false, + "login": "user-1", + "name": "User 1", + "publishNotifications": true, + "url": "https://github.com/user-1", + Symbol(type): "user", + Symbol(primaryKey): "id", + }, + "token": "secret-token-1", + Symbol(type): "crateOwnerInvitation", + Symbol(primaryKey): "id", + } + `); +}); From 3678f888625be8c59443bc1db53c329238207592 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 20 Jan 2025 15:52:47 +0100 Subject: [PATCH 019/189] msw: Implement `crateOwnership` model --- packages/crates-io-msw/index.js | 2 + .../crates-io-msw/models/crate-ownership.js | 28 +++++ .../models/crate-ownership.test.js | 117 ++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 packages/crates-io-msw/models/crate-ownership.js create mode 100644 packages/crates-io-msw/models/crate-ownership.test.js diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 9b73bb60acc..0ae4e28bf78 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,6 +1,7 @@ import apiToken from './models/api-token.js'; import category from './models/category.js'; import crateOwnerInvitation from './models/crate-owner-invitation.js'; +import crateOwnership from './models/crate-ownership.js'; import crate from './models/crate.js'; import keyword from './models/keyword.js'; import mswSession from './models/msw-session.js'; @@ -16,6 +17,7 @@ export const db = factory({ apiToken, category, crateOwnerInvitation, + crateOwnership, crate, keyword, mswSession, diff --git a/packages/crates-io-msw/models/crate-ownership.js b/packages/crates-io-msw/models/crate-ownership.js new file mode 100644 index 00000000000..47f22c7bdca --- /dev/null +++ b/packages/crates-io-msw/models/crate-ownership.js @@ -0,0 +1,28 @@ +import { nullable, oneOf, primaryKey } from '@mswjs/data'; + +import { applyDefault } from '../utils/defaults.js'; + +export default { + id: primaryKey(Number), + + emailNotifications: Boolean, + + crate: oneOf('crate'), + team: nullable(oneOf('team')), + user: nullable(oneOf('user')), + + preCreate(attrs, counter) { + applyDefault(attrs, 'id', () => counter); + applyDefault(attrs, 'emailNotifications', () => true); + + if (!attrs.crate) { + throw new Error('Missing `crate` relationship on `crate-ownership`'); + } + if (!attrs.team && !attrs.user) { + throw new Error('Missing `team` or `user` relationship on `crate-ownership`'); + } + if (attrs.team && attrs.user) { + throw new Error('`team` and `user` on a `crate-ownership` are mutually exclusive'); + } + }, +}; diff --git a/packages/crates-io-msw/models/crate-ownership.test.js b/packages/crates-io-msw/models/crate-ownership.test.js new file mode 100644 index 00000000000..00f6b389824 --- /dev/null +++ b/packages/crates-io-msw/models/crate-ownership.test.js @@ -0,0 +1,117 @@ +import { test } from 'vitest'; + +import { db } from '../index.js'; + +test('throws if `crate` is not set', ({ expect }) => { + let user = db.user.create(); + expect(() => db.crateOwnership.create({ user })).toThrowErrorMatchingInlineSnapshot( + `[Error: Missing \`crate\` relationship on \`crate-ownership\`]`, + ); +}); + +test('throws if `team` and `user` are not set', ({ expect }) => { + let crate = db.crate.create(); + expect(() => db.crateOwnership.create({ crate })).toThrowErrorMatchingInlineSnapshot( + `[Error: Missing \`team\` or \`user\` relationship on \`crate-ownership\`]`, + ); +}); + +test('throws if `team` and `user` are both set', ({ expect }) => { + let crate = db.crate.create(); + let team = db.team.create(); + let user = db.user.create(); + expect(() => db.crateOwnership.create({ crate, team, user })).toThrowErrorMatchingInlineSnapshot( + `[Error: \`team\` and \`user\` on a \`crate-ownership\` are mutually exclusive]`, + ); +}); + +test('can set `team`', ({ expect }) => { + let crate = db.crate.create(); + let team = db.team.create(); + let ownership = db.crateOwnership.create({ crate, team }); + expect(ownership).toMatchInlineSnapshot(` + { + "crate": { + "_extra_downloads": [], + "badges": [], + "categories": [], + "created_at": "2010-06-16T21:30:45Z", + "description": "This is the description for the crate called "crate-1"", + "documentation": null, + "downloads": 37035, + "homepage": null, + "id": 1, + "keywords": [], + "name": "crate-1", + "recent_downloads": 321, + "repository": null, + "updated_at": "2017-02-24T12:34:56Z", + Symbol(type): "crate", + Symbol(primaryKey): "id", + }, + "emailNotifications": true, + "id": 1, + "team": { + "avatar": "https://avatars1.githubusercontent.com/u/14631425?v=4", + "id": 1, + "login": "github:rust-lang:team-1", + "name": "team-1", + "org": "rust-lang", + "url": "https://github.com/rust-lang", + Symbol(type): "team", + Symbol(primaryKey): "id", + }, + "user": null, + Symbol(type): "crateOwnership", + Symbol(primaryKey): "id", + } + `); +}); + +test('can set `user`', ({ expect }) => { + let crate = db.crate.create(); + let user = db.user.create(); + let ownership = db.crateOwnership.create({ crate, user }); + expect(ownership).toMatchInlineSnapshot(` + { + "crate": { + "_extra_downloads": [], + "badges": [], + "categories": [], + "created_at": "2010-06-16T21:30:45Z", + "description": "This is the description for the crate called "crate-1"", + "documentation": null, + "downloads": 37035, + "homepage": null, + "id": 1, + "keywords": [], + "name": "crate-1", + "recent_downloads": 321, + "repository": null, + "updated_at": "2017-02-24T12:34:56Z", + Symbol(type): "crate", + Symbol(primaryKey): "id", + }, + "emailNotifications": true, + "id": 1, + "team": null, + "user": { + "avatar": "https://avatars1.githubusercontent.com/u/14631425?v=4", + "email": "user-1@crates.io", + "emailVerificationToken": null, + "emailVerified": true, + "followedCrates": [], + "id": 1, + "isAdmin": false, + "login": "user-1", + "name": "User 1", + "publishNotifications": true, + "url": "https://github.com/user-1", + Symbol(type): "user", + Symbol(primaryKey): "id", + }, + Symbol(type): "crateOwnership", + Symbol(primaryKey): "id", + } + `); +}); From 2a4f94fe51f082e28c41a00d1eb48fa4f77083db Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 20 Jan 2025 15:58:33 +0100 Subject: [PATCH 020/189] msw: Implement `dependency` model --- packages/crates-io-msw/index.js | 2 + packages/crates-io-msw/models/dependency.js | 35 ++++++++ .../crates-io-msw/models/dependency.test.js | 89 +++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 packages/crates-io-msw/models/dependency.js create mode 100644 packages/crates-io-msw/models/dependency.test.js diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 0ae4e28bf78..4a5b547105a 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -3,6 +3,7 @@ import category from './models/category.js'; import crateOwnerInvitation from './models/crate-owner-invitation.js'; import crateOwnership from './models/crate-ownership.js'; import crate from './models/crate.js'; +import dependency from './models/dependency.js'; import keyword from './models/keyword.js'; import mswSession from './models/msw-session.js'; import team from './models/team.js'; @@ -19,6 +20,7 @@ export const db = factory({ crateOwnerInvitation, crateOwnership, crate, + dependency, keyword, mswSession, team, diff --git a/packages/crates-io-msw/models/dependency.js b/packages/crates-io-msw/models/dependency.js new file mode 100644 index 00000000000..ad8da357a9b --- /dev/null +++ b/packages/crates-io-msw/models/dependency.js @@ -0,0 +1,35 @@ +import { nullable, oneOf, primaryKey } from '@mswjs/data'; + +import { applyDefault } from '../utils/defaults.js'; + +const REQS = ['^0.1.0', '^2.1.3', '0.3.7', '~5.2.12']; + +export default { + id: primaryKey(Number), + + default_features: Boolean, + features: Array, + kind: String, + optional: Boolean, + req: String, + target: nullable(String), + + crate: oneOf('crate'), + version: oneOf('version'), + + preCreate(attrs, counter) { + applyDefault(attrs, 'id', () => counter); + applyDefault(attrs, 'default_features', () => counter % 4 === 3); + applyDefault(attrs, 'kind', () => (counter % 3 === 0 ? 'dev' : 'normal')); + applyDefault(attrs, 'optional', () => counter % 4 !== 3); + applyDefault(attrs, 'req', () => REQS[counter % REQS.length]); + applyDefault(attrs, 'target', () => null); + + if (!attrs.crate) { + throw new Error(`Missing \`crate\` relationship on \`dependency:${attrs.id}\``); + } + if (!attrs.version) { + throw new Error(`Missing \`version\` relationship on \`dependency:${attrs.id}\``); + } + }, +}; diff --git a/packages/crates-io-msw/models/dependency.test.js b/packages/crates-io-msw/models/dependency.test.js new file mode 100644 index 00000000000..7476b00339b --- /dev/null +++ b/packages/crates-io-msw/models/dependency.test.js @@ -0,0 +1,89 @@ +import { test } from 'vitest'; + +import { db } from '../index.js'; + +test('throws if `crate` is not set', ({ expect }) => { + let version = db.version.create({ crate: db.crate.create() }); + expect(() => db.dependency.create({ version })).toThrowErrorMatchingInlineSnapshot( + `[Error: Missing \`crate\` relationship on \`dependency:1\`]`, + ); +}); + +test('throws if `version` is not set', ({ expect }) => { + let crate = db.crate.create(); + expect(() => db.dependency.create({ crate })).toThrowErrorMatchingInlineSnapshot( + `[Error: Missing \`version\` relationship on \`dependency:1\`]`, + ); +}); + +test('happy path', ({ expect }) => { + let crate = db.crate.create(); + let version = db.version.create({ crate: db.crate.create() }); + let dependency = db.dependency.create({ crate, version }); + expect(dependency).toMatchInlineSnapshot(` + { + "crate": { + "_extra_downloads": [], + "badges": [], + "categories": [], + "created_at": "2010-06-16T21:30:45Z", + "description": "This is the description for the crate called "crate-1"", + "documentation": null, + "downloads": 37035, + "homepage": null, + "id": 1, + "keywords": [], + "name": "crate-1", + "recent_downloads": 321, + "repository": null, + "updated_at": "2017-02-24T12:34:56Z", + Symbol(type): "crate", + Symbol(primaryKey): "id", + }, + "default_features": false, + "features": [], + "id": 1, + "kind": "normal", + "optional": true, + "req": "^2.1.3", + "target": null, + "version": { + "crate": { + "_extra_downloads": [], + "badges": [], + "categories": [], + "created_at": "2010-06-16T21:30:45Z", + "description": "This is the description for the crate called "crate-2"", + "documentation": null, + "downloads": 74070, + "homepage": null, + "id": 2, + "keywords": [], + "name": "crate-2", + "recent_downloads": 1926, + "repository": null, + "updated_at": "2017-02-24T12:34:56Z", + Symbol(type): "crate", + Symbol(primaryKey): "id", + }, + "crate_size": 162963, + "created_at": "2010-06-16T21:30:45Z", + "downloads": 3702, + "features": {}, + "id": 1, + "license": "MIT", + "num": "1.0.0", + "publishedBy": null, + "readme": null, + "rust_version": null, + "updated_at": "2017-02-24T12:34:56Z", + "yank_message": null, + "yanked": false, + Symbol(type): "version", + Symbol(primaryKey): "id", + }, + Symbol(type): "dependency", + Symbol(primaryKey): "id", + } + `); +}); From 0abb0dd38b7e82226aefd959f18bfff3c9b35cc9 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 11:15:01 +0100 Subject: [PATCH 021/189] msw: Import `underscore()` fn from `@ember/string` --- packages/crates-io-msw/utils/strings.js | 7 +++++++ packages/crates-io-msw/utils/strings.test.js | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/crates-io-msw/utils/strings.js b/packages/crates-io-msw/utils/strings.js index 637a6129bf7..23de13c0237 100644 --- a/packages/crates-io-msw/utils/strings.js +++ b/packages/crates-io-msw/utils/strings.js @@ -4,3 +4,10 @@ export function dasherize(str) { .toLowerCase() .replace(/[ _]/g, '-'); } + +export function underscore(str) { + return str + .replace(/([a-z\d])([A-Z]+)/g, '$1_$2') + .replace(/-|\s+/g, '_') + .toLowerCase(); +} diff --git a/packages/crates-io-msw/utils/strings.test.js b/packages/crates-io-msw/utils/strings.test.js index daf414b06ec..a48491f5e0d 100644 --- a/packages/crates-io-msw/utils/strings.test.js +++ b/packages/crates-io-msw/utils/strings.test.js @@ -1,6 +1,6 @@ import { describe, test } from 'vitest'; -import { dasherize } from './strings.js'; +import { dasherize, underscore } from './strings.js'; describe('dasherize', () => { function assert(input, expected) { @@ -18,3 +18,19 @@ describe('dasherize', () => { assert('privateDocs/ownerInvoice', 'private-docs/owner-invoice'); assert('private_docs/owner_invoice', 'private-docs/owner-invoice'); }); + +describe('underscore', () => { + function assert(input, expected) { + test(input, ({ expect }) => { + expect(underscore(input)).toBe(expected); + }); + } + + assert('my favorite items', 'my_favorite_items'); + assert('css-class-name', 'css_class_name'); + assert('action_name', 'action_name'); + assert('innerHTML', 'inner_html'); + assert('PrivateDocs/OwnerInvoice', 'private_docs/owner_invoice'); + assert('privateDocs/ownerInvoice', 'private_docs/owner_invoice'); + assert('private-docs/owner-invoice', 'private_docs/owner_invoice'); +}); From 2d325e40eef3f34da1c680356b01fa022a1995d3 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 11:15:30 +0100 Subject: [PATCH 022/189] msw: Implement basic `serializeModel()` fn --- packages/crates-io-msw/utils/serializers.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/crates-io-msw/utils/serializers.js diff --git a/packages/crates-io-msw/utils/serializers.js b/packages/crates-io-msw/utils/serializers.js new file mode 100644 index 00000000000..d3b2d1824b7 --- /dev/null +++ b/packages/crates-io-msw/utils/serializers.js @@ -0,0 +1,9 @@ +import { underscore } from './strings.js'; + +export function serializeModel(model) { + let json = {}; + for (let [key, value] of Object.entries(model)) { + json[underscore(key)] = value; + } + return json; +} From 74245ce6861544c6eaa921d89007afcc7ad7340b Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 11:16:05 +0100 Subject: [PATCH 023/189] msw: Implement `category` serializer --- packages/crates-io-msw/serializers/category.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 packages/crates-io-msw/serializers/category.js diff --git a/packages/crates-io-msw/serializers/category.js b/packages/crates-io-msw/serializers/category.js new file mode 100644 index 00000000000..6bed770d803 --- /dev/null +++ b/packages/crates-io-msw/serializers/category.js @@ -0,0 +1,10 @@ +import { db } from '../index.js'; +import { serializeModel } from '../utils/serializers.js'; + +export function serializeCategory(category) { + let serialized = serializeModel(category); + + serialized.crates_cnt ??= db.crate.count({ where: { categories: { id: { equals: category.id } } } }); + + return serialized; +} From a931e55a5108b0c066a7f307b8c71aa3758d197d Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 12:51:41 +0100 Subject: [PATCH 024/189] msw: Implement `keyword` serializer --- packages/crates-io-msw/serializers/keyword.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 packages/crates-io-msw/serializers/keyword.js diff --git a/packages/crates-io-msw/serializers/keyword.js b/packages/crates-io-msw/serializers/keyword.js new file mode 100644 index 00000000000..fce2cb0122b --- /dev/null +++ b/packages/crates-io-msw/serializers/keyword.js @@ -0,0 +1,10 @@ +import { db } from '../index.js'; +import { serializeModel } from '../utils/serializers.js'; + +export function serializeKeyword(keyword) { + let serialized = serializeModel(keyword); + + serialized.crates_cnt = db.crate.count({ where: { keywords: { id: { equals: keyword.id } } } }); + + return serialized; +} From 8403d00b2eb1ed524fe97a178eabedcef6ac3efc Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 13:26:32 +0100 Subject: [PATCH 025/189] msw: Implement `team` serializer --- packages/crates-io-msw/serializers/team.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/crates-io-msw/serializers/team.js diff --git a/packages/crates-io-msw/serializers/team.js b/packages/crates-io-msw/serializers/team.js new file mode 100644 index 00000000000..3897f952f74 --- /dev/null +++ b/packages/crates-io-msw/serializers/team.js @@ -0,0 +1,9 @@ +import { serializeModel } from '../utils/serializers.js'; + +export function serializeTeam(team) { + let serialized = serializeModel(team); + + delete serialized.org; + + return serialized; +} From a45555020d27e27f49743e1e3048890c479e4e25 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 15:26:37 +0100 Subject: [PATCH 026/189] msw: Implement `user` serializer --- packages/crates-io-msw/serializers/user.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 packages/crates-io-msw/serializers/user.js diff --git a/packages/crates-io-msw/serializers/user.js b/packages/crates-io-msw/serializers/user.js new file mode 100644 index 00000000000..9f3725977af --- /dev/null +++ b/packages/crates-io-msw/serializers/user.js @@ -0,0 +1,19 @@ +import { serializeModel } from '../utils/serializers.js'; + +export function serializeUser(user, { removePrivateData = true } = {}) { + let serialized = serializeModel(user); + + if (removePrivateData) { + delete serialized.email; + delete serialized.email_verified; + delete serialized.is_admin; + delete serialized.publish_notifications; + } else { + serialized.email_verification_sent = serialized.email_verified || Boolean(serialized.email_verification_token); + } + + delete serialized.email_verification_token; + delete serialized.followed_crates; + + return serialized; +} From aa45a7cf4673f196b1a1d9969283cf4c9650a71e Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 22 Jan 2025 11:44:06 +0100 Subject: [PATCH 027/189] msw: Implement `invite` serializer --- packages/crates-io-msw/serializers/invite.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 packages/crates-io-msw/serializers/invite.js diff --git a/packages/crates-io-msw/serializers/invite.js b/packages/crates-io-msw/serializers/invite.js new file mode 100644 index 00000000000..0e5ef7a1da0 --- /dev/null +++ b/packages/crates-io-msw/serializers/invite.js @@ -0,0 +1,18 @@ +import { serializeModel } from '../utils/serializers.js'; + +export function serializeInvite(invite) { + let serialized = serializeModel(invite); + + serialized.crate_id = serialized.crate.id; + serialized.crate_name = serialized.crate.name; + serialized.invitee_id = serialized.invitee.id; + serialized.inviter_id = serialized.inviter.id; + + delete serialized.id; + delete serialized.token; + delete serialized.crate; + delete serialized.invitee; + delete serialized.inviter; + + return serialized; +} From 990f853f516a9a70a33584db83ce0e070153e609 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 22 Jan 2025 14:57:44 +0100 Subject: [PATCH 028/189] msw: Implement `api-token` serializer --- .../crates-io-msw/serializers/api-token.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 packages/crates-io-msw/serializers/api-token.js diff --git a/packages/crates-io-msw/serializers/api-token.js b/packages/crates-io-msw/serializers/api-token.js new file mode 100644 index 00000000000..b0de50c1876 --- /dev/null +++ b/packages/crates-io-msw/serializers/api-token.js @@ -0,0 +1,24 @@ +import { serializeModel } from '../utils/serializers.js'; + +export function serializeApiToken(token, { forCreate = false } = {}) { + let serialized = serializeModel(token); + + if (serialized.created_at) { + serialized.created_at = new Date(serialized.created_at).toISOString(); + } + if (serialized.expired_at) { + serialized.expired_at = new Date(serialized.expired_at).toISOString(); + } + if (serialized.last_used_at) { + serialized.last_used_at = new Date(serialized.last_used_at).toISOString(); + } + + delete serialized.user; + + if (!forCreate) { + delete serialized.revoked; + delete serialized.token; + } + + return serialized; +} From 70eabf280b9c34e2cd37987d296ce11385a06f84 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 13:16:07 +0100 Subject: [PATCH 029/189] msw: Implement `crate` serializer --- packages/crates-io-msw/package.json | 3 +- packages/crates-io-msw/serializers/crate.js | 64 +++++++++++++++++++++ packages/crates-io-msw/utils/dates.js | 5 ++ pnpm-lock.yaml | 3 + 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/serializers/crate.js create mode 100644 packages/crates-io-msw/utils/dates.js diff --git a/packages/crates-io-msw/package.json b/packages/crates-io-msw/package.json index 71680203762..8aa6ba40433 100644 --- a/packages/crates-io-msw/package.json +++ b/packages/crates-io-msw/package.json @@ -19,7 +19,8 @@ }, "dependencies": { "@mswjs/data": "^0.16.2", - "msw": "^2.7.0" + "msw": "^2.7.0", + "semver": "^7.6.3" }, "devDependencies": { "happy-dom": "16.5.3", diff --git a/packages/crates-io-msw/serializers/crate.js b/packages/crates-io-msw/serializers/crate.js new file mode 100644 index 00000000000..d6004e5a5df --- /dev/null +++ b/packages/crates-io-msw/serializers/crate.js @@ -0,0 +1,64 @@ +import prerelease from 'semver/functions/prerelease.js'; +import semverSort from 'semver/functions/rsort.js'; + +import { db } from '../index.js'; +import { compareDates } from '../utils/dates.js'; +import { serializeModel } from '../utils/serializers.js'; + +export function serializeCrate( + crate, + { calculateVersions = true, includeCategories = false, includeKeywords = false, includeVersions = false } = {}, +) { + let versions = db.version.findMany({ where: { crate: { id: { equals: crate.id } } } }); + if (versions.length === 0) { + throw new Error(`crate \`${crate.name}\` has no associated versions`); + } + + let versionsByNum = Object.fromEntries(versions.map(it => [it.num, it])); + let versionNums = Object.keys(versionsByNum); + semverSort(versionNums, { loose: true }); + + let serialized = serializeModel(crate); + + serialized.id = crate.name; + + serialized.default_version = + versionNums.find(it => !prerelease(it, { loose: true }) && !versionsByNum[it].yanked) ?? + versionNums.find(it => !versionsByNum[it].yanked) ?? + versionNums[0]; + + serialized.yanked = versionsByNum[serialized.default_version]?.yanked ?? false; + + serialized.links = { + owner_user: `/api/v1/crates/${crate.name}/owner_user`, + owner_team: `/api/v1/crates/${crate.name}/owner_team`, + reverse_dependencies: `/api/v1/crates/${crate.name}/reverse_dependencies`, + version_downloads: `/api/v1/crates/${crate.name}/downloads`, + versions: `/api/v1/crates/${crate.name}/versions`, + }; + + if (calculateVersions) { + let unyankedVersions = versionNums.filter(it => !versionsByNum[it].yanked); + serialized.max_version = unyankedVersions[0] ?? '0.0.0'; + serialized.max_stable_version = unyankedVersions.find(it => !prerelease(it, { loose: true })) ?? null; + + let newestVersions = versions.filter(it => !it.yanked).sort((a, b) => compareDates(b.updated_at, a.updated_at)); + serialized.newest_version = newestVersions[0]?.num ?? '0.0.0'; + } else { + serialized.max_version = '0.0.0'; + serialized.newest_version = '0.0.0'; + serialized.max_stable_version = null; + } + + serialized.categories = includeCategories ? crate.categories.map(c => c.id) : null; + serialized.keywords = includeKeywords ? crate.keywords.map(k => k.id) : null; + serialized.versions = includeVersions ? versions.map(k => k.id) : null; + + delete serialized._extra_downloads; + + return serialized; +} + +export function compare(a, b) { + return a < b ? -1 : a > b ? 1 : 0; +} diff --git a/packages/crates-io-msw/utils/dates.js b/packages/crates-io-msw/utils/dates.js new file mode 100644 index 00000000000..0f73197481b --- /dev/null +++ b/packages/crates-io-msw/utils/dates.js @@ -0,0 +1,5 @@ +export function compareDates(a, b) { + let aDate = new Date(a); + let bDate = new Date(b); + return aDate < bDate ? -1 : aDate > bDate ? 1 : 0; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d70212c2c7f..30771906887 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -310,6 +310,9 @@ importers: msw: specifier: ^2.7.0 version: 2.7.0(@types/node@22.10.10)(typescript@5.7.3) + semver: + specifier: ^7.6.3 + version: 7.6.3 devDependencies: happy-dom: specifier: 16.5.3 From c85158f524f9af286337b28f08c30fc0ad7e7610 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 14:24:15 +0100 Subject: [PATCH 030/189] msw: Implement `version` serializer --- packages/crates-io-msw/serializers/version.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 packages/crates-io-msw/serializers/version.js diff --git a/packages/crates-io-msw/serializers/version.js b/packages/crates-io-msw/serializers/version.js new file mode 100644 index 00000000000..3be99dcbaee --- /dev/null +++ b/packages/crates-io-msw/serializers/version.js @@ -0,0 +1,21 @@ +import { serializeModel } from '../utils/serializers.js'; +import { serializeUser } from './user.js'; + +export function serializeVersion(version) { + let serialized = serializeModel(version); + + serialized.crate = version.crate.name; + serialized.dl_path = `/api/v1/crates/${version.crate.name}/${version.num}/download`; + serialized.readme_path = `/api/v1/crates/${version.crate.name}/${version.num}/readme`; + + serialized.links = { + dependencies: `/api/v1/crates/${version.crate.name}/${version.num}/dependencies`, + version_downloads: `/api/v1/crates/${version.crate.name}/${version.num}/downloads`, + }; + + serialized.published_by = version.publishedBy ? serializeUser(version.publishedBy) : null; + + delete serialized.readme; + + return serialized; +} From 404e209c8f4250de9fbdd4e198633591bc763169 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 16:28:13 +0100 Subject: [PATCH 031/189] msw: Implement `dependency` serializer --- packages/crates-io-msw/serializers/dependency.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 packages/crates-io-msw/serializers/dependency.js diff --git a/packages/crates-io-msw/serializers/dependency.js b/packages/crates-io-msw/serializers/dependency.js new file mode 100644 index 00000000000..5fffd138c7b --- /dev/null +++ b/packages/crates-io-msw/serializers/dependency.js @@ -0,0 +1,13 @@ +import { serializeModel } from '../utils/serializers.js'; + +export function serializeDependency(dependency) { + let serialized = serializeModel(dependency); + + serialized.crate_id = dependency.crate.name; + serialized.version_id = dependency.version.id; + + delete serialized.crate; + delete serialized.version; + + return serialized; +} From 7dd2952ddff963666c14e1a3f93e39b07b919fac Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 13:13:29 +0100 Subject: [PATCH 032/189] msw: Implement `getSession()` helper fn --- packages/crates-io-msw/utils/session.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 packages/crates-io-msw/utils/session.js diff --git a/packages/crates-io-msw/utils/session.js b/packages/crates-io-msw/utils/session.js new file mode 100644 index 00000000000..31e0d27f77d --- /dev/null +++ b/packages/crates-io-msw/utils/session.js @@ -0,0 +1,12 @@ +import { db } from '../index.js'; + +export function getSession() { + let session = db.mswSession.findFirst({}); + if (!session) { + return {}; + } + + let user = session.user; + + return { session, user }; +} From 2ebe6789e2007aeff5374359641d4d2f14e0f4dd Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 22 Jan 2025 11:15:27 +0100 Subject: [PATCH 033/189] msw: Implement docs.rs request handler --- packages/crates-io-msw/handlers/docs-rs.js | 7 +++++++ packages/crates-io-msw/handlers/docs-rs.test.js | 7 +++++++ packages/crates-io-msw/index.js | 3 ++- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/docs-rs.js create mode 100644 packages/crates-io-msw/handlers/docs-rs.test.js diff --git a/packages/crates-io-msw/handlers/docs-rs.js b/packages/crates-io-msw/handlers/docs-rs.js new file mode 100644 index 00000000000..0882c2b58a1 --- /dev/null +++ b/packages/crates-io-msw/handlers/docs-rs.js @@ -0,0 +1,7 @@ +import { http, HttpResponse } from 'msw'; + +export default [ + http.get('https://docs.rs/crate/:crate/:version/status.json', () => { + return HttpResponse.json({}); + }), +]; diff --git a/packages/crates-io-msw/handlers/docs-rs.test.js b/packages/crates-io-msw/handlers/docs-rs.test.js new file mode 100644 index 00000000000..8f4b6fc12b0 --- /dev/null +++ b/packages/crates-io-msw/handlers/docs-rs.test.js @@ -0,0 +1,7 @@ +import { assert, test } from 'vitest'; + +test('returns 200 OK and an empty object', async function () { + let response = await fetch('https://docs.rs/crate/foo/0.0.0/status.json'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), {}); +}); diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 4a5b547105a..3a64b475592 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,3 +1,4 @@ +import docsRsHandlers from './handlers/docs-rs.js'; import apiToken from './models/api-token.js'; import category from './models/category.js'; import crateOwnerInvitation from './models/crate-owner-invitation.js'; @@ -12,7 +13,7 @@ import versionDownload from './models/version-download.js'; import version from './models/version.js'; import { factory } from './utils/factory.js'; -export const handlers = []; +export const handlers = [...docsRsHandlers]; export const db = factory({ apiToken, From 4c0ef9beab3209274fe4f7145ffcec89c1208c7e Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 11:16:37 +0100 Subject: [PATCH 034/189] msw: Implement `GET /api/v1/categories` request handler --- packages/crates-io-msw/handlers/categories.js | 3 + .../crates-io-msw/handlers/categories/list.js | 14 +++ .../handlers/categories/list.test.js | 86 +++++++++++++++++++ packages/crates-io-msw/index.js | 3 +- packages/crates-io-msw/utils/handlers.js | 14 +++ 5 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/categories.js create mode 100644 packages/crates-io-msw/handlers/categories/list.js create mode 100644 packages/crates-io-msw/handlers/categories/list.test.js create mode 100644 packages/crates-io-msw/utils/handlers.js diff --git a/packages/crates-io-msw/handlers/categories.js b/packages/crates-io-msw/handlers/categories.js new file mode 100644 index 00000000000..5a12878a8c0 --- /dev/null +++ b/packages/crates-io-msw/handlers/categories.js @@ -0,0 +1,3 @@ +import listCategories from './categories/list.js'; + +export default [listCategories]; diff --git a/packages/crates-io-msw/handlers/categories/list.js b/packages/crates-io-msw/handlers/categories/list.js new file mode 100644 index 00000000000..e0d133d170c --- /dev/null +++ b/packages/crates-io-msw/handlers/categories/list.js @@ -0,0 +1,14 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeCategory } from '../../serializers/category.js'; +import { pageParams } from '../../utils/handlers.js'; + +export default http.get('/api/v1/categories', ({ request }) => { + let { skip, take } = pageParams(request); + + let categories = db.category.findMany({ skip, take, orderBy: { category: 'asc' } }); + let total = db.category.count(); + + return HttpResponse.json({ categories: categories.map(c => serializeCategory(c)), meta: { total } }); +}); diff --git a/packages/crates-io-msw/handlers/categories/list.test.js b/packages/crates-io-msw/handlers/categories/list.test.js new file mode 100644 index 00000000000..3c19ba11e9e --- /dev/null +++ b/packages/crates-io-msw/handlers/categories/list.test.js @@ -0,0 +1,86 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('empty case', async function () { + let response = await fetch('/api/v1/categories'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + categories: [], + meta: { + total: 0, + }, + }); +}); + +test('returns a paginated categories list', async function () { + db.category.create({ + category: 'no-std', + description: 'Crates that are able to function without the Rust standard library.', + }); + Array.from({ length: 2 }, () => db.category.create()); + + let response = await fetch('/api/v1/categories'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + categories: [ + { + id: 'category-2', + category: 'Category 2', + crates_cnt: 0, + created_at: '2010-06-16T21:30:45Z', + description: 'This is the description for the category called "Category 2"', + slug: 'category-2', + }, + { + id: 'category-3', + category: 'Category 3', + crates_cnt: 0, + created_at: '2010-06-16T21:30:45Z', + description: 'This is the description for the category called "Category 3"', + slug: 'category-3', + }, + { + id: 'no-std', + category: 'no-std', + crates_cnt: 0, + created_at: '2010-06-16T21:30:45Z', + description: 'Crates that are able to function without the Rust standard library.', + slug: 'no-std', + }, + ], + meta: { + total: 3, + }, + }); +}); + +test('never returns more than 10 results', async function () { + Array.from({ length: 25 }, () => db.category.create()); + + let response = await fetch('/api/v1/categories'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.strictEqual(responsePayload.categories.length, 10); + assert.strictEqual(responsePayload.meta.total, 25); +}); + +test('supports `page` and `per_page` parameters', async function () { + Array.from({ length: 25 }, (_, i) => + db.category.create({ + category: `cat-${String(i + 1).padStart(2, '0')}`, + }), + ); + + let response = await fetch('/api/v1/categories?page=2&per_page=5'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.strictEqual(responsePayload.categories.length, 5); + assert.deepEqual( + responsePayload.categories.map(it => it.id), + ['cat-06', 'cat-07', 'cat-08', 'cat-09', 'cat-10'], + ); + assert.strictEqual(responsePayload.meta.total, 25); +}); diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 3a64b475592..3770b49e5be 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,3 +1,4 @@ +import categoryHandlers from './handlers/categories.js'; import docsRsHandlers from './handlers/docs-rs.js'; import apiToken from './models/api-token.js'; import category from './models/category.js'; @@ -13,7 +14,7 @@ import versionDownload from './models/version-download.js'; import version from './models/version.js'; import { factory } from './utils/factory.js'; -export const handlers = [...docsRsHandlers]; +export const handlers = [...categoryHandlers, ...docsRsHandlers]; export const db = factory({ apiToken, diff --git a/packages/crates-io-msw/utils/handlers.js b/packages/crates-io-msw/utils/handlers.js new file mode 100644 index 00000000000..109713afd3d --- /dev/null +++ b/packages/crates-io-msw/utils/handlers.js @@ -0,0 +1,14 @@ +export function pageParams(request) { + let url = new URL(request.url); + + let page = parseInt(url.searchParams.get('page') || '1'); + let perPage = parseInt(url.searchParams.get('per_page') || '10'); + + let start = (page - 1) * perPage; + let end = start + perPage; + + let skip = start; + let take = perPage; + + return { page, perPage, start, end, skip, take }; +} From 36170ae40df24b680beee431108ae989f5e72a2d Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 11:21:47 +0100 Subject: [PATCH 035/189] msw: Implement `GET /api/v1/category_slugs` request handler --- packages/crates-io-msw/handlers/categories.js | 3 +- .../handlers/categories/list-slugs.js | 10 ++++ .../handlers/categories/list-slugs.test.js | 49 +++++++++++++++++++ .../crates-io-msw/serializers/category.js | 8 +++ 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/categories/list-slugs.js create mode 100644 packages/crates-io-msw/handlers/categories/list-slugs.test.js diff --git a/packages/crates-io-msw/handlers/categories.js b/packages/crates-io-msw/handlers/categories.js index 5a12878a8c0..fb933e599a2 100644 --- a/packages/crates-io-msw/handlers/categories.js +++ b/packages/crates-io-msw/handlers/categories.js @@ -1,3 +1,4 @@ +import listCategorySlugs from './categories/list-slugs.js'; import listCategories from './categories/list.js'; -export default [listCategories]; +export default [listCategories, listCategorySlugs]; diff --git a/packages/crates-io-msw/handlers/categories/list-slugs.js b/packages/crates-io-msw/handlers/categories/list-slugs.js new file mode 100644 index 00000000000..e15bdf78e2f --- /dev/null +++ b/packages/crates-io-msw/handlers/categories/list-slugs.js @@ -0,0 +1,10 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeCategorySlug } from '../../serializers/category.js'; + +export default http.get('/api/v1/category_slugs', () => { + let allCategories = db.category.findMany({ orderBy: { category: 'asc' } }); + + return HttpResponse.json({ category_slugs: allCategories.map(c => serializeCategorySlug(c)) }); +}); diff --git a/packages/crates-io-msw/handlers/categories/list-slugs.test.js b/packages/crates-io-msw/handlers/categories/list-slugs.test.js new file mode 100644 index 00000000000..f5d97e77b69 --- /dev/null +++ b/packages/crates-io-msw/handlers/categories/list-slugs.test.js @@ -0,0 +1,49 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('empty case', async function () { + let response = await fetch('/api/v1/category_slugs'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + category_slugs: [], + }); +}); + +test('returns a category slugs list', async function () { + db.category.create({ + category: 'no-std', + description: 'Crates that are able to function without the Rust standard library.', + }); + Array.from({ length: 2 }, () => db.category.create()); + + let response = await fetch('/api/v1/category_slugs'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + category_slugs: [ + { + description: 'This is the description for the category called "Category 2"', + id: 'category-2', + slug: 'category-2', + }, + { + description: 'This is the description for the category called "Category 3"', + id: 'category-3', + slug: 'category-3', + }, + { + description: 'Crates that are able to function without the Rust standard library.', + id: 'no-std', + slug: 'no-std', + }, + ], + }); +}); + +test('has no pagination', async function () { + Array.from({ length: 25 }, () => db.category.create()); + + let response = await fetch('/api/v1/category_slugs'); + assert.strictEqual(response.status, 200); + assert.strictEqual((await response.json()).category_slugs.length, 25); +}); diff --git a/packages/crates-io-msw/serializers/category.js b/packages/crates-io-msw/serializers/category.js index 6bed770d803..5960b99038f 100644 --- a/packages/crates-io-msw/serializers/category.js +++ b/packages/crates-io-msw/serializers/category.js @@ -8,3 +8,11 @@ export function serializeCategory(category) { return serialized; } + +export function serializeCategorySlug(category) { + return { + id: category.id, + slug: category.slug, + description: category.description, + }; +} From 571ff2efb636949830da90cd03cf396da78dddf8 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 11:33:46 +0100 Subject: [PATCH 036/189] msw: Implement `GET /api/v1/categories/:category_id` request handler --- packages/crates-io-msw/handlers/categories.js | 3 +- .../crates-io-msw/handlers/categories/get.js | 15 ++++++ .../handlers/categories/get.test.js | 49 +++++++++++++++++++ packages/crates-io-msw/utils/handlers.js | 6 +++ 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/categories/get.js create mode 100644 packages/crates-io-msw/handlers/categories/get.test.js diff --git a/packages/crates-io-msw/handlers/categories.js b/packages/crates-io-msw/handlers/categories.js index fb933e599a2..afeffe580ac 100644 --- a/packages/crates-io-msw/handlers/categories.js +++ b/packages/crates-io-msw/handlers/categories.js @@ -1,4 +1,5 @@ +import getCategory from './categories/get.js'; import listCategorySlugs from './categories/list-slugs.js'; import listCategories from './categories/list.js'; -export default [listCategories, listCategorySlugs]; +export default [listCategories, getCategory, listCategorySlugs]; diff --git a/packages/crates-io-msw/handlers/categories/get.js b/packages/crates-io-msw/handlers/categories/get.js new file mode 100644 index 00000000000..902a3eb8232 --- /dev/null +++ b/packages/crates-io-msw/handlers/categories/get.js @@ -0,0 +1,15 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeCategory } from '../../serializers/category.js'; +import { notFound } from '../../utils/handlers.js'; + +export default http.get('/api/v1/categories/:category_id', ({ params }) => { + let catId = params.category_id; + let category = db.category.findFirst({ where: { id: { equals: catId } } }); + if (!category) { + return notFound(); + } + + return HttpResponse.json({ category: serializeCategory(category) }); +}); diff --git a/packages/crates-io-msw/handlers/categories/get.test.js b/packages/crates-io-msw/handlers/categories/get.test.js new file mode 100644 index 00000000000..636854d1b4c --- /dev/null +++ b/packages/crates-io-msw/handlers/categories/get.test.js @@ -0,0 +1,49 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 404 for unknown categories', async function () { + let response = await fetch('/api/v1/categories/foo'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('returns a category object for known categories', async function () { + db.category.create({ + category: 'no-std', + description: 'Crates that are able to function without the Rust standard library.', + }); + + let response = await fetch('/api/v1/categories/no-std'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + category: { + id: 'no-std', + category: 'no-std', + crates_cnt: 0, + created_at: '2010-06-16T21:30:45Z', + description: 'Crates that are able to function without the Rust standard library.', + slug: 'no-std', + }, + }); +}); + +test('calculates `crates_cnt` correctly', async function () { + let cli = db.category.create({ category: 'cli' }); + Array.from({ length: 7 }, () => db.crate.create({ categories: [cli] })); + let notCli = db.category.create({ category: 'not-cli' }); + Array.from({ length: 3 }, () => db.crate.create({ categories: [notCli] })); + + let response = await fetch('/api/v1/categories/cli'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + category: { + category: 'cli', + crates_cnt: 7, + created_at: '2010-06-16T21:30:45Z', + description: 'This is the description for the category called "cli"', + id: 'cli', + slug: 'cli', + }, + }); +}); diff --git a/packages/crates-io-msw/utils/handlers.js b/packages/crates-io-msw/utils/handlers.js index 109713afd3d..f803055a0e4 100644 --- a/packages/crates-io-msw/utils/handlers.js +++ b/packages/crates-io-msw/utils/handlers.js @@ -1,3 +1,9 @@ +import { HttpResponse } from 'msw'; + +export function notFound() { + return HttpResponse.json({ errors: [{ detail: 'Not Found' }] }, { status: 404 }); +} + export function pageParams(request) { let url = new URL(request.url); From ef202e452e90e92ba6257b2a67497bee71a827c2 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 12:52:16 +0100 Subject: [PATCH 037/189] msw: Implement `GET /api/v1/keywords` request handler --- packages/crates-io-msw/handlers/keywords.js | 3 + .../crates-io-msw/handlers/keywords/list.js | 14 ++++ .../handlers/keywords/list.test.js | 70 +++++++++++++++++++ packages/crates-io-msw/index.js | 3 +- 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/keywords.js create mode 100644 packages/crates-io-msw/handlers/keywords/list.js create mode 100644 packages/crates-io-msw/handlers/keywords/list.test.js diff --git a/packages/crates-io-msw/handlers/keywords.js b/packages/crates-io-msw/handlers/keywords.js new file mode 100644 index 00000000000..69b4c7696ef --- /dev/null +++ b/packages/crates-io-msw/handlers/keywords.js @@ -0,0 +1,3 @@ +import listKeywords from './keywords/list.js'; + +export default [listKeywords]; diff --git a/packages/crates-io-msw/handlers/keywords/list.js b/packages/crates-io-msw/handlers/keywords/list.js new file mode 100644 index 00000000000..07b750919e0 --- /dev/null +++ b/packages/crates-io-msw/handlers/keywords/list.js @@ -0,0 +1,14 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeKeyword } from '../../serializers/keyword.js'; +import { pageParams } from '../../utils/handlers.js'; + +export default http.get('/api/v1/keywords', ({ request }) => { + let { skip, take } = pageParams(request); + + let keywords = db.keyword.findMany({ skip, take, orderBy: { crates_cnt: 'desc' } }); + let total = db.keyword.count(); + + return HttpResponse.json({ keywords: keywords.map(k => serializeKeyword(k)), meta: { total } }); +}); diff --git a/packages/crates-io-msw/handlers/keywords/list.test.js b/packages/crates-io-msw/handlers/keywords/list.test.js new file mode 100644 index 00000000000..a127b087ea7 --- /dev/null +++ b/packages/crates-io-msw/handlers/keywords/list.test.js @@ -0,0 +1,70 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('empty case', async function () { + let response = await fetch('/api/v1/keywords'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + keywords: [], + meta: { + total: 0, + }, + }); +}); + +test('returns a paginated keywords list', async function () { + db.keyword.create({ keyword: 'api' }); + Array.from({ length: 2 }, () => db.keyword.create()); + + let response = await fetch('/api/v1/keywords'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + keywords: [ + { + id: 'api', + crates_cnt: 0, + keyword: 'api', + }, + { + id: 'keyword-2', + crates_cnt: 0, + keyword: 'keyword-2', + }, + { + id: 'keyword-3', + crates_cnt: 0, + keyword: 'keyword-3', + }, + ], + meta: { + total: 3, + }, + }); +}); + +test('never returns more than 10 results', async function () { + Array.from({ length: 25 }, () => db.keyword.create()); + + let response = await fetch('/api/v1/keywords'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.strictEqual(responsePayload.keywords.length, 10); + assert.strictEqual(responsePayload.meta.total, 25); +}); + +test('supports `page` and `per_page` parameters', async function () { + Array.from({ length: 25 }, (_, i) => db.keyword.create({ keyword: `k${String(i + 1).padStart(2, '0')}` })); + + let response = await fetch('/api/v1/keywords?page=2&per_page=5'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.strictEqual(responsePayload.keywords.length, 5); + assert.deepEqual( + responsePayload.keywords.map(it => it.id), + ['k06', 'k07', 'k08', 'k09', 'k10'], + ); + assert.strictEqual(responsePayload.meta.total, 25); +}); diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 3770b49e5be..6898fca5d99 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,5 +1,6 @@ import categoryHandlers from './handlers/categories.js'; import docsRsHandlers from './handlers/docs-rs.js'; +import keywordHandlers from './handlers/keywords.js'; import apiToken from './models/api-token.js'; import category from './models/category.js'; import crateOwnerInvitation from './models/crate-owner-invitation.js'; @@ -14,7 +15,7 @@ import versionDownload from './models/version-download.js'; import version from './models/version.js'; import { factory } from './utils/factory.js'; -export const handlers = [...categoryHandlers, ...docsRsHandlers]; +export const handlers = [...categoryHandlers, ...docsRsHandlers, ...keywordHandlers]; export const db = factory({ apiToken, From 4c6eb08767687d92893bf8af7ee6b105690dffec Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 12:52:29 +0100 Subject: [PATCH 038/189] msw: Implement `GET /api/v1/keywords/:keyword_id` request handler --- packages/crates-io-msw/handlers/keywords.js | 3 +- .../crates-io-msw/handlers/keywords/get.js | 15 +++++++ .../handlers/keywords/get.test.js | 40 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/keywords/get.js create mode 100644 packages/crates-io-msw/handlers/keywords/get.test.js diff --git a/packages/crates-io-msw/handlers/keywords.js b/packages/crates-io-msw/handlers/keywords.js index 69b4c7696ef..2e668293399 100644 --- a/packages/crates-io-msw/handlers/keywords.js +++ b/packages/crates-io-msw/handlers/keywords.js @@ -1,3 +1,4 @@ +import getKeyword from './keywords/get.js'; import listKeywords from './keywords/list.js'; -export default [listKeywords]; +export default [listKeywords, getKeyword]; diff --git a/packages/crates-io-msw/handlers/keywords/get.js b/packages/crates-io-msw/handlers/keywords/get.js new file mode 100644 index 00000000000..8432e00d650 --- /dev/null +++ b/packages/crates-io-msw/handlers/keywords/get.js @@ -0,0 +1,15 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeKeyword } from '../../serializers/keyword.js'; +import { notFound } from '../../utils/handlers.js'; + +export default http.get('/api/v1/keywords/:keyword_id', ({ params }) => { + let keywordId = params.keyword_id; + let keyword = db.keyword.findFirst({ where: { id: { equals: keywordId } } }); + if (!keyword) { + return notFound(); + } + + return HttpResponse.json({ keyword: serializeKeyword(keyword) }); +}); diff --git a/packages/crates-io-msw/handlers/keywords/get.test.js b/packages/crates-io-msw/handlers/keywords/get.test.js new file mode 100644 index 00000000000..7e7c501bebe --- /dev/null +++ b/packages/crates-io-msw/handlers/keywords/get.test.js @@ -0,0 +1,40 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 404 for unknown keywords', async function () { + let response = await fetch('/api/v1/keywords/foo'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('returns a keyword object for known keywords', async function () { + db.keyword.create({ keyword: 'cli' }); + + let response = await fetch('/api/v1/keywords/cli'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + keyword: { + id: 'cli', + crates_cnt: 0, + keyword: 'cli', + }, + }); +}); + +test('calculates `crates_cnt` correctly', async function () { + let cli = db.keyword.create({ keyword: 'cli' }); + Array.from({ length: 7 }, () => db.crate.create({ keywords: [cli] })); + let notCli = db.keyword.create({ keyword: 'not-cli' }); + Array.from({ length: 3 }, () => db.crate.create({ keywords: [notCli] })); + + let response = await fetch('/api/v1/keywords/cli'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + keyword: { + id: 'cli', + crates_cnt: 7, + keyword: 'cli', + }, + }); +}); From ab1b1d5cc88782352c3a85d20b58bb14d9856ced Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 13:12:34 +0100 Subject: [PATCH 039/189] msw: Implement `DELETE /api/private/session` request handler --- packages/crates-io-msw/handlers/sessions.js | 3 +++ .../crates-io-msw/handlers/sessions/delete.js | 8 +++++++ .../handlers/sessions/delete.test.js | 22 +++++++++++++++++++ packages/crates-io-msw/index.js | 3 ++- 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/sessions.js create mode 100644 packages/crates-io-msw/handlers/sessions/delete.js create mode 100644 packages/crates-io-msw/handlers/sessions/delete.test.js diff --git a/packages/crates-io-msw/handlers/sessions.js b/packages/crates-io-msw/handlers/sessions.js new file mode 100644 index 00000000000..34c40fb0d67 --- /dev/null +++ b/packages/crates-io-msw/handlers/sessions.js @@ -0,0 +1,3 @@ +import deleteSession from './sessions/delete.js'; + +export default [deleteSession]; diff --git a/packages/crates-io-msw/handlers/sessions/delete.js b/packages/crates-io-msw/handlers/sessions/delete.js new file mode 100644 index 00000000000..343be4a7437 --- /dev/null +++ b/packages/crates-io-msw/handlers/sessions/delete.js @@ -0,0 +1,8 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; + +export default http.delete('/api/private/session', () => { + db.mswSession.deleteMany({}); + return HttpResponse.json({ ok: true }); +}); diff --git a/packages/crates-io-msw/handlers/sessions/delete.test.js b/packages/crates-io-msw/handlers/sessions/delete.test.js new file mode 100644 index 00000000000..ea79f9a4ddf --- /dev/null +++ b/packages/crates-io-msw/handlers/sessions/delete.test.js @@ -0,0 +1,22 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 200 when authenticated', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/private/session', { method: 'DELETE' }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { ok: true }); + + assert.notOk(db.mswSession.findFirst({})); +}); + +test('returns 200 when unauthenticated', async function () { + let response = await fetch('/api/private/session', { method: 'DELETE' }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { ok: true }); + + assert.notOk(db.mswSession.findFirst({})); +}); diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 6898fca5d99..138ebf9a0bc 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,6 +1,7 @@ import categoryHandlers from './handlers/categories.js'; import docsRsHandlers from './handlers/docs-rs.js'; import keywordHandlers from './handlers/keywords.js'; +import sessionHandlers from './handlers/sessions.js'; import apiToken from './models/api-token.js'; import category from './models/category.js'; import crateOwnerInvitation from './models/crate-owner-invitation.js'; @@ -15,7 +16,7 @@ import versionDownload from './models/version-download.js'; import version from './models/version.js'; import { factory } from './utils/factory.js'; -export const handlers = [...categoryHandlers, ...docsRsHandlers, ...keywordHandlers]; +export const handlers = [...categoryHandlers, ...docsRsHandlers, ...keywordHandlers, ...sessionHandlers]; export const db = factory({ apiToken, From 14839ecde6d2ed20ad80e62ca3c3faf060a6a3be Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 15 Jan 2025 14:09:34 +0100 Subject: [PATCH 040/189] msw: Implement `GET /api/v1/site_metadata` request handler --- packages/crates-io-msw/handlers/metadata.js | 13 +++++++++++++ packages/crates-io-msw/handlers/metadata.test.js | 13 +++++++++++++ packages/crates-io-msw/index.js | 9 ++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/metadata.js create mode 100644 packages/crates-io-msw/handlers/metadata.test.js diff --git a/packages/crates-io-msw/handlers/metadata.js b/packages/crates-io-msw/handlers/metadata.js new file mode 100644 index 00000000000..bdc76af739e --- /dev/null +++ b/packages/crates-io-msw/handlers/metadata.js @@ -0,0 +1,13 @@ +import { http, HttpResponse } from 'msw'; + +const EXAMPLE_SHA1 = '5048d31943118c6d67359bd207d307c854e82f45'; + +export default [ + http.get('/api/v1/site_metadata', () => { + return HttpResponse.json({ + commit: EXAMPLE_SHA1, + deployed_sha: EXAMPLE_SHA1, + read_only: false, + }); + }), +]; diff --git a/packages/crates-io-msw/handlers/metadata.test.js b/packages/crates-io-msw/handlers/metadata.test.js new file mode 100644 index 00000000000..2b6bbce4261 --- /dev/null +++ b/packages/crates-io-msw/handlers/metadata.test.js @@ -0,0 +1,13 @@ +import { assert, expect, test } from 'vitest'; + +test('returns the deployed SHA1 and read-only status', async function () { + let response = await fetch('/api/v1/site_metadata'); + assert.strictEqual(response.status, 200); + expect(await response.json()).toMatchInlineSnapshot(` + { + "commit": "5048d31943118c6d67359bd207d307c854e82f45", + "deployed_sha": "5048d31943118c6d67359bd207d307c854e82f45", + "read_only": false, + } + `); +}); diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 138ebf9a0bc..6c9be07a1f5 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,6 +1,7 @@ import categoryHandlers from './handlers/categories.js'; import docsRsHandlers from './handlers/docs-rs.js'; import keywordHandlers from './handlers/keywords.js'; +import metadataHandlers from './handlers/metadata.js'; import sessionHandlers from './handlers/sessions.js'; import apiToken from './models/api-token.js'; import category from './models/category.js'; @@ -16,7 +17,13 @@ import versionDownload from './models/version-download.js'; import version from './models/version.js'; import { factory } from './utils/factory.js'; -export const handlers = [...categoryHandlers, ...docsRsHandlers, ...keywordHandlers, ...sessionHandlers]; +export const handlers = [ + ...categoryHandlers, + ...docsRsHandlers, + ...keywordHandlers, + ...metadataHandlers, + ...sessionHandlers, +]; export const db = factory({ apiToken, From 3462a1a635790d67453e5d1d5cfe189bee754fdf Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 13:26:51 +0100 Subject: [PATCH 041/189] msw: Implement `GET /api/v1/teams/:team_id` request handler --- packages/crates-io-msw/handlers/teams.js | 3 +++ packages/crates-io-msw/handlers/teams/get.js | 15 +++++++++++ .../crates-io-msw/handlers/teams/get.test.js | 25 +++++++++++++++++++ packages/crates-io-msw/index.js | 2 ++ 4 files changed, 45 insertions(+) create mode 100644 packages/crates-io-msw/handlers/teams.js create mode 100644 packages/crates-io-msw/handlers/teams/get.js create mode 100644 packages/crates-io-msw/handlers/teams/get.test.js diff --git a/packages/crates-io-msw/handlers/teams.js b/packages/crates-io-msw/handlers/teams.js new file mode 100644 index 00000000000..fa1b14ca35d --- /dev/null +++ b/packages/crates-io-msw/handlers/teams.js @@ -0,0 +1,3 @@ +import getTeam from './teams/get.js'; + +export default [getTeam]; diff --git a/packages/crates-io-msw/handlers/teams/get.js b/packages/crates-io-msw/handlers/teams/get.js new file mode 100644 index 00000000000..8fb593aa11f --- /dev/null +++ b/packages/crates-io-msw/handlers/teams/get.js @@ -0,0 +1,15 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeTeam } from '../../serializers/team.js'; +import { notFound } from '../../utils/handlers.js'; + +export default http.get('/api/v1/teams/:team_id', ({ params }) => { + let login = params.team_id; + let team = db.team.findFirst({ where: { login: { equals: login } } }); + if (!team) { + return notFound(); + } + + return HttpResponse.json({ team: serializeTeam(team) }); +}); diff --git a/packages/crates-io-msw/handlers/teams/get.test.js b/packages/crates-io-msw/handlers/teams/get.test.js new file mode 100644 index 00000000000..7e164e26f74 --- /dev/null +++ b/packages/crates-io-msw/handlers/teams/get.test.js @@ -0,0 +1,25 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 404 for unknown teams', async function () { + let response = await fetch('/api/v1/teams/foo'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('returns a team object for known teams', async function () { + let team = db.team.create({ name: 'maintainers' }); + + let response = await fetch(`/api/v1/teams/${team.login}`); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + team: { + id: 1, + avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', + login: 'github:rust-lang:maintainers', + name: 'maintainers', + url: 'https://github.com/rust-lang', + }, + }); +}); diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 6c9be07a1f5..c837a975016 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -3,6 +3,7 @@ import docsRsHandlers from './handlers/docs-rs.js'; import keywordHandlers from './handlers/keywords.js'; import metadataHandlers from './handlers/metadata.js'; import sessionHandlers from './handlers/sessions.js'; +import teamHandlers from './handlers/teams.js'; import apiToken from './models/api-token.js'; import category from './models/category.js'; import crateOwnerInvitation from './models/crate-owner-invitation.js'; @@ -23,6 +24,7 @@ export const handlers = [ ...keywordHandlers, ...metadataHandlers, ...sessionHandlers, + ...teamHandlers, ]; export const db = factory({ From bc1e4a5c689d376a5ccb2f64f7a444dc87910108 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 15:26:47 +0100 Subject: [PATCH 042/189] msw: Implement `GET /api/v1/users/:user_id` request handler --- packages/crates-io-msw/handlers/users.js | 3 +++ packages/crates-io-msw/handlers/users/get.js | 15 +++++++++++ .../crates-io-msw/handlers/users/get.test.js | 25 +++++++++++++++++++ packages/crates-io-msw/index.js | 2 ++ 4 files changed, 45 insertions(+) create mode 100644 packages/crates-io-msw/handlers/users.js create mode 100644 packages/crates-io-msw/handlers/users/get.js create mode 100644 packages/crates-io-msw/handlers/users/get.test.js diff --git a/packages/crates-io-msw/handlers/users.js b/packages/crates-io-msw/handlers/users.js new file mode 100644 index 00000000000..86a3fb8a2e0 --- /dev/null +++ b/packages/crates-io-msw/handlers/users.js @@ -0,0 +1,3 @@ +import getUser from './users/get.js'; + +export default [getUser]; diff --git a/packages/crates-io-msw/handlers/users/get.js b/packages/crates-io-msw/handlers/users/get.js new file mode 100644 index 00000000000..ee299d708d1 --- /dev/null +++ b/packages/crates-io-msw/handlers/users/get.js @@ -0,0 +1,15 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeUser } from '../../serializers/user.js'; +import { notFound } from '../../utils/handlers.js'; + +export default http.get('/api/v1/users/:user_id', ({ params }) => { + let login = params.user_id; + let user = db.user.findFirst({ where: { login: { equals: login } } }); + if (!user) { + return notFound(); + } + + return HttpResponse.json({ user: serializeUser(user) }); +}); diff --git a/packages/crates-io-msw/handlers/users/get.test.js b/packages/crates-io-msw/handlers/users/get.test.js new file mode 100644 index 00000000000..f5314186740 --- /dev/null +++ b/packages/crates-io-msw/handlers/users/get.test.js @@ -0,0 +1,25 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 404 for unknown users', async function () { + let response = await fetch('/api/v1/users/foo'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('returns a user object for known users', async function () { + let user = db.user.create({ name: 'John Doe' }); + + let response = await fetch(`/api/v1/users/${user.login}`); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + user: { + id: 1, + avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', + login: 'john-doe', + name: 'John Doe', + url: 'https://github.com/john-doe', + }, + }); +}); diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index c837a975016..787b847d8ff 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -4,6 +4,7 @@ import keywordHandlers from './handlers/keywords.js'; import metadataHandlers from './handlers/metadata.js'; import sessionHandlers from './handlers/sessions.js'; import teamHandlers from './handlers/teams.js'; +import userHandlers from './handlers/users.js'; import apiToken from './models/api-token.js'; import category from './models/category.js'; import crateOwnerInvitation from './models/crate-owner-invitation.js'; @@ -25,6 +26,7 @@ export const handlers = [ ...metadataHandlers, ...sessionHandlers, ...teamHandlers, + ...userHandlers, ]; export const db = factory({ From c537922f914ea8c85f59fcbc65f1bdb34cf66475 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 15:32:31 +0100 Subject: [PATCH 043/189] msw: Implement `PUT /api/v1/users/:user_id` request handler --- packages/crates-io-msw/handlers/users.js | 3 +- .../crates-io-msw/handlers/users/update.js | 44 ++++++++++ .../handlers/users/update.test.js | 83 +++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/users/update.js create mode 100644 packages/crates-io-msw/handlers/users/update.test.js diff --git a/packages/crates-io-msw/handlers/users.js b/packages/crates-io-msw/handlers/users.js index 86a3fb8a2e0..df76b54e986 100644 --- a/packages/crates-io-msw/handlers/users.js +++ b/packages/crates-io-msw/handlers/users.js @@ -1,3 +1,4 @@ import getUser from './users/get.js'; +import updateUser from './users/update.js'; -export default [getUser]; +export default [getUser, updateUser]; diff --git a/packages/crates-io-msw/handlers/users/update.js b/packages/crates-io-msw/handlers/users/update.js new file mode 100644 index 00000000000..83480ee544a --- /dev/null +++ b/packages/crates-io-msw/handlers/users/update.js @@ -0,0 +1,44 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { getSession } from '../../utils/session.js'; + +export default http.put('/api/v1/users/:user_id', async ({ params, request }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + if (user.id.toString() !== params.user_id) { + return HttpResponse.json({ errors: [{ detail: 'current user does not match requested user' }] }, { status: 400 }); + } + + let json = await request.json(); + if (!json || !json.user) { + return HttpResponse.json({ errors: [{ detail: 'invalid json request' }] }, { status: 400 }); + } + + if (json.user.publish_notifications !== undefined) { + db.user.update({ + where: { id: { equals: user.id } }, + data: { publishNotifications: json.user.publish_notifications }, + }); + } + + if (json.user.email !== undefined) { + if (!json.user.email) { + return HttpResponse.json({ errors: [{ detail: 'empty email rejected' }] }, { status: 400 }); + } + + db.user.update({ + where: { id: { equals: user.id } }, + data: { + email: json.user.email, + emailVerified: false, + emailVerificationToken: 'secret123', + }, + }); + } + + return HttpResponse.json({ ok: true }); +}); diff --git a/packages/crates-io-msw/handlers/users/update.test.js b/packages/crates-io-msw/handlers/users/update.test.js new file mode 100644 index 00000000000..0456a47d530 --- /dev/null +++ b/packages/crates-io-msw/handlers/users/update.test.js @@ -0,0 +1,83 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('updates the user with a new email address', async function () { + let user = db.user.create({ email: 'old@email.com' }); + db.mswSession.create({ user }); + + let body = JSON.stringify({ user: { email: 'new@email.com' } }); + let response = await fetch(`/api/v1/users/${user.id}`, { method: 'PUT', body }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { ok: true }); + + user = db.user.findFirst({ where: { id: user.id } }); + assert.strictEqual(user.email, 'new@email.com'); + assert.strictEqual(user.emailVerified, false); + assert.strictEqual(user.emailVerificationToken, 'secret123'); +}); + +test('updates the `publish_notifications` settings', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + assert.strictEqual(user.publishNotifications, true); + + let body = JSON.stringify({ user: { publish_notifications: false } }); + let response = await fetch(`/api/v1/users/${user.id}`, { method: 'PUT', body }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { ok: true }); + + user = db.user.findFirst({ where: { id: user.id } }); + assert.strictEqual(user.publishNotifications, false); +}); + +test('returns 403 when not logged in', async function () { + let user = db.user.create({ email: 'old@email.com' }); + + let body = JSON.stringify({ user: { email: 'new@email.com' } }); + let response = await fetch(`/api/v1/users/${user.id}`, { method: 'PUT', body }); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { errors: [{ detail: 'must be logged in to perform that action' }] }); + + user = db.user.findFirst({ where: { id: user.id } }); + assert.strictEqual(user.email, 'old@email.com'); +}); + +test('returns 400 when requesting the wrong user id', async function () { + let user = db.user.create({ email: 'old@email.com' }); + db.mswSession.create({ user }); + + let body = JSON.stringify({ user: { email: 'new@email.com' } }); + let response = await fetch(`/api/v1/users/wrong-id`, { method: 'PUT', body }); + assert.strictEqual(response.status, 400); + assert.deepEqual(await response.json(), { errors: [{ detail: 'current user does not match requested user' }] }); + + user = db.user.findFirst({ where: { id: user.id } }); + assert.strictEqual(user.email, 'old@email.com'); +}); + +test('returns 400 when sending an invalid payload', async function () { + let user = db.user.create({ email: 'old@email.com' }); + db.mswSession.create({ user }); + + let body = JSON.stringify({}); + let response = await fetch(`/api/v1/users/${user.id}`, { method: 'PUT', body }); + assert.strictEqual(response.status, 400); + assert.deepEqual(await response.json(), { errors: [{ detail: 'invalid json request' }] }); + + user = db.user.findFirst({ where: { id: user.id } }); + assert.strictEqual(user.email, 'old@email.com'); +}); + +test('returns 400 when sending an empty email address', async function () { + let user = db.user.create({ email: 'old@email.com' }); + db.mswSession.create({ user }); + + let body = JSON.stringify({ user: { email: '' } }); + let response = await fetch(`/api/v1/users/${user.id}`, { method: 'PUT', body }); + assert.strictEqual(response.status, 400); + assert.deepEqual(await response.json(), { errors: [{ detail: 'empty email rejected' }] }); + + user = db.user.findFirst({ where: { id: user.id } }); + assert.strictEqual(user.email, 'old@email.com'); +}); From 11edd81c927a3a77aa0ea5cac918aee7bceb10da Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 15:37:32 +0100 Subject: [PATCH 044/189] msw: Implement `PUT /api/v1/users/:user_id/resend` request handler --- packages/crates-io-msw/handlers/users.js | 3 +- .../crates-io-msw/handlers/users/resend.js | 18 ++++++++++++ .../handlers/users/resend.test.js | 29 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/users/resend.js create mode 100644 packages/crates-io-msw/handlers/users/resend.test.js diff --git a/packages/crates-io-msw/handlers/users.js b/packages/crates-io-msw/handlers/users.js index df76b54e986..00958055e00 100644 --- a/packages/crates-io-msw/handlers/users.js +++ b/packages/crates-io-msw/handlers/users.js @@ -1,4 +1,5 @@ import getUser from './users/get.js'; +import resend from './users/resend.js'; import updateUser from './users/update.js'; -export default [getUser, updateUser]; +export default [getUser, updateUser, resend]; diff --git a/packages/crates-io-msw/handlers/users/resend.js b/packages/crates-io-msw/handlers/users/resend.js new file mode 100644 index 00000000000..a9afc9395b3 --- /dev/null +++ b/packages/crates-io-msw/handlers/users/resend.js @@ -0,0 +1,18 @@ +import { http, HttpResponse } from 'msw'; + +import { getSession } from '../../utils/session.js'; + +export default http.put('/api/v1/users/:user_id/resend', ({ params }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + if (user.id.toString() !== params.user_id) { + return HttpResponse.json({ errors: [{ detail: 'current user does not match requested user' }] }, { status: 400 }); + } + + // let's pretend that we're sending an email here... :D + + return HttpResponse.json({ ok: true }); +}); diff --git a/packages/crates-io-msw/handlers/users/resend.test.js b/packages/crates-io-msw/handlers/users/resend.test.js new file mode 100644 index 00000000000..a260624ff5a --- /dev/null +++ b/packages/crates-io-msw/handlers/users/resend.test.js @@ -0,0 +1,29 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns `ok`', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch(`/api/v1/users/${user.id}/resend`, { method: 'PUT' }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { ok: true }); +}); + +test('returns 403 when not logged in', async function () { + let user = db.user.create(); + + let response = await fetch(`/api/v1/users/${user.id}/resend`, { method: 'PUT' }); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { errors: [{ detail: 'must be logged in to perform that action' }] }); +}); + +test('returns 400 when requesting the wrong user id', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch(`/api/v1/users/wrong-id/resend`, { method: 'PUT' }); + assert.strictEqual(response.status, 400); + assert.deepEqual(await response.json(), { errors: [{ detail: 'current user does not match requested user' }] }); +}); From 0329a7748f3e598c7f3d653cf73037d7eeea293a Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 21 Jan 2025 15:53:52 +0100 Subject: [PATCH 045/189] msw: Implement `GET /api/v1/me` request handler --- packages/crates-io-msw/handlers/users.js | 3 +- packages/crates-io-msw/handlers/users/me.js | 23 ++++++++ .../crates-io-msw/handlers/users/me.test.js | 55 +++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/users/me.js create mode 100644 packages/crates-io-msw/handlers/users/me.test.js diff --git a/packages/crates-io-msw/handlers/users.js b/packages/crates-io-msw/handlers/users.js index 00958055e00..5c265bb9b66 100644 --- a/packages/crates-io-msw/handlers/users.js +++ b/packages/crates-io-msw/handlers/users.js @@ -1,5 +1,6 @@ import getUser from './users/get.js'; +import me from './users/me.js'; import resend from './users/resend.js'; import updateUser from './users/update.js'; -export default [getUser, updateUser, resend]; +export default [getUser, updateUser, resend, me]; diff --git a/packages/crates-io-msw/handlers/users/me.js b/packages/crates-io-msw/handlers/users/me.js new file mode 100644 index 00000000000..e435006946a --- /dev/null +++ b/packages/crates-io-msw/handlers/users/me.js @@ -0,0 +1,23 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeUser } from '../../serializers/user.js'; +import { getSession } from '../../utils/session.js'; + +export default http.get('/api/v1/me', () => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let ownerships = db.crateOwnership.findMany({ where: { user: { id: { equals: user.id } } } }); + + return HttpResponse.json({ + user: serializeUser(user, { removePrivateData: false }), + owned_crates: ownerships.map(ownership => ({ + id: ownership.crate.id, + name: ownership.crate.name, + email_notifications: ownership.emailNotifications, + })), + }); +}); diff --git a/packages/crates-io-msw/handlers/users/me.test.js b/packages/crates-io-msw/handlers/users/me.test.js new file mode 100644 index 00000000000..fba39926019 --- /dev/null +++ b/packages/crates-io-msw/handlers/users/me.test.js @@ -0,0 +1,55 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns the `user` resource including the private fields', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/me'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + user: { + id: 1, + avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', + email: 'user-1@crates.io', + email_verification_sent: true, + email_verified: true, + is_admin: false, + login: 'user-1', + name: 'User 1', + publish_notifications: true, + url: 'https://github.com/user-1', + }, + owned_crates: [], + }); +}); + +test('returns a list of `owned_crates`', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let [crate1, , crate3] = Array.from({ length: 3 }, () => db.crate.create()); + + db.crateOwnership.create({ crate: crate1, user }); + db.crateOwnership.create({ crate: crate3, user }); + + let response = await fetch('/api/v1/me'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.deepEqual(responsePayload.owned_crates, [ + { id: crate1.id, name: 'crate-1', email_notifications: true }, + { id: crate3.id, name: 'crate-3', email_notifications: true }, + ]); +}); + +test('returns an error if unauthenticated', async function () { + db.user.create(); + + let response = await fetch('/api/v1/me'); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); From 125687b69c75b4502f8cd74666bc07f59d5fcdf2 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 22 Jan 2025 11:44:29 +0100 Subject: [PATCH 046/189] msw: Implement `GET /api/private/crate_owner_invitations` request handler --- packages/crates-io-msw/handlers/invites.js | 3 + .../crates-io-msw/handlers/invites/list.js | 55 +++++ .../handlers/invites/list.test.js | 221 ++++++++++++++++++ packages/crates-io-msw/index.js | 2 + 4 files changed, 281 insertions(+) create mode 100644 packages/crates-io-msw/handlers/invites.js create mode 100644 packages/crates-io-msw/handlers/invites/list.js create mode 100644 packages/crates-io-msw/handlers/invites/list.test.js diff --git a/packages/crates-io-msw/handlers/invites.js b/packages/crates-io-msw/handlers/invites.js new file mode 100644 index 00000000000..e629d6f821f --- /dev/null +++ b/packages/crates-io-msw/handlers/invites.js @@ -0,0 +1,3 @@ +import listInvites from './invites/list.js'; + +export default [listInvites]; diff --git a/packages/crates-io-msw/handlers/invites/list.js b/packages/crates-io-msw/handlers/invites/list.js new file mode 100644 index 00000000000..257832f457f --- /dev/null +++ b/packages/crates-io-msw/handlers/invites/list.js @@ -0,0 +1,55 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeInvite } from '../../serializers/invite.js'; +import { serializeUser } from '../../serializers/user.js'; +import { notFound } from '../../utils/handlers.js'; +import { getSession } from '../../utils/session.js'; + +export default http.get('/api/private/crate_owner_invitations', ({ request }) => { + let url = new URL(request.url); + + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let invites; + if (url.searchParams.has('crate_name')) { + let crate = db.crate.findFirst({ where: { name: { equals: url.searchParams.get('crate_name') } } }); + if (!crate) return notFound(); + + invites = db.crateOwnerInvitation.findMany({ where: { crate: { id: { equals: crate.id } } } }); + } else if (url.searchParams.has('invitee_id')) { + let inviteeId = parseInt(url.searchParams.get('invitee_id')); + if (inviteeId !== user.id) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + invites = db.crateOwnerInvitation.findMany({ where: { invitee: { id: { equals: inviteeId } } } }); + } else { + return HttpResponse.json({ errors: [{ detail: 'missing or invalid filter' }] }, { status: 400 }); + } + + let perPage = 10; + let start = parseInt(url.searchParams.get('__start__') ?? '0'); + let end = start + perPage; + + let nextPage = null; + if (invites.length > end) { + url.searchParams.set('__start__', end); + nextPage = url.search; + } + + invites = invites.slice(start, end); + + let inviters = invites.map(invite => invite.inviter); + let invitees = invites.map(invite => invite.invitee); + let users = Array.from(new Set([...inviters, ...invitees])).sort((a, b) => a.id - b.id); + + return HttpResponse.json({ + crate_owner_invitations: invites.map(invite => serializeInvite(invite)), + users: users.map(user => serializeUser(user)), + meta: { next_page: nextPage }, + }); +}); diff --git a/packages/crates-io-msw/handlers/invites/list.test.js b/packages/crates-io-msw/handlers/invites/list.test.js new file mode 100644 index 00000000000..551bcd05903 --- /dev/null +++ b/packages/crates-io-msw/handlers/invites/list.test.js @@ -0,0 +1,221 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('happy path (invitee_id)', async function () { + let nanomsg = db.crate.create({ name: 'nanomsg' }); + db.version.create({ crate: nanomsg }); + + let ember = db.crate.create({ name: 'ember-rs' }); + db.version.create({ crate: ember }); + + let user = db.user.create(); + db.mswSession.create({ user }); + + let inviter = db.user.create({ name: 'janed' }); + db.crateOwnerInvitation.create({ + crate: nanomsg, + createdAt: '2016-12-24T12:34:56Z', + invitee: user, + inviter, + }); + + let inviter2 = db.user.create({ name: 'wycats' }); + db.crateOwnerInvitation.create({ + crate: ember, + createdAt: '2020-12-31T12:34:56Z', + invitee: user, + inviter: inviter2, + }); + + let response = await fetch(`/api/private/crate_owner_invitations?invitee_id=${user.id}`); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + crate_owner_invitations: [ + { + crate_id: Number(nanomsg.id), + crate_name: 'nanomsg', + created_at: '2016-12-24T12:34:56Z', + expires_at: '2017-01-24T12:34:56Z', + invitee_id: Number(user.id), + inviter_id: Number(inviter.id), + }, + { + crate_id: Number(ember.id), + crate_name: 'ember-rs', + created_at: '2020-12-31T12:34:56Z', + expires_at: '2017-01-24T12:34:56Z', + invitee_id: Number(user.id), + inviter_id: Number(inviter2.id), + }, + ], + users: [ + { + avatar: user.avatar, + id: Number(user.id), + login: user.login, + name: user.name, + url: user.url, + }, + { + avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', + id: Number(inviter.id), + login: 'janed', + name: 'janed', + url: 'https://github.com/janed', + }, + { + avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', + id: Number(inviter2.id), + login: 'wycats', + name: 'wycats', + url: 'https://github.com/wycats', + }, + ], + meta: { + next_page: null, + }, + }); +}); + +test('happy path with empty response (invitee_id)', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch(`/api/private/crate_owner_invitations?invitee_id=${user.id}`); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + crate_owner_invitations: [], + users: [], + meta: { + next_page: null, + }, + }); +}); + +test('happy path with pagination (invitee_id)', async function () { + let inviter = db.user.create(); + + let user = db.user.create(); + db.mswSession.create({ user }); + + for (let i = 0; i < 15; i++) { + let crate = db.crate.create(); + db.version.create({ crate }); + db.crateOwnerInvitation.create({ crate, invitee: user, inviter }); + } + + let response = await fetch(`/api/private/crate_owner_invitations?invitee_id=${user.id}`); + assert.strictEqual(response.status, 200); + let responseJSON = await response.json(); + assert.strictEqual(responseJSON['crate_owner_invitations'].length, 10); + assert.ok(responseJSON.meta['next_page']); + + response = await fetch(`/api/private/crate_owner_invitations${responseJSON.meta['next_page']}`); + assert.strictEqual(response.status, 200); + responseJSON = await response.json(); + assert.strictEqual(responseJSON['crate_owner_invitations'].length, 5); + assert.strictEqual(responseJSON.meta['next_page'], null); +}); + +test('happy path (crate_name)', async function () { + let nanomsg = db.crate.create({ name: 'nanomsg' }); + db.version.create({ crate: nanomsg }); + + let ember = db.crate.create({ name: 'ember-rs' }); + db.version.create({ crate: ember }); + + let user = db.user.create(); + db.mswSession.create({ user }); + + let inviter = db.user.create({ name: 'janed' }); + db.crateOwnerInvitation.create({ + crate: nanomsg, + createdAt: '2016-12-24T12:34:56Z', + invitee: user, + inviter, + }); + + let inviter2 = db.user.create({ name: 'wycats' }); + db.crateOwnerInvitation.create({ + crate: ember, + createdAt: '2020-12-31T12:34:56Z', + invitee: user, + inviter: inviter2, + }); + + let response = await fetch(`/api/private/crate_owner_invitations?crate_name=ember-rs`); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + crate_owner_invitations: [ + { + crate_id: Number(ember.id), + crate_name: 'ember-rs', + created_at: '2020-12-31T12:34:56Z', + expires_at: '2017-01-24T12:34:56Z', + invitee_id: Number(user.id), + inviter_id: Number(inviter2.id), + }, + ], + users: [ + { + avatar: user.avatar, + id: Number(user.id), + login: user.login, + name: user.name, + url: user.url, + }, + { + avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', + id: Number(inviter2.id), + login: 'wycats', + name: 'wycats', + url: 'https://github.com/wycats', + }, + ], + meta: { + next_page: null, + }, + }); +}); + +test('returns 403 if unauthenticated', async function () { + let response = await fetch(`/api/private/crate_owner_invitations?invitee_id=42`); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); + +test('returns 400 if query params are missing', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch(`/api/private/crate_owner_invitations`); + assert.strictEqual(response.status, 400); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'missing or invalid filter' }], + }); +}); + +test("returns 404 if crate can't be found", async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch(`/api/private/crate_owner_invitations?crate_name=foo`); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'Not Found' }], + }); +}); + +test('returns 403 if requesting for other user', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch(`/api/private/crate_owner_invitations?invitee_id=${user.id + 1}`); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 787b847d8ff..64360ea0a79 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,5 +1,6 @@ import categoryHandlers from './handlers/categories.js'; import docsRsHandlers from './handlers/docs-rs.js'; +import inviteHandlers from './handlers/invites.js'; import keywordHandlers from './handlers/keywords.js'; import metadataHandlers from './handlers/metadata.js'; import sessionHandlers from './handlers/sessions.js'; @@ -22,6 +23,7 @@ import { factory } from './utils/factory.js'; export const handlers = [ ...categoryHandlers, ...docsRsHandlers, + ...inviteHandlers, ...keywordHandlers, ...metadataHandlers, ...sessionHandlers, From f207e283493c827bf8df764de9679be58856f12b Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 22 Jan 2025 12:14:30 +0100 Subject: [PATCH 047/189] msw: Implement `GET /api/v1/me/crate_owner_invitations` request handler --- packages/crates-io-msw/handlers/invites.js | 3 +- .../handlers/invites/legacy-list.js | 24 +++++ .../handlers/invites/legacy-list.test.js | 93 +++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/invites/legacy-list.js create mode 100644 packages/crates-io-msw/handlers/invites/legacy-list.test.js diff --git a/packages/crates-io-msw/handlers/invites.js b/packages/crates-io-msw/handlers/invites.js index e629d6f821f..25ef651dacc 100644 --- a/packages/crates-io-msw/handlers/invites.js +++ b/packages/crates-io-msw/handlers/invites.js @@ -1,3 +1,4 @@ +import legacyListInvites from './invites/legacy-list.js'; import listInvites from './invites/list.js'; -export default [listInvites]; +export default [listInvites, legacyListInvites]; diff --git a/packages/crates-io-msw/handlers/invites/legacy-list.js b/packages/crates-io-msw/handlers/invites/legacy-list.js new file mode 100644 index 00000000000..6f70f2a9f85 --- /dev/null +++ b/packages/crates-io-msw/handlers/invites/legacy-list.js @@ -0,0 +1,24 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeInvite } from '../../serializers/invite.js'; +import { serializeUser } from '../../serializers/user.js'; +import { getSession } from '../../utils/session.js'; + +export default http.get('/api/v1/me/crate_owner_invitations', () => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let invites = db.crateOwnerInvitation.findMany({ where: { invitee: { id: { equals: user.id } } } }); + + let inviters = invites.map(invite => invite.inviter); + let invitees = invites.map(invite => invite.invitee); + let users = Array.from(new Set([...inviters, ...invitees])).sort((a, b) => a.id - b.id); + + return HttpResponse.json({ + crate_owner_invitations: invites.map(invite => serializeInvite(invite)), + users: users.map(user => serializeUser(user)), + }); +}); diff --git a/packages/crates-io-msw/handlers/invites/legacy-list.test.js b/packages/crates-io-msw/handlers/invites/legacy-list.test.js new file mode 100644 index 00000000000..91e7b4c768e --- /dev/null +++ b/packages/crates-io-msw/handlers/invites/legacy-list.test.js @@ -0,0 +1,93 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('empty case', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/me/crate_owner_invitations'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { crate_owner_invitations: [], users: [] }); +}); + +test('returns the list of invitations for the authenticated user', async function () { + let nanomsg = db.crate.create({ name: 'nanomsg' }); + db.version.create({ crate: nanomsg }); + + let ember = db.crate.create({ name: 'ember-rs' }); + db.version.create({ crate: ember }); + + let user = db.user.create(); + db.mswSession.create({ user }); + + let inviter = db.user.create({ name: 'janed' }); + db.crateOwnerInvitation.create({ + crate: nanomsg, + createdAt: '2016-12-24T12:34:56Z', + invitee: user, + inviter, + }); + + let inviter2 = db.user.create({ name: 'wycats' }); + db.crateOwnerInvitation.create({ + crate: ember, + createdAt: '2020-12-31T12:34:56Z', + invitee: user, + inviter: inviter2, + }); + + let response = await fetch('/api/v1/me/crate_owner_invitations'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + crate_owner_invitations: [ + { + crate_id: Number(nanomsg.id), + crate_name: 'nanomsg', + created_at: '2016-12-24T12:34:56Z', + expires_at: '2017-01-24T12:34:56Z', + invitee_id: Number(user.id), + inviter_id: Number(inviter.id), + }, + { + crate_id: Number(ember.id), + crate_name: 'ember-rs', + created_at: '2020-12-31T12:34:56Z', + expires_at: '2017-01-24T12:34:56Z', + invitee_id: Number(user.id), + inviter_id: Number(inviter2.id), + }, + ], + users: [ + { + avatar: user.avatar, + id: Number(user.id), + login: user.login, + name: user.name, + url: user.url, + }, + { + avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', + id: Number(inviter.id), + login: 'janed', + name: 'janed', + url: 'https://github.com/janed', + }, + { + avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', + id: Number(inviter2.id), + login: 'wycats', + name: 'wycats', + url: 'https://github.com/wycats', + }, + ], + }); +}); + +test('returns an error if unauthenticated', async function () { + let response = await fetch('/api/v1/me/crate_owner_invitations'); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); From 48db71427f1cd496676458a30929c30be5d0be66 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 22 Jan 2025 12:41:53 +0100 Subject: [PATCH 048/189] msw: Implement `PUT /api/v1/me/crate_owner_invitations/:crate_id` request handler --- packages/crates-io-msw/handlers/invites.js | 3 +- .../handlers/invites/redeem-by-crate-id.js | 31 +++++++ .../invites/redeem-by-crate-id.test.js | 88 +++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/invites/redeem-by-crate-id.js create mode 100644 packages/crates-io-msw/handlers/invites/redeem-by-crate-id.test.js diff --git a/packages/crates-io-msw/handlers/invites.js b/packages/crates-io-msw/handlers/invites.js index 25ef651dacc..2271c2a58e9 100644 --- a/packages/crates-io-msw/handlers/invites.js +++ b/packages/crates-io-msw/handlers/invites.js @@ -1,4 +1,5 @@ import legacyListInvites from './invites/legacy-list.js'; import listInvites from './invites/list.js'; +import redeemByCrateId from './invites/redeem-by-crate-id.js'; -export default [listInvites, legacyListInvites]; +export default [listInvites, legacyListInvites, redeemByCrateId]; diff --git a/packages/crates-io-msw/handlers/invites/redeem-by-crate-id.js b/packages/crates-io-msw/handlers/invites/redeem-by-crate-id.js new file mode 100644 index 00000000000..61837c62252 --- /dev/null +++ b/packages/crates-io-msw/handlers/invites/redeem-by-crate-id.js @@ -0,0 +1,31 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { notFound } from '../../utils/handlers.js'; +import { getSession } from '../../utils/session.js'; + +export default http.put('/api/v1/me/crate_owner_invitations/:crate_id', async ({ request }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let body = await request.json(); + let { accepted, crate_id: crateId } = body.crate_owner_invite; + + let invite = db.crateOwnerInvitation.findFirst({ + where: { + crate: { id: { equals: parseInt(crateId) } }, + invitee: { id: { equals: user.id } }, + }, + }); + if (!invite) return notFound(); + + if (accepted) { + db.crateOwnership.create({ crate: invite.crate, user }); + } + + db.crateOwnerInvitation.delete({ where: { id: invite.id } }); + + return HttpResponse.json({ crate_owner_invitation: { crate_id: crateId, accepted } }); +}); diff --git a/packages/crates-io-msw/handlers/invites/redeem-by-crate-id.test.js b/packages/crates-io-msw/handlers/invites/redeem-by-crate-id.test.js new file mode 100644 index 00000000000..32ec482ebb9 --- /dev/null +++ b/packages/crates-io-msw/handlers/invites/redeem-by-crate-id.test.js @@ -0,0 +1,88 @@ +import { test as _test, assert } from 'vitest'; + +import { db } from '../../index.js'; + +let test = _test.extend({ + // eslint-disable-next-line no-empty-pattern + serde: async ({}, use) => { + let serde = db.crate.create({ name: 'serde' }); + db.version.create({ crate: serde }); + await use(serde); + }, +}); + +test('can accept an invitation', async function ({ serde }) { + let inviter = db.user.create(); + let invitee = db.user.create(); + db.mswSession.create({ user: invitee }); + + db.crateOwnerInvitation.create({ crate: serde, invitee, inviter }); + + let body = JSON.stringify({ crate_owner_invite: { crate_id: serde.id, accepted: true } }); + let response = await fetch('/api/v1/me/crate_owner_invitations/serde', { method: 'PUT', body }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + crate_owner_invitation: { + accepted: true, + crate_id: serde.id, + }, + }); + + let invites = db.crateOwnerInvitation.findMany({ where: { crate: { id: serde.id }, invitee: { id: invitee.id } } }); + assert.strictEqual(invites.length, 0); + let owners = db.crateOwnership.findMany({ where: { crate: { id: serde.id }, user: { id: invitee.id } } }); + assert.strictEqual(owners.length, 1); +}); + +test('can decline an invitation', async function ({ serde }) { + let inviter = db.user.create(); + let invitee = db.user.create(); + db.mswSession.create({ user: invitee }); + + db.crateOwnerInvitation.create({ crate: serde, invitee, inviter }); + + let body = JSON.stringify({ crate_owner_invite: { crate_id: serde.id, accepted: false } }); + let response = await fetch('/api/v1/me/crate_owner_invitations/serde', { method: 'PUT', body }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + crate_owner_invitation: { + accepted: false, + crate_id: serde.id, + }, + }); + + let invites = db.crateOwnerInvitation.findMany({ where: { crate: { id: serde.id }, invitee: { id: invitee.id } } }); + assert.strictEqual(invites.length, 0); + let owners = db.crateOwnership.findMany({ where: { crate: { id: serde.id }, user: { id: invitee.id } } }); + assert.strictEqual(owners.length, 0); +}); + +test('returns 404 if invite does not exist', async function ({ serde }) { + let user = db.user.create(); + db.mswSession.create({ user }); + + let body = JSON.stringify({ crate_owner_invite: { crate_id: serde.id, accepted: true } }); + let response = await fetch('/api/v1/me/crate_owner_invitations/serde', { method: 'PUT', body }); + assert.strictEqual(response.status, 404); +}); + +test('returns 404 if invite is for another user', async function ({ serde }) { + let inviter = db.user.create(); + let invitee = db.user.create(); + db.mswSession.create({ user: inviter }); + + db.crateOwnerInvitation.create({ crate: serde, invitee, inviter }); + + let body = JSON.stringify({ crate_owner_invite: { crate_id: serde.id, accepted: true } }); + let response = await fetch('/api/v1/me/crate_owner_invitations/serde', { method: 'PUT', body }); + assert.strictEqual(response.status, 404); +}); + +test('returns an error if unauthenticated', async function ({ serde }) { + let body = JSON.stringify({ crate_owner_invite: { crate_id: serde.id, accepted: true } }); + let response = await fetch('/api/v1/me/crate_owner_invitations/serde', { method: 'PUT', body }); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); From 8d576069ffc0a6fccc674683ac2c6455d556291d Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 22 Jan 2025 13:50:55 +0100 Subject: [PATCH 049/189] msw: Implement `PUT /api/v1/me/crate_owner_invitations/accept/:token` request handler --- packages/crates-io-msw/handlers/invites.js | 3 +- .../handlers/invites/redeem-by-token.js | 16 +++++++++ .../handlers/invites/redeem-by-token.test.js | 36 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/invites/redeem-by-token.js create mode 100644 packages/crates-io-msw/handlers/invites/redeem-by-token.test.js diff --git a/packages/crates-io-msw/handlers/invites.js b/packages/crates-io-msw/handlers/invites.js index 2271c2a58e9..b8cb2e45de6 100644 --- a/packages/crates-io-msw/handlers/invites.js +++ b/packages/crates-io-msw/handlers/invites.js @@ -1,5 +1,6 @@ import legacyListInvites from './invites/legacy-list.js'; import listInvites from './invites/list.js'; import redeemByCrateId from './invites/redeem-by-crate-id.js'; +import redeemByToken from './invites/redeem-by-token.js'; -export default [listInvites, legacyListInvites, redeemByCrateId]; +export default [listInvites, legacyListInvites, redeemByCrateId, redeemByToken]; diff --git a/packages/crates-io-msw/handlers/invites/redeem-by-token.js b/packages/crates-io-msw/handlers/invites/redeem-by-token.js new file mode 100644 index 00000000000..98df6daa60b --- /dev/null +++ b/packages/crates-io-msw/handlers/invites/redeem-by-token.js @@ -0,0 +1,16 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { notFound } from '../../utils/handlers.js'; + +export default http.put('/api/v1/me/crate_owner_invitations/accept/:token', async ({ params }) => { + let { token } = params; + + let invite = db.crateOwnerInvitation.findFirst({ where: { token: { equals: token } } }); + if (!invite) return notFound(); + + db.crateOwnership.create({ crate: invite.crate, user: invite.invitee }); + db.crateOwnerInvitation.delete({ where: { id: invite.id } }); + + return HttpResponse.json({ crate_owner_invitation: { crate_id: invite.crate.id, accepted: true } }); +}); diff --git a/packages/crates-io-msw/handlers/invites/redeem-by-token.test.js b/packages/crates-io-msw/handlers/invites/redeem-by-token.test.js new file mode 100644 index 00000000000..c8cd870d2b8 --- /dev/null +++ b/packages/crates-io-msw/handlers/invites/redeem-by-token.test.js @@ -0,0 +1,36 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('can accept an invitation', async function () { + let serde = db.crate.create({ name: 'serde' }); + db.version.create({ crate: serde }); + + let inviter = db.user.create(); + let invitee = db.user.create(); + db.mswSession.create({ user: invitee }); + + let invite = db.crateOwnerInvitation.create({ crate: serde, invitee, inviter }); + + let response = await fetch(`/api/v1/me/crate_owner_invitations/accept/${invite.token}`, { method: 'PUT' }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + crate_owner_invitation: { + accepted: true, + crate_id: serde.id, + }, + }); + + let invites = db.crateOwnerInvitation.findMany({ where: { crate: { id: serde.id }, invitee: { id: invitee.id } } }); + assert.strictEqual(invites.length, 0); + let owners = db.crateOwnership.findMany({ where: { crate: { id: serde.id }, user: { id: invitee.id } } }); + assert.strictEqual(owners.length, 1); +}); + +test('returns 404 if invite does not exist', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/me/crate_owner_invitations/accept/secret-token', { method: 'PUT' }); + assert.strictEqual(response.status, 404); +}); From 8df29aa3f1b4d54c209ccf74e92562ee32f873fe Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 22 Jan 2025 14:04:26 +0100 Subject: [PATCH 050/189] msw: Implement `PUT /api/v1/confirm/:token` request handler --- packages/crates-io-msw/handlers/users.js | 3 +- .../handlers/users/confirm-email.js | 16 ++++++++ .../handlers/users/confirm-email.test.js | 37 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/users/confirm-email.js create mode 100644 packages/crates-io-msw/handlers/users/confirm-email.test.js diff --git a/packages/crates-io-msw/handlers/users.js b/packages/crates-io-msw/handlers/users.js index 5c265bb9b66..51a9a9e05b6 100644 --- a/packages/crates-io-msw/handlers/users.js +++ b/packages/crates-io-msw/handlers/users.js @@ -1,6 +1,7 @@ +import confirmEmail from './users/confirm-email.js'; import getUser from './users/get.js'; import me from './users/me.js'; import resend from './users/resend.js'; import updateUser from './users/update.js'; -export default [getUser, updateUser, resend, me]; +export default [getUser, updateUser, resend, me, confirmEmail]; diff --git a/packages/crates-io-msw/handlers/users/confirm-email.js b/packages/crates-io-msw/handlers/users/confirm-email.js new file mode 100644 index 00000000000..d42c7b13d44 --- /dev/null +++ b/packages/crates-io-msw/handlers/users/confirm-email.js @@ -0,0 +1,16 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; + +export default http.put('/api/v1/confirm/:token', ({ params }) => { + let { token } = params; + + let user = db.user.findFirst({ where: { emailVerificationToken: { equals: token } } }); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'Email belonging to token not found.' }] }, { status: 400 }); + } + + db.user.update({ where: { id: user.id }, data: { emailVerified: true, emailVerificationToken: null } }); + + return HttpResponse.json({ ok: true }); +}); diff --git a/packages/crates-io-msw/handlers/users/confirm-email.test.js b/packages/crates-io-msw/handlers/users/confirm-email.test.js new file mode 100644 index 00000000000..64c7fea7207 --- /dev/null +++ b/packages/crates-io-msw/handlers/users/confirm-email.test.js @@ -0,0 +1,37 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns `ok: true` for a known token (unauthenticated)', async function () { + let user = db.user.create({ emailVerificationToken: 'foo' }); + assert.strictEqual(user.emailVerified, false); + + let response = await fetch('/api/v1/confirm/foo', { method: 'PUT' }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { ok: true }); + + user = db.user.findFirst({ where: { id: user.id } }); + assert.strictEqual(user.emailVerified, true); +}); + +test('returns `ok: true` for a known token (authenticated)', async function () { + let user = db.user.create({ emailVerificationToken: 'foo' }); + assert.strictEqual(user.emailVerified, false); + + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/confirm/foo', { method: 'PUT' }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { ok: true }); + + user = db.user.findFirst({ where: { id: user.id } }); + assert.strictEqual(user.emailVerified, true); +}); + +test('returns an error for unknown tokens', async function () { + let response = await fetch('/api/v1/confirm/unknown', { method: 'PUT' }); + assert.strictEqual(response.status, 400); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'Email belonging to token not found.' }], + }); +}); From a91fff1cbf747c33bd73ee96f0a3f64e5d229cc1 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 22 Jan 2025 14:57:57 +0100 Subject: [PATCH 051/189] msw: Implement `PUT /api/v1/me/tokens` request handler --- packages/crates-io-msw/handlers/api-tokens.js | 3 + .../handlers/api-tokens/create.js | 27 +++++ .../handlers/api-tokens/create.test.js | 110 ++++++++++++++++++ packages/crates-io-msw/index.js | 2 + 4 files changed, 142 insertions(+) create mode 100644 packages/crates-io-msw/handlers/api-tokens.js create mode 100644 packages/crates-io-msw/handlers/api-tokens/create.js create mode 100644 packages/crates-io-msw/handlers/api-tokens/create.test.js diff --git a/packages/crates-io-msw/handlers/api-tokens.js b/packages/crates-io-msw/handlers/api-tokens.js new file mode 100644 index 00000000000..cfdf568d58d --- /dev/null +++ b/packages/crates-io-msw/handlers/api-tokens.js @@ -0,0 +1,3 @@ +import createToken from './api-tokens/create.js'; + +export default [createToken]; diff --git a/packages/crates-io-msw/handlers/api-tokens/create.js b/packages/crates-io-msw/handlers/api-tokens/create.js new file mode 100644 index 00000000000..4ad4bf360c2 --- /dev/null +++ b/packages/crates-io-msw/handlers/api-tokens/create.js @@ -0,0 +1,27 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeApiToken } from '../../serializers/api-token.js'; +import { getSession } from '../../utils/session.js'; + +export default http.put('/api/v1/me/tokens', async ({ request }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let json = await request.json(); + + let token = db.apiToken.create({ + user, + name: json.api_token.name, + crateScopes: json.api_token.crate_scopes ?? null, + endpointScopes: json.api_token.endpoint_scopes ?? null, + expiredAt: json.api_token.expired_at ?? null, + createdAt: new Date().toISOString(), + }); + + return HttpResponse.json({ + api_token: serializeApiToken(token, { forCreate: true }), + }); +}); diff --git a/packages/crates-io-msw/handlers/api-tokens/create.test.js b/packages/crates-io-msw/handlers/api-tokens/create.test.js new file mode 100644 index 00000000000..48e57d9127d --- /dev/null +++ b/packages/crates-io-msw/handlers/api-tokens/create.test.js @@ -0,0 +1,110 @@ +import { afterEach, assert, beforeEach, test, vi } from 'vitest'; + +import { db } from '../../index.js'; + +beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2017-11-20T11:23:45Z')); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +test('creates a new API token', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let body = JSON.stringify({ api_token: { name: 'foooo' } }); + let response = await fetch('/api/v1/me/tokens', { method: 'PUT', body }); + assert.strictEqual(response.status, 200); + + let token = db.apiToken.findMany({})[0]; + assert.ok(token); + + assert.deepEqual(await response.json(), { + api_token: { + id: 1, + crate_scopes: null, + created_at: '2017-11-20T11:23:45.000Z', + endpoint_scopes: null, + expired_at: null, + last_used_at: null, + name: 'foooo', + revoked: false, + token: token.token, + }, + }); +}); + +test('creates a new API token with scopes', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let body = JSON.stringify({ + api_token: { + name: 'foooo', + crate_scopes: ['serde', 'serde-*'], + endpoint_scopes: ['publish-update'], + }, + }); + let response = await fetch('/api/v1/me/tokens', { method: 'PUT', body }); + assert.strictEqual(response.status, 200); + + let token = db.apiToken.findMany({})[0]; + assert.ok(token); + + assert.deepEqual(await response.json(), { + api_token: { + id: 1, + crate_scopes: ['serde', 'serde-*'], + created_at: '2017-11-20T11:23:45.000Z', + endpoint_scopes: ['publish-update'], + expired_at: null, + last_used_at: null, + name: 'foooo', + revoked: false, + token: token.token, + }, + }); +}); + +test('creates a new API token with expiry date', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let body = JSON.stringify({ + api_token: { + name: 'foooo', + expired_at: '2023-12-24T12:34:56Z', + }, + }); + let response = await fetch('/api/v1/me/tokens', { method: 'PUT', body }); + assert.strictEqual(response.status, 200); + + let token = db.apiToken.findMany({})[0]; + assert.ok(token); + + assert.deepEqual(await response.json(), { + api_token: { + id: 1, + crate_scopes: null, + created_at: '2017-11-20T11:23:45.000Z', + endpoint_scopes: null, + expired_at: '2023-12-24T12:34:56.000Z', + last_used_at: null, + name: 'foooo', + revoked: false, + token: token.token, + }, + }); +}); + +test('returns an error if unauthenticated', async function () { + let body = JSON.stringify({ api_token: {} }); + let response = await fetch('/api/v1/me/tokens', { method: 'PUT', body }); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 64360ea0a79..053a95d671b 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,3 +1,4 @@ +import apiTokenHandlers from './handlers/api-tokens.js'; import categoryHandlers from './handlers/categories.js'; import docsRsHandlers from './handlers/docs-rs.js'; import inviteHandlers from './handlers/invites.js'; @@ -21,6 +22,7 @@ import version from './models/version.js'; import { factory } from './utils/factory.js'; export const handlers = [ + ...apiTokenHandlers, ...categoryHandlers, ...docsRsHandlers, ...inviteHandlers, From 3abeb17d61d1a78ee83ca5dc92a84b02779e8f86 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 22 Jan 2025 15:31:28 +0100 Subject: [PATCH 052/189] msw: Implement `GET /api/v1/me/tokens` request handler --- packages/crates-io-msw/handlers/api-tokens.js | 3 +- .../crates-io-msw/handlers/api-tokens/list.js | 30 +++++++ .../handlers/api-tokens/list.test.js | 78 +++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/api-tokens/list.js create mode 100644 packages/crates-io-msw/handlers/api-tokens/list.test.js diff --git a/packages/crates-io-msw/handlers/api-tokens.js b/packages/crates-io-msw/handlers/api-tokens.js index cfdf568d58d..2108b9391f9 100644 --- a/packages/crates-io-msw/handlers/api-tokens.js +++ b/packages/crates-io-msw/handlers/api-tokens.js @@ -1,3 +1,4 @@ import createToken from './api-tokens/create.js'; +import listTokens from './api-tokens/list.js'; -export default [createToken]; +export default [createToken, listTokens]; diff --git a/packages/crates-io-msw/handlers/api-tokens/list.js b/packages/crates-io-msw/handlers/api-tokens/list.js new file mode 100644 index 00000000000..1a5759296ee --- /dev/null +++ b/packages/crates-io-msw/handlers/api-tokens/list.js @@ -0,0 +1,30 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeApiToken } from '../../serializers/api-token.js'; +import { getSession } from '../../utils/session.js'; + +export default http.get('/api/v1/me/tokens', async ({ request }) => { + let url = new URL(request.url); + + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let expiredAfter = new Date(); + if (url.searchParams.has('expired_days')) { + expiredAfter.setUTCDate(expiredAfter.getUTCDate() - url.searchParams.get('expired_days')); + } + + let apiTokens = db.apiToken + .findMany({ + where: { user: { id: { equals: user.id } } }, + orderBy: { id: 'desc' }, + }) + .filter(token => !token.expiredAt || new Date(token.expiredAt) > expiredAfter); + + return HttpResponse.json({ + api_tokens: apiTokens.map(token => serializeApiToken(token)), + }); +}); diff --git a/packages/crates-io-msw/handlers/api-tokens/list.test.js b/packages/crates-io-msw/handlers/api-tokens/list.test.js new file mode 100644 index 00000000000..221805a9167 --- /dev/null +++ b/packages/crates-io-msw/handlers/api-tokens/list.test.js @@ -0,0 +1,78 @@ +import { afterEach, assert, beforeEach, test, vi } from 'vitest'; + +import { db } from '../../index.js'; + +beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2017-11-20T12:00:00Z')); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +test('returns the list of API token for the authenticated `user`', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + db.apiToken.create({ + user, + createdAt: '2017-11-19T12:59:22Z', + crateScopes: ['serde', 'serde-*'], + endpointScopes: ['publish-update'], + }); + db.apiToken.create({ user, createdAt: '2017-11-19T13:59:22Z', expiredAt: '2023-11-20T10:59:22Z' }); + db.apiToken.create({ user, createdAt: '2017-11-19T14:59:22Z' }); + db.apiToken.create({ user, createdAt: '2017-11-19T15:59:22Z', expiredAt: '2017-11-20T10:59:22Z' }); + + let response = await fetch('/api/v1/me/tokens'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + api_tokens: [ + { + id: 3, + crate_scopes: null, + created_at: '2017-11-19T14:59:22.000Z', + endpoint_scopes: null, + expired_at: null, + last_used_at: null, + name: 'API Token 3', + }, + { + id: 2, + crate_scopes: null, + created_at: '2017-11-19T13:59:22.000Z', + endpoint_scopes: null, + expired_at: '2023-11-20T10:59:22.000Z', + last_used_at: null, + name: 'API Token 2', + }, + { + id: 1, + crate_scopes: ['serde', 'serde-*'], + created_at: '2017-11-19T12:59:22.000Z', + endpoint_scopes: ['publish-update'], + expired_at: null, + last_used_at: null, + name: 'API Token 1', + }, + ], + }); +}); + +test('empty list case', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/me/tokens'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { api_tokens: [] }); +}); + +test('returns an error if unauthenticated', async function () { + let response = await fetch('/api/v1/me/tokens'); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); From 20d7f4631fdfa73216b5cac95d26de873146f446 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 22 Jan 2025 15:37:18 +0100 Subject: [PATCH 053/189] msw: Implement `DELETE /api/v1/me/tokens/:tokenId` request handler --- packages/crates-io-msw/handlers/api-tokens.js | 3 +- .../handlers/api-tokens/delete.js | 21 ++++++++++++++ .../handlers/api-tokens/delete.test.js | 28 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/api-tokens/delete.js create mode 100644 packages/crates-io-msw/handlers/api-tokens/delete.test.js diff --git a/packages/crates-io-msw/handlers/api-tokens.js b/packages/crates-io-msw/handlers/api-tokens.js index 2108b9391f9..61e772584e1 100644 --- a/packages/crates-io-msw/handlers/api-tokens.js +++ b/packages/crates-io-msw/handlers/api-tokens.js @@ -1,4 +1,5 @@ import createToken from './api-tokens/create.js'; +import deleteToken from './api-tokens/delete.js'; import listTokens from './api-tokens/list.js'; -export default [createToken, listTokens]; +export default [createToken, listTokens, deleteToken]; diff --git a/packages/crates-io-msw/handlers/api-tokens/delete.js b/packages/crates-io-msw/handlers/api-tokens/delete.js new file mode 100644 index 00000000000..6cd4b2a4e00 --- /dev/null +++ b/packages/crates-io-msw/handlers/api-tokens/delete.js @@ -0,0 +1,21 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { getSession } from '../../utils/session.js'; + +export default http.delete('/api/v1/me/tokens/:tokenId', async ({ params }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let { tokenId } = params; + db.apiToken.delete({ + where: { + id: { equals: parseInt(tokenId) }, + user: { id: { equals: user.id } }, + }, + }); + + return HttpResponse.json({}); +}); diff --git a/packages/crates-io-msw/handlers/api-tokens/delete.test.js b/packages/crates-io-msw/handlers/api-tokens/delete.test.js new file mode 100644 index 00000000000..0db2a45a5b9 --- /dev/null +++ b/packages/crates-io-msw/handlers/api-tokens/delete.test.js @@ -0,0 +1,28 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('revokes an API token', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let token = db.apiToken.create({ user }); + + let response = await fetch(`/api/v1/me/tokens/${token.id}`, { method: 'DELETE' }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), {}); + + let tokens = db.apiToken.findMany({}); + assert.strictEqual(tokens.length, 0); +}); + +test('returns an error if unauthenticated', async function () { + let user = db.user.create(); + let token = db.apiToken.create({ user }); + + let response = await fetch(`/api/v1/me/tokens/${token.id}`, { method: 'DELETE' }); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); From 59105c0b9af12f881e61dd343993774d5741a1ab Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 22 Jan 2025 15:43:01 +0100 Subject: [PATCH 054/189] msw: Implement `GET /api/v1/me/tokens/:tokenId` request handler --- packages/crates-io-msw/handlers/api-tokens.js | 3 +- .../crates-io-msw/handlers/api-tokens/get.js | 26 +++++++++++ .../handlers/api-tokens/get.test.js | 45 +++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/api-tokens/get.js create mode 100644 packages/crates-io-msw/handlers/api-tokens/get.test.js diff --git a/packages/crates-io-msw/handlers/api-tokens.js b/packages/crates-io-msw/handlers/api-tokens.js index 61e772584e1..ac9e93265cf 100644 --- a/packages/crates-io-msw/handlers/api-tokens.js +++ b/packages/crates-io-msw/handlers/api-tokens.js @@ -1,5 +1,6 @@ import createToken from './api-tokens/create.js'; import deleteToken from './api-tokens/delete.js'; +import getToken from './api-tokens/get.js'; import listTokens from './api-tokens/list.js'; -export default [createToken, listTokens, deleteToken]; +export default [createToken, listTokens, getToken, deleteToken]; diff --git a/packages/crates-io-msw/handlers/api-tokens/get.js b/packages/crates-io-msw/handlers/api-tokens/get.js new file mode 100644 index 00000000000..2d743a52815 --- /dev/null +++ b/packages/crates-io-msw/handlers/api-tokens/get.js @@ -0,0 +1,26 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeApiToken } from '../../serializers/api-token.js'; +import { notFound } from '../../utils/handlers.js'; +import { getSession } from '../../utils/session.js'; + +export default http.get('/api/v1/me/tokens/:tokenId', async ({ params }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let { tokenId } = params; + let token = db.apiToken.findFirst({ + where: { + id: { equals: parseInt(tokenId) }, + user: { id: { equals: user.id } }, + }, + }); + if (!token) return notFound(); + + return HttpResponse.json({ + api_token: serializeApiToken(token), + }); +}); diff --git a/packages/crates-io-msw/handlers/api-tokens/get.test.js b/packages/crates-io-msw/handlers/api-tokens/get.test.js new file mode 100644 index 00000000000..7f68044cbf7 --- /dev/null +++ b/packages/crates-io-msw/handlers/api-tokens/get.test.js @@ -0,0 +1,45 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns the requested token', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let token = db.apiToken.create({ + user, + crateScopes: ['serde', 'serde-*'], + endpointScopes: ['publish-update'], + }); + + let response = await fetch(`/api/v1/me/tokens/${token.id}`); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + api_token: { + id: 1, + crate_scopes: ['serde', 'serde-*'], + created_at: '2017-11-19T17:59:22.000Z', + endpoint_scopes: ['publish-update'], + expired_at: null, + last_used_at: null, + name: 'API Token 1', + }, + }); +}); + +test('returns 404 if token not found', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/me/tokens/42'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('returns an error if unauthenticated', async function () { + let response = await fetch('/api/v1/me/tokens/42'); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); From f0da1a2fd306581298e421fe3c3ab5703fb36751 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 13:16:18 +0100 Subject: [PATCH 055/189] msw: Implement `GET /api/v1/crates` request handler --- packages/crates-io-msw/handlers/crates.js | 3 + .../crates-io-msw/handlers/crates/list.js | 83 +++++++ .../handlers/crates/list.test.js | 226 ++++++++++++++++++ packages/crates-io-msw/index.js | 2 + 4 files changed, 314 insertions(+) create mode 100644 packages/crates-io-msw/handlers/crates.js create mode 100644 packages/crates-io-msw/handlers/crates/list.js create mode 100644 packages/crates-io-msw/handlers/crates/list.test.js diff --git a/packages/crates-io-msw/handlers/crates.js b/packages/crates-io-msw/handlers/crates.js new file mode 100644 index 00000000000..3b18bf3b5b4 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates.js @@ -0,0 +1,3 @@ +import listCrates from './crates/list.js'; + +export default [listCrates]; diff --git a/packages/crates-io-msw/handlers/crates/list.js b/packages/crates-io-msw/handlers/crates/list.js new file mode 100644 index 00000000000..7afeca24b02 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/list.js @@ -0,0 +1,83 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeCrate } from '../../serializers/crate.js'; +import { pageParams } from '../../utils/handlers.js'; +import { getSession } from '../../utils/session.js'; + +export default http.get('/api/v1/crates', async ({ request }) => { + let url = new URL(request.url); + + const { start, end } = pageParams(request); + + let crates = db.crate.findMany({}); + + if (url.searchParams.get('following') === '1') { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + crates = user.followedCrates; + } + + let letter = url.searchParams.get('letter'); + if (letter) { + letter = letter.toLowerCase(); + crates = crates.filter(crate => crate.name[0].toLowerCase() === letter); + } + + let q = url.searchParams.get('q'); + if (q) { + q = q.toLowerCase(); + crates = crates.filter(crate => crate.name.toLowerCase().includes(q)); + } + + let userId = url.searchParams.get('user_id'); + if (userId) { + userId = parseInt(userId, 10); + crates = crates.filter(crate => + db.crateOwnership.findFirst({ + where: { + crate: { id: { equals: crate.id } }, + user: { id: { equals: userId } }, + }, + }), + ); + } + + let teamId = url.searchParams.get('team_id'); + if (teamId) { + teamId = parseInt(teamId, 10); + crates = crates.filter(crate => + db.crateOwnership.findFirst({ + where: { + crate: { id: { equals: crate.id } }, + team: { id: { equals: teamId } }, + }, + }), + ); + } + + let ids = url.searchParams.getAll('ids[]'); + if (ids.length !== 0) { + crates = crates.filter(crate => ids.includes(crate.name)); + } + + let sort = url.searchParams.get('sort'); + if (sort === 'alpha') { + crates = crates.sort((a, b) => compare(a.name.toLowerCase(), b.name.toLowerCase())); + } else if (sort === 'recent-downloads') { + crates = crates.sort((a, b) => b.recent_downloads - a.recent_downloads); + } + + let total = crates.length; + crates = crates.slice(start, end); + crates = crates.map(c => ({ ...serializeCrate(c), exact_match: c.name.toLowerCase() === q })); + + return HttpResponse.json({ crates, meta: { total } }); +}); + +export function compare(a, b) { + return a < b ? -1 : a > b ? 1 : 0; +} diff --git a/packages/crates-io-msw/handlers/crates/list.test.js b/packages/crates-io-msw/handlers/crates/list.test.js new file mode 100644 index 00000000000..c63ba8119f8 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/list.test.js @@ -0,0 +1,226 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('empty case', async function () { + let response = await fetch('/api/v1/crates'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + crates: [], + meta: { + total: 0, + }, + }); +}); + +test('returns a paginated crates list', async function () { + let crate = db.crate.create({ name: 'rand' }); + db.version.create({ + crate, + created_at: '2020-11-06T12:34:56Z', + num: '1.0.0', + updated_at: '2020-11-06T12:34:56Z', + }); + db.version.create({ + crate, + created_at: '2020-12-25T12:34:56Z', + num: '2.0.0-beta.1', + updated_at: '2020-12-25T12:34:56Z', + }); + + let response = await fetch('/api/v1/crates'); + // assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + crates: [ + { + id: 'rand', + badges: [], + categories: null, + created_at: '2010-06-16T21:30:45Z', + default_version: '1.0.0', + description: 'This is the description for the crate called "rand"', + documentation: null, + downloads: 37_035, + exact_match: false, + homepage: null, + keywords: null, + links: { + owner_team: '/api/v1/crates/rand/owner_team', + owner_user: '/api/v1/crates/rand/owner_user', + reverse_dependencies: '/api/v1/crates/rand/reverse_dependencies', + version_downloads: '/api/v1/crates/rand/downloads', + versions: '/api/v1/crates/rand/versions', + }, + max_version: '2.0.0-beta.1', + max_stable_version: '1.0.0', + name: 'rand', + newest_version: '2.0.0-beta.1', + repository: null, + recent_downloads: 321, + updated_at: '2017-02-24T12:34:56Z', + versions: null, + yanked: false, + }, + ], + meta: { + total: 1, + }, + }); +}); + +test('never returns more than 10 results', async function () { + let crates = Array.from({ length: 25 }, () => db.crate.create()); + crates.forEach(crate => db.version.create({ crate })); + + let response = await fetch('/api/v1/crates'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.strictEqual(responsePayload.crates.length, 10); + assert.strictEqual(responsePayload.meta.total, 25); +}); + +test('supports `page` and `per_page` parameters', async function () { + let crates = Array.from({ length: 25 }, (_, i) => + db.crate.create({ name: `crate-${String(i + 1).padStart(2, '0')}` }), + ); + crates.forEach(crate => db.version.create({ crate })); + + let response = await fetch('/api/v1/crates?page=2&per_page=5'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.strictEqual(responsePayload.crates.length, 5); + assert.deepEqual( + responsePayload.crates.map(it => it.id), + ['crate-06', 'crate-07', 'crate-08', 'crate-09', 'crate-10'], + ); + assert.strictEqual(responsePayload.meta.total, 25); +}); + +test('supports a `letter` parameter', async function () { + let foo = db.crate.create({ name: 'foo' }); + db.version.create({ crate: foo }); + let bar = db.crate.create({ name: 'bar' }); + db.version.create({ crate: bar }); + let baz = db.crate.create({ name: 'BAZ' }); + db.version.create({ crate: baz }); + + let response = await fetch('/api/v1/crates?letter=b'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.strictEqual(responsePayload.crates.length, 2); + assert.deepEqual( + responsePayload.crates.map(it => it.id), + ['bar', 'BAZ'], + ); + assert.strictEqual(responsePayload.meta.total, 2); +}); + +test('supports a `q` parameter', async function () { + let crate1 = db.crate.create({ name: '123456' }); + db.version.create({ crate: crate1 }); + let crate2 = db.crate.create({ name: '123' }); + db.version.create({ crate: crate2 }); + let crate3 = db.crate.create({ name: '87654' }); + db.version.create({ crate: crate3 }); + + let response = await fetch('/api/v1/crates?q=123'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.strictEqual(responsePayload.crates.length, 2); + assert.deepEqual( + responsePayload.crates.map(it => it.id), + ['123456', '123'], + ); + assert.deepEqual( + responsePayload.crates.map(it => it.exact_match), + [false, true], + ); + assert.strictEqual(responsePayload.meta.total, 2); +}); + +test('supports a `user_id` parameter', async function () { + let user1 = db.user.create(); + let user2 = db.user.create(); + + let foo = db.crate.create({ name: 'foo' }); + db.version.create({ crate: foo }); + let bar = db.crate.create({ name: 'bar' }); + db.crateOwnership.create({ crate: bar, user: user1 }); + db.version.create({ crate: bar }); + let baz = db.crate.create({ name: 'baz' }); + db.crateOwnership.create({ crate: baz, user: user2 }); + db.version.create({ crate: baz }); + + let response = await fetch(`/api/v1/crates?user_id=${user1.id}`); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.strictEqual(responsePayload.crates.length, 1); + assert.strictEqual(responsePayload.crates[0].id, 'bar'); + assert.strictEqual(responsePayload.meta.total, 1); +}); + +test('supports a `team_id` parameter', async function () { + let team1 = db.team.create(); + let team2 = db.team.create(); + + let foo = db.crate.create({ name: 'foo' }); + db.version.create({ crate: foo }); + let bar = db.crate.create({ name: 'bar' }); + db.crateOwnership.create({ crate: bar, team: team1 }); + db.version.create({ crate: bar }); + let baz = db.crate.create({ name: 'baz' }); + db.crateOwnership.create({ crate: baz, team: team2 }); + db.version.create({ crate: baz }); + + let response = await fetch(`/api/v1/crates?team_id=${team1.id}`); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.strictEqual(responsePayload.crates.length, 1); + assert.strictEqual(responsePayload.crates[0].id, 'bar'); + assert.strictEqual(responsePayload.meta.total, 1); +}); + +test('supports a `following` parameter', async function () { + let foo = db.crate.create({ name: 'foo' }); + db.version.create({ crate: foo }); + let bar = db.crate.create({ name: 'bar' }); + db.version.create({ crate: bar }); + + let user = db.user.create({ followedCrates: [bar] }); + db.mswSession.create({ user }); + + let response = await fetch(`/api/v1/crates?following=1`); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.strictEqual(responsePayload.crates.length, 1); + assert.strictEqual(responsePayload.crates[0].id, 'bar'); + assert.strictEqual(responsePayload.meta.total, 1); +}); + +test('supports multiple `ids[]` parameters', async function () { + let foo = db.crate.create({ name: 'foo' }); + db.version.create({ crate: foo }); + let bar = db.crate.create({ name: 'bar' }); + db.version.create({ crate: bar }); + let baz = db.crate.create({ name: 'baz' }); + db.version.create({ crate: baz }); + let other = db.crate.create({ name: 'other' }); + db.version.create({ crate: other }); + + let response = await fetch(`/api/v1/crates?ids[]=foo&ids[]=bar&ids[]=baz&ids[]=baz&ids[]=unknown`); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.strictEqual(responsePayload.crates.length, 3); + assert.strictEqual(responsePayload.crates[0].id, 'foo'); + assert.strictEqual(responsePayload.crates[1].id, 'bar'); + assert.strictEqual(responsePayload.crates[2].id, 'baz'); + assert.strictEqual(responsePayload.meta.total, 3); +}); diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 053a95d671b..0f060c2205a 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -1,5 +1,6 @@ import apiTokenHandlers from './handlers/api-tokens.js'; import categoryHandlers from './handlers/categories.js'; +import cratesHandlers from './handlers/crates.js'; import docsRsHandlers from './handlers/docs-rs.js'; import inviteHandlers from './handlers/invites.js'; import keywordHandlers from './handlers/keywords.js'; @@ -24,6 +25,7 @@ import { factory } from './utils/factory.js'; export const handlers = [ ...apiTokenHandlers, ...categoryHandlers, + ...cratesHandlers, ...docsRsHandlers, ...inviteHandlers, ...keywordHandlers, From c26e09129d07dc73926a5ee1be49cd925a4824c3 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 14:24:29 +0100 Subject: [PATCH 056/189] msw: Implement `GET /api/v1/crates/:name` request handler --- packages/crates-io-msw/handlers/crates.js | 3 +- packages/crates-io-msw/handlers/crates/get.js | 55 +++ .../crates-io-msw/handlers/crates/get.test.js | 330 ++++++++++++++++++ 3 files changed, 387 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/crates/get.js create mode 100644 packages/crates-io-msw/handlers/crates/get.test.js diff --git a/packages/crates-io-msw/handlers/crates.js b/packages/crates-io-msw/handlers/crates.js index 3b18bf3b5b4..4fd42e0303f 100644 --- a/packages/crates-io-msw/handlers/crates.js +++ b/packages/crates-io-msw/handlers/crates.js @@ -1,3 +1,4 @@ +import getCrate from './crates/get.js'; import listCrates from './crates/list.js'; -export default [listCrates]; +export default [listCrates, getCrate]; diff --git a/packages/crates-io-msw/handlers/crates/get.js b/packages/crates-io-msw/handlers/crates/get.js new file mode 100644 index 00000000000..ae2d13c3f4d --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/get.js @@ -0,0 +1,55 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeCategory } from '../../serializers/category.js'; +import { serializeCrate } from '../../serializers/crate.js'; +import { serializeKeyword } from '../../serializers/keyword.js'; +import { serializeVersion } from '../../serializers/version.js'; +import { notFound } from '../../utils/handlers.js'; + +const DEFAULT_INCLUDES = ['versions', 'keywords', 'categories']; + +export default http.get('/api/v1/crates/:name', async ({ request, params }) => { + let { name } = params; + let canonicalName = toCanonicalName(name); + let crate = db.crate.findMany({}).find(it => toCanonicalName(it.name) === canonicalName); + if (!crate) return notFound(); + + let versions = db.version.findMany({ where: { crate: { id: { equals: crate.id } } } }); + versions.sort((a, b) => b.id - a.id); + + let url = new URL(request.url); + let include = url.searchParams.get('include'); + let includes = include == null || include === 'full' ? DEFAULT_INCLUDES : include.split(','); + + let includeCategories = includes.includes('categories'); + let includeKeywords = includes.includes('keywords'); + let includeVersions = includes.includes('versions'); + let includeDefaultVersion = includes.includes('default_version'); + + let serializedCrate = serializeCrate(crate, { + calculateVersions: includeVersions, + includeCategories, + includeKeywords, + includeVersions, + }); + + let serializedVersions = null; + if (includeVersions) { + serializedVersions = versions.map(v => serializeVersion(v)); + } else if (includeDefaultVersion) { + let defaultVersion = versions.find(v => v.num === serializedCrate.default_version); + serializedVersions = [serializeVersion(defaultVersion)]; + } + + return HttpResponse.json({ + crate: serializedCrate, + categories: includeCategories ? crate.categories.map(c => serializeCategory(c)) : null, + keywords: includeKeywords ? crate.keywords.map(k => serializeKeyword(k)) : null, + versions: serializedVersions, + }); +}); + +function toCanonicalName(name) { + return name.toLowerCase().replace(/-/g, '_'); +} diff --git a/packages/crates-io-msw/handlers/crates/get.test.js b/packages/crates-io-msw/handlers/crates/get.test.js new file mode 100644 index 00000000000..9fed9f1a89b --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/get.test.js @@ -0,0 +1,330 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 404 for unknown crates', async function () { + let response = await fetch('/api/v1/crates/foo'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('returns a crate object for known crates', async function () { + let crate = db.crate.create({ name: 'rand' }); + db.version.create({ crate, num: '1.0.0-beta.1' }); + + let response = await fetch('/api/v1/crates/rand'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + categories: [], + crate: { + badges: [], + categories: [], + created_at: '2010-06-16T21:30:45Z', + default_version: '1.0.0-beta.1', + description: 'This is the description for the crate called "rand"', + documentation: null, + downloads: 37_035, + homepage: null, + id: 'rand', + keywords: [], + links: { + owner_team: '/api/v1/crates/rand/owner_team', + owner_user: '/api/v1/crates/rand/owner_user', + reverse_dependencies: '/api/v1/crates/rand/reverse_dependencies', + version_downloads: '/api/v1/crates/rand/downloads', + versions: '/api/v1/crates/rand/versions', + }, + max_version: '1.0.0-beta.1', + max_stable_version: null, + name: 'rand', + newest_version: '1.0.0-beta.1', + repository: null, + recent_downloads: 321, + updated_at: '2017-02-24T12:34:56Z', + versions: [1], + yanked: false, + }, + keywords: [], + versions: [ + { + id: 1, + crate: 'rand', + crate_size: 162_963, + created_at: '2010-06-16T21:30:45Z', + dl_path: '/api/v1/crates/rand/1.0.0-beta.1/download', + downloads: 3702, + features: {}, + license: 'MIT', + links: { + dependencies: '/api/v1/crates/rand/1.0.0-beta.1/dependencies', + version_downloads: '/api/v1/crates/rand/1.0.0-beta.1/downloads', + }, + num: '1.0.0-beta.1', + published_by: null, + readme_path: '/api/v1/crates/rand/1.0.0-beta.1/readme', + rust_version: null, + updated_at: '2017-02-24T12:34:56Z', + yanked: false, + yank_message: null, + }, + ], + }); +}); + +test('works for non-canonical names', async function () { + let crate = db.crate.create({ name: 'foo-bar' }); + db.version.create({ crate, num: '1.0.0-beta.1' }); + + let response = await fetch('/api/v1/crates/foo_bar'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + categories: [], + crate: { + badges: [], + categories: [], + created_at: '2010-06-16T21:30:45Z', + default_version: '1.0.0-beta.1', + description: 'This is the description for the crate called "foo-bar"', + documentation: null, + downloads: 37_035, + homepage: null, + id: 'foo-bar', + keywords: [], + links: { + owner_team: '/api/v1/crates/foo-bar/owner_team', + owner_user: '/api/v1/crates/foo-bar/owner_user', + reverse_dependencies: '/api/v1/crates/foo-bar/reverse_dependencies', + version_downloads: '/api/v1/crates/foo-bar/downloads', + versions: '/api/v1/crates/foo-bar/versions', + }, + max_version: '1.0.0-beta.1', + max_stable_version: null, + name: 'foo-bar', + newest_version: '1.0.0-beta.1', + repository: null, + recent_downloads: 321, + updated_at: '2017-02-24T12:34:56Z', + versions: [1], + yanked: false, + }, + keywords: [], + versions: [ + { + id: 1, + crate: 'foo-bar', + crate_size: 162_963, + created_at: '2010-06-16T21:30:45Z', + dl_path: '/api/v1/crates/foo-bar/1.0.0-beta.1/download', + downloads: 3702, + features: {}, + license: 'MIT', + links: { + dependencies: '/api/v1/crates/foo-bar/1.0.0-beta.1/dependencies', + version_downloads: '/api/v1/crates/foo-bar/1.0.0-beta.1/downloads', + }, + num: '1.0.0-beta.1', + published_by: null, + readme_path: '/api/v1/crates/foo-bar/1.0.0-beta.1/readme', + rust_version: null, + updated_at: '2017-02-24T12:34:56Z', + yanked: false, + yank_message: null, + }, + ], + }); +}); + +test('includes related versions', async function () { + let crate = db.crate.create({ name: 'rand' }); + db.version.create({ crate, num: '1.0.0' }); + db.version.create({ crate, num: '1.1.0' }); + db.version.create({ crate, num: '1.2.0' }); + + let response = await fetch('/api/v1/crates/rand'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.deepEqual(responsePayload.crate.versions, [1, 2, 3]); + assert.deepEqual(responsePayload.versions, [ + { + id: 3, + crate: 'rand', + crate_size: 488_889, + created_at: '2010-06-16T21:30:45Z', + dl_path: '/api/v1/crates/rand/1.2.0/download', + downloads: 11_106, + features: {}, + license: 'MIT/Apache-2.0', + links: { + dependencies: '/api/v1/crates/rand/1.2.0/dependencies', + version_downloads: '/api/v1/crates/rand/1.2.0/downloads', + }, + num: '1.2.0', + published_by: null, + readme_path: '/api/v1/crates/rand/1.2.0/readme', + rust_version: null, + updated_at: '2017-02-24T12:34:56Z', + yanked: false, + yank_message: null, + }, + { + id: 2, + crate: 'rand', + crate_size: 325_926, + created_at: '2010-06-16T21:30:45Z', + dl_path: '/api/v1/crates/rand/1.1.0/download', + downloads: 7404, + features: {}, + license: 'Apache-2.0', + links: { + dependencies: '/api/v1/crates/rand/1.1.0/dependencies', + version_downloads: '/api/v1/crates/rand/1.1.0/downloads', + }, + num: '1.1.0', + published_by: null, + readme_path: '/api/v1/crates/rand/1.1.0/readme', + rust_version: null, + updated_at: '2017-02-24T12:34:56Z', + yanked: false, + yank_message: null, + }, + { + id: 1, + crate: 'rand', + crate_size: 162_963, + created_at: '2010-06-16T21:30:45Z', + dl_path: '/api/v1/crates/rand/1.0.0/download', + downloads: 3702, + features: {}, + license: 'MIT', + links: { + dependencies: '/api/v1/crates/rand/1.0.0/dependencies', + version_downloads: '/api/v1/crates/rand/1.0.0/downloads', + }, + num: '1.0.0', + published_by: null, + readme_path: '/api/v1/crates/rand/1.0.0/readme', + rust_version: null, + updated_at: '2017-02-24T12:34:56Z', + yanked: false, + yank_message: null, + }, + ]); +}); + +test('includes related categories', async function () { + let noStd = db.category.create({ category: 'no-std' }); + db.category.create({ category: 'cli' }); + let crate = db.crate.create({ name: 'rand', categories: [noStd] }); + db.version.create({ crate }); + + let response = await fetch('/api/v1/crates/rand'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.deepEqual(responsePayload.crate.categories, ['no-std']); + assert.deepEqual(responsePayload.categories, [ + { + id: 'no-std', + category: 'no-std', + crates_cnt: 1, + created_at: '2010-06-16T21:30:45Z', + description: 'This is the description for the category called "no-std"', + slug: 'no-std', + }, + ]); +}); + +test('includes related keywords', async function () { + let noStd = db.keyword.create({ keyword: 'no-std' }); + db.keyword.create({ keyword: 'cli' }); + let crate = db.crate.create({ name: 'rand', keywords: [noStd] }); + db.version.create({ crate }); + + let response = await fetch('/api/v1/crates/rand'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.deepEqual(responsePayload.crate.keywords, ['no-std']); + assert.deepEqual(responsePayload.keywords, [ + { + crates_cnt: 1, + id: 'no-std', + keyword: 'no-std', + }, + ]); +}); + +test('without versions included', async function () { + db.category.create({ category: 'no-std' }); + db.category.create({ category: 'cli' }); + db.keyword.create({ keyword: 'no-std' }); + db.keyword.create({ keyword: 'cli' }); + let crate = db.crate.create({ name: 'rand', categoryIds: ['no-std'], keywordIds: ['no-std'] }); + db.version.create({ crate, num: '1.0.0' }); + db.version.create({ crate, num: '1.1.0' }); + db.version.create({ crate, num: '1.2.0' }); + + let req = await fetch('/api/v1/crates/rand'); + let expected = await req.json(); + + let response = await fetch('/api/v1/crates/rand?include=keywords,categories'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.deepEqual(responsePayload, { + ...expected, + crate: { + ...expected.crate, + max_version: '0.0.0', + newest_version: '0.0.0', + max_stable_version: null, + versions: null, + }, + versions: null, + }); +}); + +test('includes default_version', async function () { + let crate = db.crate.create({ name: 'rand' }); + db.version.create({ crate, num: '1.0.0' }); + db.version.create({ crate, num: '1.1.0' }); + db.version.create({ crate, num: '1.2.0' }); + + let req = await fetch('/api/v1/crates/rand'); + let expected = await req.json(); + + let response = await fetch('/api/v1/crates/rand?include=default_version'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + let default_version = expected.versions.find(it => it.num === responsePayload.crate.default_version); + assert.deepEqual(responsePayload, { + ...expected, + crate: { + ...expected.crate, + categories: null, + keywords: null, + max_version: '0.0.0', + newest_version: '0.0.0', + max_stable_version: null, + versions: null, + }, + categories: null, + keywords: null, + versions: [default_version], + }); + + let resp_both = await fetch('/api/v1/crates/rand?include=versions,default_version'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await resp_both.json(), { + ...expected, + crate: { + ...expected.crate, + categories: null, + keywords: null, + }, + categories: null, + keywords: null, + }); +}); From cb91248a2608105c76492bcb53d3f0f34379be56 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 14:46:29 +0100 Subject: [PATCH 057/189] msw: Implement `GET /api/v1/crates/:name/versions` request handler --- packages/crates-io-msw/handlers/versions.js | 3 + .../crates-io-msw/handlers/versions/list.js | 35 ++++ .../handlers/versions/list.test.js | 150 ++++++++++++++++++ packages/crates-io-msw/index.js | 2 + .../crates-io-msw/utils/release-tracks.js | 16 ++ 5 files changed, 206 insertions(+) create mode 100644 packages/crates-io-msw/handlers/versions.js create mode 100644 packages/crates-io-msw/handlers/versions/list.js create mode 100644 packages/crates-io-msw/handlers/versions/list.test.js create mode 100644 packages/crates-io-msw/utils/release-tracks.js diff --git a/packages/crates-io-msw/handlers/versions.js b/packages/crates-io-msw/handlers/versions.js new file mode 100644 index 00000000000..b113cd56eac --- /dev/null +++ b/packages/crates-io-msw/handlers/versions.js @@ -0,0 +1,3 @@ +import listVersions from './versions/list.js'; + +export default [listVersions]; diff --git a/packages/crates-io-msw/handlers/versions/list.js b/packages/crates-io-msw/handlers/versions/list.js new file mode 100644 index 00000000000..a122846b162 --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/list.js @@ -0,0 +1,35 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeVersion } from '../../serializers/version.js'; +import { notFound } from '../../utils/handlers.js'; +import { calculateReleaseTracks } from '../../utils/release-tracks.js'; + +export default http.get('/api/v1/crates/:name/versions', async ({ request, params }) => { + let { name } = params; + let crate = db.crate.findFirst({ where: { name: { equals: name } } }); + if (!crate) return notFound(); + + let versions = db.version.findMany({ where: { crate: { id: { equals: crate.id } } } }); + + let url = new URL(request.url); + let nums = url.searchParams.getAll('nums[]'); + if (nums.length !== 0) { + versions = versions.filter(v => nums.includes(v.num)); + } + + versions.sort((a, b) => b.id - a.id); + let total = versions.length; + + let include = url.searchParams.get('include') ?? ''; + let includes = include ? include.split(',') : []; + + let serializedVersions = versions.map(v => serializeVersion(v, { includePublishedBy: true })); + let meta = { total, next_page: null }; + + if (includes.includes('release_tracks')) { + meta.release_tracks = calculateReleaseTracks(versions); + } + + return HttpResponse.json({ versions: serializedVersions, meta }); +}); diff --git a/packages/crates-io-msw/handlers/versions/list.test.js b/packages/crates-io-msw/handlers/versions/list.test.js new file mode 100644 index 00000000000..7d034682ebb --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/list.test.js @@ -0,0 +1,150 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 404 for unknown crates', async function () { + let response = await fetch('/api/v1/crates/foo/versions'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('empty case', async function () { + db.crate.create({ name: 'rand' }); + + let response = await fetch('/api/v1/crates/rand/versions'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + versions: [], + meta: { total: 0, next_page: null }, + }); +}); + +test('returns all versions belonging to the specified crate', async function () { + let user = db.user.create(); + let crate = db.crate.create({ name: 'rand' }); + db.version.create({ crate, num: '1.0.0' }); + db.version.create({ crate, num: '1.1.0', publishedBy: user }); + db.version.create({ crate, num: '1.2.0', rust_version: '1.69' }); + + let response = await fetch('/api/v1/crates/rand/versions'); + // assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + versions: [ + { + id: 3, + crate: 'rand', + crate_size: 488_889, + created_at: '2010-06-16T21:30:45Z', + dl_path: '/api/v1/crates/rand/1.2.0/download', + downloads: 11_106, + features: {}, + license: 'MIT/Apache-2.0', + links: { + dependencies: '/api/v1/crates/rand/1.2.0/dependencies', + version_downloads: '/api/v1/crates/rand/1.2.0/downloads', + }, + num: '1.2.0', + published_by: null, + readme_path: '/api/v1/crates/rand/1.2.0/readme', + rust_version: '1.69', + updated_at: '2017-02-24T12:34:56Z', + yanked: false, + yank_message: null, + }, + { + id: 2, + crate: 'rand', + crate_size: 325_926, + created_at: '2010-06-16T21:30:45Z', + dl_path: '/api/v1/crates/rand/1.1.0/download', + downloads: 7404, + features: {}, + license: 'Apache-2.0', + links: { + dependencies: '/api/v1/crates/rand/1.1.0/dependencies', + version_downloads: '/api/v1/crates/rand/1.1.0/downloads', + }, + num: '1.1.0', + published_by: { + id: 1, + avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', + login: 'user-1', + name: 'User 1', + url: 'https://github.com/user-1', + }, + readme_path: '/api/v1/crates/rand/1.1.0/readme', + rust_version: null, + updated_at: '2017-02-24T12:34:56Z', + yanked: false, + yank_message: null, + }, + { + id: 1, + crate: 'rand', + crate_size: 162_963, + created_at: '2010-06-16T21:30:45Z', + dl_path: '/api/v1/crates/rand/1.0.0/download', + downloads: 3702, + features: {}, + license: 'MIT', + links: { + dependencies: '/api/v1/crates/rand/1.0.0/dependencies', + version_downloads: '/api/v1/crates/rand/1.0.0/downloads', + }, + num: '1.0.0', + published_by: null, + readme_path: '/api/v1/crates/rand/1.0.0/readme', + rust_version: null, + updated_at: '2017-02-24T12:34:56Z', + yanked: false, + yank_message: null, + }, + ], + meta: { total: 3, next_page: null }, + }); +}); + +test('supports multiple `ids[]` parameters', async function () { + let user = db.user.create(); + let crate = db.crate.create({ name: 'rand' }); + db.version.create({ crate, num: '1.0.0' }); + db.version.create({ crate, num: '1.1.0', publishedBy: user }); + db.version.create({ crate, num: '1.2.0', rust_version: '1.69' }); + let response = await fetch('/api/v1/crates/rand/versions?nums[]=1.0.0&nums[]=1.2.0'); + assert.strictEqual(response.status, 200); + let json = await response.json(); + assert.deepEqual( + json.versions.map(v => v.num), + ['1.2.0', '1.0.0'], + ); +}); + +test('include `release_tracks` meta', async function () { + let user = db.user.create(); + let crate = db.crate.create({ name: 'rand' }); + db.version.create({ crate, num: '0.0.1' }); + db.version.create({ crate, num: '0.0.2', yanked: true }); + db.version.create({ crate, num: '1.0.0' }); + db.version.create({ crate, num: '1.1.0', publishedBy: user }); + db.version.create({ crate, num: '1.2.0', rust_version: '1.69', yanked: true }); + + let req = await fetch('/api/v1/crates/rand/versions'); + let expected = await req.json(); + + let response = await fetch('/api/v1/crates/rand/versions?include=release_tracks'); + // assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + ...expected, + meta: { + ...expected.meta, + release_tracks: { + '0.0': { + highest: '0.0.1', + }, + 1: { + highest: '1.1.0', + }, + }, + }, + }); +}); diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 0f060c2205a..1ecbb424b6c 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -8,6 +8,7 @@ import metadataHandlers from './handlers/metadata.js'; import sessionHandlers from './handlers/sessions.js'; import teamHandlers from './handlers/teams.js'; import userHandlers from './handlers/users.js'; +import versionHandlers from './handlers/versions.js'; import apiToken from './models/api-token.js'; import category from './models/category.js'; import crateOwnerInvitation from './models/crate-owner-invitation.js'; @@ -33,6 +34,7 @@ export const handlers = [ ...sessionHandlers, ...teamHandlers, ...userHandlers, + ...versionHandlers, ]; export const db = factory({ diff --git a/packages/crates-io-msw/utils/release-tracks.js b/packages/crates-io-msw/utils/release-tracks.js new file mode 100644 index 00000000000..16518e39956 --- /dev/null +++ b/packages/crates-io-msw/utils/release-tracks.js @@ -0,0 +1,16 @@ +import semverParse from 'semver/functions/parse.js'; +import semverSort from 'semver/functions/rsort.js'; + +export function calculateReleaseTracks(versions) { + let versionNums = versions.filter(it => !it.yanked).map(it => it.num); + semverSort(versionNums, { loose: true }); + let tracks = {}; + for (let num of versionNums) { + let semver = semverParse(num, { loose: true }); + if (!semver || semver.prerelease.length !== 0) continue; + let name = semver.major == 0 ? `0.${semver.minor}` : `${semver.major}`; + if (name in tracks) continue; + tracks[name] = { highest: num }; + } + return tracks; +} From 68a3f8dd315b3d33dacc557fad45880721eea744 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 15:37:52 +0100 Subject: [PATCH 058/189] msw: Implement `GET /api/v1/crates/:name/:version` request handler --- packages/crates-io-msw/handlers/versions.js | 3 +- .../crates-io-msw/handlers/versions/get.js | 25 ++++++++++ .../handlers/versions/get.test.js | 50 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/versions/get.js create mode 100644 packages/crates-io-msw/handlers/versions/get.test.js diff --git a/packages/crates-io-msw/handlers/versions.js b/packages/crates-io-msw/handlers/versions.js index b113cd56eac..19bdfa5c0aa 100644 --- a/packages/crates-io-msw/handlers/versions.js +++ b/packages/crates-io-msw/handlers/versions.js @@ -1,3 +1,4 @@ +import getVersion from './versions/get.js'; import listVersions from './versions/list.js'; -export default [listVersions]; +export default [listVersions, getVersion]; diff --git a/packages/crates-io-msw/handlers/versions/get.js b/packages/crates-io-msw/handlers/versions/get.js new file mode 100644 index 00000000000..114e3960728 --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/get.js @@ -0,0 +1,25 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeVersion } from '../../serializers/version.js'; +import { notFound } from '../../utils/handlers.js'; + +export default http.get('/api/v1/crates/:name/:version', async ({ params }) => { + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) return notFound(); + + let version = db.version.findFirst({ + where: { + crate: { id: { equals: crate.id } }, + num: { equals: params.version }, + }, + }); + if (!version) { + let errorMessage = `crate \`${crate.name}\` does not have a version \`${params.version}\``; + return HttpResponse.json({ errors: [{ detail: errorMessage }] }, { status: 404 }); + } + + return HttpResponse.json({ + version: serializeVersion(version), + }); +}); diff --git a/packages/crates-io-msw/handlers/versions/get.test.js b/packages/crates-io-msw/handlers/versions/get.test.js new file mode 100644 index 00000000000..6c116c90a0c --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/get.test.js @@ -0,0 +1,50 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 404 for unknown crates', async function () { + let response = await fetch('/api/v1/crates/foo/1.0.0-beta.1'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('returns 404 for unknown versions', async function () { + let crate = db.crate.create({ name: 'rand' }); + db.version.create({ crate, num: '1.0.0-alpha.1' }); + let response = await fetch('/api/v1/crates/rand/1.0.0-beta.1'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'crate `rand` does not have a version `1.0.0-beta.1`' }], + }); +}); + +test('returns a version object for known version', async function () { + let crate = db.crate.create({ name: 'rand' }); + db.version.create({ crate, num: '1.0.0-beta.1' }); + + let response = await fetch('/api/v1/crates/rand/1.0.0-beta.1'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + version: { + crate: 'rand', + crate_size: 162_963, + created_at: '2010-06-16T21:30:45Z', + dl_path: '/api/v1/crates/rand/1.0.0-beta.1/download', + downloads: 3702, + features: {}, + id: 1, + license: 'MIT', + links: { + dependencies: '/api/v1/crates/rand/1.0.0-beta.1/dependencies', + version_downloads: '/api/v1/crates/rand/1.0.0-beta.1/downloads', + }, + num: '1.0.0-beta.1', + published_by: null, + readme_path: '/api/v1/crates/rand/1.0.0-beta.1/readme', + rust_version: null, + updated_at: '2017-02-24T12:34:56Z', + yank_message: null, + yanked: false, + }, + }); +}); From 87e1a9f1ce6a6fd0c1e4bf81b01887991060faa9 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 16:02:14 +0100 Subject: [PATCH 059/189] msw: Implement `DELETE /api/v1/crates/:name` request handler --- packages/crates-io-msw/handlers/crates.js | 3 +- .../crates-io-msw/handlers/crates/delete.js | 20 +++++++++++ .../handlers/crates/delete.test.js | 34 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/crates/delete.js create mode 100644 packages/crates-io-msw/handlers/crates/delete.test.js diff --git a/packages/crates-io-msw/handlers/crates.js b/packages/crates-io-msw/handlers/crates.js index 4fd42e0303f..fcc15ceb8f1 100644 --- a/packages/crates-io-msw/handlers/crates.js +++ b/packages/crates-io-msw/handlers/crates.js @@ -1,4 +1,5 @@ +import deleteCrate from './crates/delete.js'; import getCrate from './crates/get.js'; import listCrates from './crates/list.js'; -export default [listCrates, getCrate]; +export default [listCrates, getCrate, deleteCrate]; diff --git a/packages/crates-io-msw/handlers/crates/delete.js b/packages/crates-io-msw/handlers/crates/delete.js new file mode 100644 index 00000000000..15bcc4422cb --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/delete.js @@ -0,0 +1,20 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { getSession } from '../../utils/session.js'; + +export default http.delete('/api/v1/crates/:name', async ({ params }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) { + return HttpResponse.json({ errors: [{ detail: `crate \`${params.name}\` does not exist` }] }, { status: 404 }); + } + + db.crate.delete({ where: { id: crate.id } }); + + return new HttpResponse(null, { status: 204 }); +}); diff --git a/packages/crates-io-msw/handlers/crates/delete.test.js b/packages/crates-io-msw/handlers/crates/delete.test.js new file mode 100644 index 00000000000..aeff3e6382b --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/delete.test.js @@ -0,0 +1,34 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 403 if unauthenticated', async function () { + let response = await fetch('/api/v1/crates/foo', { method: 'DELETE' }); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); + +test('returns 404 for unknown crates', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/foo', { method: 'DELETE' }); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'crate `foo` does not exist' }] }); +}); + +test('deletes crates', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let crate = db.crate.create({ name: 'foo' }); + db.crateOwnership.create({ crate, user }); + + let response = await fetch('/api/v1/crates/foo', { method: 'DELETE' }); + assert.strictEqual(response.status, 204); + assert.deepEqual(await response.text(), ''); + + assert.strictEqual(db.crate.findFirst({ where: { name: { equals: 'foo' } } }), null); +}); From cbe86e248482985d9fc1726aa37c56fb4a15e66a Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 16:09:47 +0100 Subject: [PATCH 060/189] msw: Implement `GET /api/v1/crates/:name/following` request handler --- packages/crates-io-msw/handlers/crates.js | 3 +- .../handlers/crates/following.js | 21 ++++++++++ .../handlers/crates/following.test.js | 42 +++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/crates/following.js create mode 100644 packages/crates-io-msw/handlers/crates/following.test.js diff --git a/packages/crates-io-msw/handlers/crates.js b/packages/crates-io-msw/handlers/crates.js index fcc15ceb8f1..1611e3a1371 100644 --- a/packages/crates-io-msw/handlers/crates.js +++ b/packages/crates-io-msw/handlers/crates.js @@ -1,5 +1,6 @@ import deleteCrate from './crates/delete.js'; +import following from './crates/following.js'; import getCrate from './crates/get.js'; import listCrates from './crates/list.js'; -export default [listCrates, getCrate, deleteCrate]; +export default [listCrates, getCrate, deleteCrate, following]; diff --git a/packages/crates-io-msw/handlers/crates/following.js b/packages/crates-io-msw/handlers/crates/following.js new file mode 100644 index 00000000000..4d248d21892 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/following.js @@ -0,0 +1,21 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { notFound } from '../../utils/handlers.js'; +import { getSession } from '../../utils/session.js'; + +export default http.get('/api/v1/crates/:name/following', async ({ params }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) { + return notFound(); + } + + let following = user.followedCrates.includes(crate); + + return HttpResponse.json({ following }); +}); diff --git a/packages/crates-io-msw/handlers/crates/following.test.js b/packages/crates-io-msw/handlers/crates/following.test.js new file mode 100644 index 00000000000..06828af29f2 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/following.test.js @@ -0,0 +1,42 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 403 if unauthenticated', async function () { + let response = await fetch('/api/v1/crates/foo/following'); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); + +test('returns 404 for unknown crates', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/foo/following'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('returns true if the authenticated user follows the crate', async function () { + let crate = db.crate.create({ name: 'rand' }); + + let user = db.user.create({ followedCrates: [crate] }); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/rand/following'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { following: true }); +}); + +test('returns false if the authenticated user is not following the crate', async function () { + db.crate.create({ name: 'rand' }); + + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/rand/following'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { following: false }); +}); From 4206f5a59eb5b11b1851cdaf21328756739f9c66 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 16:13:43 +0100 Subject: [PATCH 061/189] msw: Implement `PUT /api/v1/crates/:name/follow` request handler --- packages/crates-io-msw/handlers/crates.js | 3 +- .../crates-io-msw/handlers/crates/follow.js | 26 ++++++++++++++ .../handlers/crates/follow.test.js | 36 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/crates/follow.js create mode 100644 packages/crates-io-msw/handlers/crates/follow.test.js diff --git a/packages/crates-io-msw/handlers/crates.js b/packages/crates-io-msw/handlers/crates.js index 1611e3a1371..433df5be768 100644 --- a/packages/crates-io-msw/handlers/crates.js +++ b/packages/crates-io-msw/handlers/crates.js @@ -1,6 +1,7 @@ import deleteCrate from './crates/delete.js'; +import followCrate from './crates/follow.js'; import following from './crates/following.js'; import getCrate from './crates/get.js'; import listCrates from './crates/list.js'; -export default [listCrates, getCrate, deleteCrate, following]; +export default [listCrates, getCrate, deleteCrate, following, followCrate]; diff --git a/packages/crates-io-msw/handlers/crates/follow.js b/packages/crates-io-msw/handlers/crates/follow.js new file mode 100644 index 00000000000..0c716342450 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/follow.js @@ -0,0 +1,26 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { notFound } from '../../utils/handlers.js'; +import { getSession } from '../../utils/session.js'; + +export default http.put('/api/v1/crates/:name/follow', async ({ params }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) { + return notFound(); + } + + db.user.update({ + where: { id: { equals: user.id } }, + data: { + followedCrates: [...user.followedCrates.filter(c => c.id !== crate.id), crate], + }, + }); + + return HttpResponse.json({ ok: true }); +}); diff --git a/packages/crates-io-msw/handlers/crates/follow.test.js b/packages/crates-io-msw/handlers/crates/follow.test.js new file mode 100644 index 00000000000..e0eafa98806 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/follow.test.js @@ -0,0 +1,36 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 403 if unauthenticated', async function () { + let response = await fetch('/api/v1/crates/foo/follow', { method: 'PUT' }); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); + +test('returns 404 for unknown crates', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/foo/follow', { method: 'PUT' }); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('makes the authenticated user follow the crate', async function () { + let crate = db.crate.create({ name: 'rand' }); + + let user = db.user.create(); + db.mswSession.create({ user }); + + assert.deepEqual(user.followedCrates, []); + + let response = await fetch('/api/v1/crates/rand/follow', { method: 'PUT' }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { ok: true }); + + user = db.user.findFirst({ where: { id: user.id } }); + assert.deepEqual(user.followedCrates, [crate]); +}); From e2fbce4b55def2470496c24855f9abf9422466a5 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 16:16:22 +0100 Subject: [PATCH 062/189] msw: Implement `DELETE /api/v1/crates/:name/follow` request handler --- packages/crates-io-msw/handlers/crates.js | 3 +- .../crates-io-msw/handlers/crates/unfollow.js | 26 ++++++++++++++ .../handlers/crates/unfollow.test.js | 36 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/crates/unfollow.js create mode 100644 packages/crates-io-msw/handlers/crates/unfollow.test.js diff --git a/packages/crates-io-msw/handlers/crates.js b/packages/crates-io-msw/handlers/crates.js index 433df5be768..5e0fdf08a64 100644 --- a/packages/crates-io-msw/handlers/crates.js +++ b/packages/crates-io-msw/handlers/crates.js @@ -3,5 +3,6 @@ import followCrate from './crates/follow.js'; import following from './crates/following.js'; import getCrate from './crates/get.js'; import listCrates from './crates/list.js'; +import unfollowCrate from './crates/unfollow.js'; -export default [listCrates, getCrate, deleteCrate, following, followCrate]; +export default [listCrates, getCrate, deleteCrate, following, followCrate, unfollowCrate]; diff --git a/packages/crates-io-msw/handlers/crates/unfollow.js b/packages/crates-io-msw/handlers/crates/unfollow.js new file mode 100644 index 00000000000..2701fc61b5d --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/unfollow.js @@ -0,0 +1,26 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { notFound } from '../../utils/handlers.js'; +import { getSession } from '../../utils/session.js'; + +export default http.delete('/api/v1/crates/:name/follow', async ({ params }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) { + return notFound(); + } + + db.user.update({ + where: { id: { equals: user.id } }, + data: { + followedCrates: user.followedCrates.filter(c => c.id !== crate.id), + }, + }); + + return HttpResponse.json({ ok: true }); +}); diff --git a/packages/crates-io-msw/handlers/crates/unfollow.test.js b/packages/crates-io-msw/handlers/crates/unfollow.test.js new file mode 100644 index 00000000000..2ca9621ab4b --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/unfollow.test.js @@ -0,0 +1,36 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 403 if unauthenticated', async function () { + let response = await fetch('/api/v1/crates/foo/follow', { method: 'DELETE' }); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); + +test('returns 404 for unknown crates', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/foo/follow', { method: 'DELETE' }); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('makes the authenticated user unfollow the crate', async function () { + let crate = db.crate.create({ name: 'rand' }); + + let user = db.user.create({ followedCrates: [crate] }); + db.mswSession.create({ user }); + + assert.deepEqual(user.followedCrates, [crate]); + + let response = await fetch('/api/v1/crates/rand/follow', { method: 'DELETE' }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { ok: true }); + + user = db.user.findFirst({ where: { id: user.id } }); + assert.deepEqual(user.followedCrates, []); +}); From def0e95433470f3c5ee6b3e72d5ae43b45c37e18 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 16:28:24 +0100 Subject: [PATCH 063/189] msw: Implement `GET /api/v1/crates/:name/:version/dependencies` request handler --- packages/crates-io-msw/handlers/versions.js | 3 +- .../handlers/versions/dependencies.js | 27 +++++++ .../handlers/versions/dependencies.test.js | 80 +++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/versions/dependencies.js create mode 100644 packages/crates-io-msw/handlers/versions/dependencies.test.js diff --git a/packages/crates-io-msw/handlers/versions.js b/packages/crates-io-msw/handlers/versions.js index 19bdfa5c0aa..98af9d4b8e6 100644 --- a/packages/crates-io-msw/handlers/versions.js +++ b/packages/crates-io-msw/handlers/versions.js @@ -1,4 +1,5 @@ +import dependencies from './versions/dependencies.js'; import getVersion from './versions/get.js'; import listVersions from './versions/list.js'; -export default [listVersions, getVersion]; +export default [listVersions, getVersion, dependencies]; diff --git a/packages/crates-io-msw/handlers/versions/dependencies.js b/packages/crates-io-msw/handlers/versions/dependencies.js new file mode 100644 index 00000000000..205142b5b5a --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/dependencies.js @@ -0,0 +1,27 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeDependency } from '../../serializers/dependency.js'; +import { notFound } from '../../utils/handlers.js'; + +export default http.get('/api/v1/crates/:name/:version/dependencies', async ({ params }) => { + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) return notFound(); + + let version = db.version.findFirst({ + where: { + crate: { id: { equals: crate.id } }, + num: { equals: params.version }, + }, + }); + if (!version) { + let errorMessage = `crate \`${crate.name}\` does not have a version \`${params.version}\``; + return HttpResponse.json({ errors: [{ detail: errorMessage }] }, { status: 404 }); + } + + let dependencies = db.dependency.findMany({ where: { version: { id: { equals: version.id } } } }); + + return HttpResponse.json({ + dependencies: dependencies.map(d => serializeDependency(d)), + }); +}); diff --git a/packages/crates-io-msw/handlers/versions/dependencies.test.js b/packages/crates-io-msw/handlers/versions/dependencies.test.js new file mode 100644 index 00000000000..a6f96601ccb --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/dependencies.test.js @@ -0,0 +1,80 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 404 for unknown crates', async function () { + let response = await fetch('/api/v1/crates/foo/1.0.0/dependencies'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('returns 404 for unknown versions', async function () { + db.crate.create({ name: 'rand' }); + + let response = await fetch('/api/v1/crates/rand/1.0.0/dependencies'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'crate `rand` does not have a version `1.0.0`' }] }); +}); + +test('empty case', async function () { + let crate = db.crate.create({ name: 'rand' }); + db.version.create({ crate, num: '1.0.0' }); + + let response = await fetch('/api/v1/crates/rand/1.0.0/dependencies'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + dependencies: [], + }); +}); + +test('returns a list of dependencies belonging to the specified crate version', async function () { + let crate = db.crate.create({ name: 'rand' }); + let version = db.version.create({ crate, num: '1.0.0' }); + + let foo = db.crate.create({ name: 'foo' }); + db.dependency.create({ crate: foo, version }); + let bar = db.crate.create({ name: 'bar' }); + db.dependency.create({ crate: bar, version }); + let baz = db.crate.create({ name: 'baz' }); + db.dependency.create({ crate: baz, version }); + + let response = await fetch('/api/v1/crates/rand/1.0.0/dependencies'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + dependencies: [ + { + id: 1, + crate_id: 'foo', + default_features: false, + features: [], + kind: 'normal', + optional: true, + req: '^2.1.3', + target: null, + version_id: 1, + }, + { + id: 2, + crate_id: 'bar', + default_features: false, + features: [], + kind: 'normal', + optional: true, + req: '0.3.7', + target: null, + version_id: 1, + }, + { + id: 3, + crate_id: 'baz', + default_features: true, + features: [], + kind: 'dev', + optional: false, + req: '~5.2.12', + target: null, + version_id: 1, + }, + ], + }); +}); From f2cd3d9824732005a9b056c5b62c0721ccfecacd Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 16:32:44 +0100 Subject: [PATCH 064/189] msw: Implement `GET /api/v1/crates/:name/:version/downloads` request handler --- packages/crates-io-msw/handlers/versions.js | 3 +- .../handlers/versions/downloads.js | 30 ++++++++++ .../handlers/versions/downloads.test.js | 58 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/versions/downloads.js create mode 100644 packages/crates-io-msw/handlers/versions/downloads.test.js diff --git a/packages/crates-io-msw/handlers/versions.js b/packages/crates-io-msw/handlers/versions.js index 98af9d4b8e6..1306e9c4435 100644 --- a/packages/crates-io-msw/handlers/versions.js +++ b/packages/crates-io-msw/handlers/versions.js @@ -1,5 +1,6 @@ import dependencies from './versions/dependencies.js'; +import downloads from './versions/downloads.js'; import getVersion from './versions/get.js'; import listVersions from './versions/list.js'; -export default [listVersions, getVersion, dependencies]; +export default [listVersions, getVersion, dependencies, downloads]; diff --git a/packages/crates-io-msw/handlers/versions/downloads.js b/packages/crates-io-msw/handlers/versions/downloads.js new file mode 100644 index 00000000000..112a6100809 --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/downloads.js @@ -0,0 +1,30 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { notFound } from '../../utils/handlers.js'; + +export default http.get('/api/v1/crates/:name/:version/downloads', async ({ params }) => { + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) return notFound(); + + let version = db.version.findFirst({ + where: { + crate: { id: { equals: crate.id } }, + num: { equals: params.version }, + }, + }); + if (!version) { + let errorMessage = `crate \`${crate.name}\` does not have a version \`${params.version}\``; + return HttpResponse.json({ errors: [{ detail: errorMessage }] }, { status: 404 }); + } + + let downloads = db.versionDownload.findMany({ where: { version: { id: { equals: version.id } } } }); + + return HttpResponse.json({ + version_downloads: downloads.map(download => ({ + date: download.date, + downloads: download.downloads, + version: download.version.id, + })), + }); +}); diff --git a/packages/crates-io-msw/handlers/versions/downloads.test.js b/packages/crates-io-msw/handlers/versions/downloads.test.js new file mode 100644 index 00000000000..acca83fd730 --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/downloads.test.js @@ -0,0 +1,58 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 404 for unknown crates', async function () { + let response = await fetch('/api/v1/crates/foo/1.0.0/downloads'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('returns 404 for unknown versions', async function () { + db.crate.create({ name: 'rand' }); + + let response = await fetch('/api/v1/crates/rand/1.0.0/downloads'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'crate `rand` does not have a version `1.0.0`' }] }); +}); + +test('empty case', async function () { + let crate = db.crate.create({ name: 'rand' }); + db.version.create({ crate, num: '1.0.0' }); + + let response = await fetch('/api/v1/crates/rand/1.0.0/downloads'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + version_downloads: [], + }); +}); + +test('returns a list of version downloads belonging to the specified crate version', async function () { + let crate = db.crate.create({ name: 'rand' }); + let version = db.version.create({ crate, num: '1.0.0' }); + db.versionDownload.create({ version, date: '2020-01-13' }); + db.versionDownload.create({ version, date: '2020-01-14' }); + db.versionDownload.create({ version, date: '2020-01-15' }); + + let response = await fetch('/api/v1/crates/rand/1.0.0/downloads'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + version_downloads: [ + { + date: '2020-01-13', + downloads: 7035, + version: 1, + }, + { + date: '2020-01-14', + downloads: 14_070, + version: 1, + }, + { + date: '2020-01-15', + downloads: 21_105, + version: 1, + }, + ], + }); +}); From 56334bb8d715e4b4e93ee904afa0cfb22327bbf7 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 16:44:54 +0100 Subject: [PATCH 065/189] msw: Implement `GET /api/v1/crates/:name/owner_user` request handler --- packages/crates-io-msw/handlers/crates.js | 3 +- .../handlers/crates/user-owners.js | 18 +++++++++ .../handlers/crates/user-owners.test.js | 40 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/crates/user-owners.js create mode 100644 packages/crates-io-msw/handlers/crates/user-owners.test.js diff --git a/packages/crates-io-msw/handlers/crates.js b/packages/crates-io-msw/handlers/crates.js index 5e0fdf08a64..264f65dd886 100644 --- a/packages/crates-io-msw/handlers/crates.js +++ b/packages/crates-io-msw/handlers/crates.js @@ -4,5 +4,6 @@ import following from './crates/following.js'; import getCrate from './crates/get.js'; import listCrates from './crates/list.js'; import unfollowCrate from './crates/unfollow.js'; +import userOwners from './crates/user-owners.js'; -export default [listCrates, getCrate, deleteCrate, following, followCrate, unfollowCrate]; +export default [listCrates, getCrate, deleteCrate, following, followCrate, unfollowCrate, userOwners]; diff --git a/packages/crates-io-msw/handlers/crates/user-owners.js b/packages/crates-io-msw/handlers/crates/user-owners.js new file mode 100644 index 00000000000..1182cdba937 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/user-owners.js @@ -0,0 +1,18 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeUser } from '../../serializers/user.js'; +import { notFound } from '../../utils/handlers.js'; + +export default http.get('/api/v1/crates/:name/owner_user', async ({ params }) => { + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) { + return notFound(); + } + + let ownerships = db.crateOwnership.findMany({ where: { crate: { id: { equals: crate.id } } } }); + + return HttpResponse.json({ + users: ownerships.filter(o => o.user).map(o => ({ ...serializeUser(o.user), kind: 'user' })), + }); +}); diff --git a/packages/crates-io-msw/handlers/crates/user-owners.test.js b/packages/crates-io-msw/handlers/crates/user-owners.test.js new file mode 100644 index 00000000000..e5426153674 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/user-owners.test.js @@ -0,0 +1,40 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 404 for unknown crates', async function () { + let response = await fetch('/api/v1/crates/foo/owner_user'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('empty case', async function () { + db.crate.create({ name: 'rand' }); + + let response = await fetch('/api/v1/crates/rand/owner_user'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + users: [], + }); +}); + +test('returns the list of users that own the specified crate', async function () { + let user = db.user.create({ name: 'John Doe' }); + let crate = db.crate.create({ name: 'rand' }); + db.crateOwnership.create({ crate, user }); + + let response = await fetch('/api/v1/crates/rand/owner_user'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + users: [ + { + id: 1, + avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', + kind: 'user', + login: 'john-doe', + name: 'John Doe', + url: 'https://github.com/john-doe', + }, + ], + }); +}); From 8024266a31b0672568655cb166265263ad11d5d7 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 16:47:44 +0100 Subject: [PATCH 066/189] msw: Implement `GET /api/v1/crates/:name/owner_team` request handler --- packages/crates-io-msw/handlers/crates.js | 3 +- .../handlers/crates/team-owners.js | 18 +++++++++ .../handlers/crates/team-owners.test.js | 40 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/crates/team-owners.js create mode 100644 packages/crates-io-msw/handlers/crates/team-owners.test.js diff --git a/packages/crates-io-msw/handlers/crates.js b/packages/crates-io-msw/handlers/crates.js index 264f65dd886..a0ab30b66a7 100644 --- a/packages/crates-io-msw/handlers/crates.js +++ b/packages/crates-io-msw/handlers/crates.js @@ -3,7 +3,8 @@ import followCrate from './crates/follow.js'; import following from './crates/following.js'; import getCrate from './crates/get.js'; import listCrates from './crates/list.js'; +import teamOwners from './crates/team-owners.js'; import unfollowCrate from './crates/unfollow.js'; import userOwners from './crates/user-owners.js'; -export default [listCrates, getCrate, deleteCrate, following, followCrate, unfollowCrate, userOwners]; +export default [listCrates, getCrate, deleteCrate, following, followCrate, unfollowCrate, userOwners, teamOwners]; diff --git a/packages/crates-io-msw/handlers/crates/team-owners.js b/packages/crates-io-msw/handlers/crates/team-owners.js new file mode 100644 index 00000000000..d5c30c76558 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/team-owners.js @@ -0,0 +1,18 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeTeam } from '../../serializers/team.js'; +import { notFound } from '../../utils/handlers.js'; + +export default http.get('/api/v1/crates/:name/owner_team', async ({ params }) => { + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) { + return notFound(); + } + + let ownerships = db.crateOwnership.findMany({ where: { crate: { id: { equals: crate.id } } } }); + + return HttpResponse.json({ + teams: ownerships.filter(o => o.team).map(o => ({ ...serializeTeam(o.team), kind: 'team' })), + }); +}); diff --git a/packages/crates-io-msw/handlers/crates/team-owners.test.js b/packages/crates-io-msw/handlers/crates/team-owners.test.js new file mode 100644 index 00000000000..25a9981aff5 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/team-owners.test.js @@ -0,0 +1,40 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 404 for unknown crates', async function () { + let response = await fetch('/api/v1/crates/foo/owner_team'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('empty case', async function () { + db.crate.create({ name: 'rand' }); + + let response = await fetch('/api/v1/crates/rand/owner_team'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + teams: [], + }); +}); + +test('returns the list of teams that own the specified crate', async function () { + let team = db.team.create({ name: 'maintainers' }); + let crate = db.crate.create({ name: 'rand' }); + db.crateOwnership.create({ crate, team }); + + let response = await fetch('/api/v1/crates/rand/owner_team'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + teams: [ + { + id: 1, + avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', + kind: 'team', + login: 'github:rust-lang:maintainers', + name: 'maintainers', + url: 'https://github.com/rust-lang', + }, + ], + }); +}); From 9bbc9b7fbc7cc5d879069af0172342b5c4a36ae7 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 17:27:41 +0100 Subject: [PATCH 067/189] msw: Implement `GET /api/v1/crates/:name/downloads` request handler --- packages/crates-io-msw/handlers/crates.js | 13 ++++- .../handlers/crates/downloads.js | 20 +++++++ .../handlers/crates/downloads.test.js | 55 +++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/crates/downloads.js create mode 100644 packages/crates-io-msw/handlers/crates/downloads.test.js diff --git a/packages/crates-io-msw/handlers/crates.js b/packages/crates-io-msw/handlers/crates.js index a0ab30b66a7..5994b515650 100644 --- a/packages/crates-io-msw/handlers/crates.js +++ b/packages/crates-io-msw/handlers/crates.js @@ -1,4 +1,5 @@ import deleteCrate from './crates/delete.js'; +import downloads from './crates/downloads.js'; import followCrate from './crates/follow.js'; import following from './crates/following.js'; import getCrate from './crates/get.js'; @@ -7,4 +8,14 @@ import teamOwners from './crates/team-owners.js'; import unfollowCrate from './crates/unfollow.js'; import userOwners from './crates/user-owners.js'; -export default [listCrates, getCrate, deleteCrate, following, followCrate, unfollowCrate, userOwners, teamOwners]; +export default [ + listCrates, + getCrate, + deleteCrate, + following, + followCrate, + unfollowCrate, + userOwners, + teamOwners, + downloads, +]; diff --git a/packages/crates-io-msw/handlers/crates/downloads.js b/packages/crates-io-msw/handlers/crates/downloads.js new file mode 100644 index 00000000000..21239edd403 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/downloads.js @@ -0,0 +1,20 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { notFound } from '../../utils/handlers.js'; + +export default http.get('/api/v1/crates/:name/downloads', async ({ params }) => { + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) return notFound(); + + let downloads = db.versionDownload.findMany({ where: { version: { crate: { id: { equals: crate.id } } } } }); + + return HttpResponse.json({ + version_downloads: downloads.map(download => ({ + date: download.date, + downloads: download.downloads, + version: download.version.id, + })), + meta: { extra_downloads: crate._extra_downloads }, + }); +}); diff --git a/packages/crates-io-msw/handlers/crates/downloads.test.js b/packages/crates-io-msw/handlers/crates/downloads.test.js new file mode 100644 index 00000000000..4ce9c8f52a2 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/downloads.test.js @@ -0,0 +1,55 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 404 for unknown crates', async function () { + let response = await fetch('/api/v1/crates/foo/downloads'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('empty case', async function () { + db.crate.create({ name: 'rand' }); + + let response = await fetch('/api/v1/crates/rand/downloads'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + version_downloads: [], + meta: { + extra_downloads: [], + }, + }); +}); + +test('returns a list of version downloads belonging to the specified crate version', async function () { + let crate = db.crate.create({ name: 'rand' }); + let versions = Array.from({ length: 2 }, () => db.version.create({ crate })); + db.versionDownload.create({ version: versions[0], date: '2020-01-13' }); + db.versionDownload.create({ version: versions[1], date: '2020-01-14' }); + db.versionDownload.create({ version: versions[1], date: '2020-01-15' }); + + let response = await fetch('/api/v1/crates/rand/downloads'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + version_downloads: [ + { + date: '2020-01-13', + downloads: 7035, + version: 1, + }, + { + date: '2020-01-14', + downloads: 14_070, + version: 2, + }, + { + date: '2020-01-15', + downloads: 21_105, + version: 2, + }, + ], + meta: { + extra_downloads: [], + }, + }); +}); From 29a5005185aef64d06f21d8a5de0f954fd65a130 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 17:50:32 +0100 Subject: [PATCH 068/189] msw: Implement `GET /api/v1/crates/:name/reverse_dependencies` request handler --- packages/crates-io-msw/handlers/crates.js | 2 + .../handlers/crates/reverse-dependencies.js | 29 ++++ .../crates/reverse-dependencies.test.js | 159 ++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 packages/crates-io-msw/handlers/crates/reverse-dependencies.js create mode 100644 packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js diff --git a/packages/crates-io-msw/handlers/crates.js b/packages/crates-io-msw/handlers/crates.js index 5994b515650..6f73ace1a73 100644 --- a/packages/crates-io-msw/handlers/crates.js +++ b/packages/crates-io-msw/handlers/crates.js @@ -4,6 +4,7 @@ import followCrate from './crates/follow.js'; import following from './crates/following.js'; import getCrate from './crates/get.js'; import listCrates from './crates/list.js'; +import reverseDependencies from './crates/reverse-dependencies.js'; import teamOwners from './crates/team-owners.js'; import unfollowCrate from './crates/unfollow.js'; import userOwners from './crates/user-owners.js'; @@ -17,5 +18,6 @@ export default [ unfollowCrate, userOwners, teamOwners, + reverseDependencies, downloads, ]; diff --git a/packages/crates-io-msw/handlers/crates/reverse-dependencies.js b/packages/crates-io-msw/handlers/crates/reverse-dependencies.js new file mode 100644 index 00000000000..4925fbbdb82 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/reverse-dependencies.js @@ -0,0 +1,29 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeDependency } from '../../serializers/dependency.js'; +import { serializeVersion } from '../../serializers/version.js'; +import { notFound, pageParams } from '../../utils/handlers.js'; + +export default http.get('/api/v1/crates/:name/reverse_dependencies', async ({ request, params }) => { + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) return notFound(); + + let { start, end } = pageParams(request); + + let allDependencies = db.dependency.findMany({ + where: { crate: { id: { equals: crate.id } } }, + orderBy: { version: { crate: { downloads: 'desc' } } }, + }); + + let dependencies = allDependencies.slice(start, end); + let total = allDependencies.length; + + let versions = dependencies.map(d => d.version); + + return HttpResponse.json({ + dependencies: dependencies.map(d => serializeDependency(d)), + versions: versions.map(v => serializeVersion(v)), + meta: { total }, + }); +}); diff --git a/packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js b/packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js new file mode 100644 index 00000000000..af19abf58aa --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js @@ -0,0 +1,159 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 404 for unknown crates', async function () { + let response = await fetch('/api/v1/crates/foo/reverse_dependencies'); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('empty case', async function () { + db.crate.create({ name: 'rand' }); + + let response = await fetch('/api/v1/crates/rand/reverse_dependencies'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + dependencies: [], + versions: [], + meta: { + total: 0, + }, + }); +}); + +test('returns a paginated list of crate versions depending to the specified crate', async function () { + let crate = db.crate.create({ name: 'foo' }); + + db.dependency.create({ + crate, + version: db.version.create({ + crate: db.crate.create({ name: 'bar' }), + }), + }); + + db.dependency.create({ + crate, + version: db.version.create({ + crate: db.crate.create({ name: 'baz' }), + }), + }); + + let response = await fetch('/api/v1/crates/foo/reverse_dependencies'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + dependencies: [ + { + id: 2, + crate_id: 'foo', + default_features: false, + features: [], + kind: 'normal', + optional: true, + req: '0.3.7', + target: null, + version_id: 2, + }, + { + id: 1, + crate_id: 'foo', + default_features: false, + features: [], + kind: 'normal', + optional: true, + req: '^2.1.3', + target: null, + version_id: 1, + }, + ], + versions: [ + { + id: 2, + crate: 'baz', + crate_size: 325_926, + created_at: '2010-06-16T21:30:45Z', + dl_path: '/api/v1/crates/baz/1.0.1/download', + downloads: 7404, + features: {}, + license: 'Apache-2.0', + links: { + dependencies: '/api/v1/crates/baz/1.0.1/dependencies', + version_downloads: '/api/v1/crates/baz/1.0.1/downloads', + }, + num: '1.0.1', + published_by: null, + readme_path: '/api/v1/crates/baz/1.0.1/readme', + rust_version: null, + updated_at: '2017-02-24T12:34:56Z', + yanked: false, + yank_message: null, + }, + { + id: 1, + crate: 'bar', + crate_size: 162_963, + created_at: '2010-06-16T21:30:45Z', + dl_path: '/api/v1/crates/bar/1.0.0/download', + downloads: 3702, + features: {}, + license: 'MIT', + links: { + dependencies: '/api/v1/crates/bar/1.0.0/dependencies', + version_downloads: '/api/v1/crates/bar/1.0.0/downloads', + }, + num: '1.0.0', + published_by: null, + readme_path: '/api/v1/crates/bar/1.0.0/readme', + rust_version: null, + updated_at: '2017-02-24T12:34:56Z', + yanked: false, + yank_message: null, + }, + ], + meta: { + total: 2, + }, + }); +}); + +test('never returns more than 10 results', async function () { + let crate = db.crate.create({ name: 'foo' }); + + Array.from({ length: 25 }, () => + db.dependency.create({ + crate, + version: db.version.create({ + crate: db.crate.create({ name: 'bar' }), + }), + }), + ); + + let response = await fetch('/api/v1/crates/foo/reverse_dependencies'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.strictEqual(responsePayload.dependencies.length, 10); + assert.strictEqual(responsePayload.versions.length, 10); + assert.strictEqual(responsePayload.meta.total, 25); +}); + +test('supports `page` and `per_page` parameters', async function () { + let crate = db.crate.create({ name: 'foo' }); + + let crates = Array.from({ length: 25 }, (_, i) => + db.crate.create({ name: `crate-${String(i + 1).padStart(2, '0')}` }), + ); + let versions = crates.map(crate => db.version.create({ crate })); + versions.forEach(version => db.dependency.create({ crate, version })); + + let response = await fetch('/api/v1/crates/foo/reverse_dependencies?page=2&per_page=5'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.strictEqual(responsePayload.dependencies.length, 5); + assert.deepEqual( + responsePayload.versions.map(it => it.crate), + ['crate-24', 'crate-02', 'crate-15', 'crate-06', 'crate-19'], + ); + assert.strictEqual(responsePayload.meta.total, 25); +}); From 69ca1aed4e761e142e346dc5df5915d04e662828 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 18:12:59 +0100 Subject: [PATCH 069/189] msw: Implement `PUT /api/v1/crates/:name/owners` request handler --- packages/crates-io-msw/handlers/crates.js | 2 + .../handlers/crates/add-owners.js | 54 +++++++++ .../handlers/crates/add-owners.test.js | 111 ++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 packages/crates-io-msw/handlers/crates/add-owners.js create mode 100644 packages/crates-io-msw/handlers/crates/add-owners.test.js diff --git a/packages/crates-io-msw/handlers/crates.js b/packages/crates-io-msw/handlers/crates.js index 6f73ace1a73..40dfe561d5b 100644 --- a/packages/crates-io-msw/handlers/crates.js +++ b/packages/crates-io-msw/handlers/crates.js @@ -1,3 +1,4 @@ +import addOwners from './crates/add-owners.js'; import deleteCrate from './crates/delete.js'; import downloads from './crates/downloads.js'; import followCrate from './crates/follow.js'; @@ -16,6 +17,7 @@ export default [ following, followCrate, unfollowCrate, + addOwners, userOwners, teamOwners, reverseDependencies, diff --git a/packages/crates-io-msw/handlers/crates/add-owners.js b/packages/crates-io-msw/handlers/crates/add-owners.js new file mode 100644 index 00000000000..227d27ef6a8 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/add-owners.js @@ -0,0 +1,54 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { notFound } from '../../utils/handlers.js'; +import { getSession } from '../../utils/session.js'; + +export default http.put('/api/v1/crates/:name/owners', async ({ request, params }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) { + return notFound(); + } + + let body = await request.json(); + + let users = []; + let teams = []; + let msgs = []; + for (let login of body.owners) { + if (login.includes(':')) { + let team = db.team.findFirst({ where: { login: { equals: login } } }); + if (!team) { + let errorMessage = `could not find team with login \`${login}\``; + return HttpResponse.json({ errors: [{ detail: errorMessage }] }, { status: 404 }); + } + + teams.push(team); + msgs.push(`team ${login} has been added as an owner of crate ${crate.name}`); + } else { + let user = db.user.findFirst({ where: { login: { equals: login } } }); + if (!user) { + let errorMessage = `could not find user with login \`${login}\``; + return HttpResponse.json({ errors: [{ detail: errorMessage }] }, { status: 404 }); + } + + users.push(user); + msgs.push(`user ${login} has been invited to be an owner of crate ${crate.name}`); + } + } + + for (let team of teams) { + db.crateOwnership.create({ crate, team }); + } + + for (let invitee of users) { + db.crateOwnerInvitation.create({ crate, inviter: user, invitee }); + } + + return HttpResponse.json({ ok: true, msg: msgs.join(',') }); +}); diff --git a/packages/crates-io-msw/handlers/crates/add-owners.test.js b/packages/crates-io-msw/handlers/crates/add-owners.test.js new file mode 100644 index 00000000000..cdaae3e7ae6 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/add-owners.test.js @@ -0,0 +1,111 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +const ADD_USER_BODY = JSON.stringify({ owners: ['john-doe'] }); + +test('returns 403 if unauthenticated', async function () { + let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body: ADD_USER_BODY }); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); + +test('returns 404 for unknown crates', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body: ADD_USER_BODY }); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('can add new owner', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let crate = db.crate.create({ name: 'foo' }); + db.crateOwnership.create({ crate, user }); + + let user2 = db.user.create(); + + let body = JSON.stringify({ owners: [user2.login] }); + let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + ok: true, + msg: 'user user-2 has been invited to be an owner of crate foo', + }); + + let owners = db.crateOwnership.findMany({ where: { crate: { id: { equals: crate.id } } } }); + assert.strictEqual(owners.length, 1); + assert.strictEqual(owners[0].user.id, user.id); + + let invites = db.crateOwnerInvitation.findMany({ where: { crate: { id: { equals: crate.id } } } }); + assert.strictEqual(invites.length, 1); + assert.strictEqual(invites[0].inviter.id, user.id); + assert.strictEqual(invites[0].invitee.id, user2.id); +}); + +test('can add team owner', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let crate = db.crate.create({ name: 'foo' }); + db.crateOwnership.create({ crate, user }); + + let team = db.team.create(); + + let body = JSON.stringify({ owners: [team.login] }); + let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + ok: true, + msg: 'team github:rust-lang:team-1 has been added as an owner of crate foo', + }); + + let owners = db.crateOwnership.findMany({ where: { crate: { id: { equals: crate.id } } } }); + assert.strictEqual(owners.length, 2); + assert.strictEqual(owners[0].user.id, user.id); + assert.strictEqual(owners[0].team, null); + assert.strictEqual(owners[1].user, null); + assert.strictEqual(owners[1].team.id, user.id); + + let invites = db.crateOwnerInvitation.findMany({ where: { crate: { id: { equals: crate.id } } } }); + assert.strictEqual(invites.length, 0); +}); + +test('can add multiple owners', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let crate = db.crate.create({ name: 'foo' }); + db.crateOwnership.create({ crate, user }); + + let team = db.team.create(); + let user2 = db.user.create(); + let user3 = db.user.create(); + + let body = JSON.stringify({ owners: [user2.login, team.login, user3.login] }); + let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + ok: true, + msg: 'user user-2 has been invited to be an owner of crate foo,team github:rust-lang:team-1 has been added as an owner of crate foo,user user-3 has been invited to be an owner of crate foo', + }); + + let owners = db.crateOwnership.findMany({ where: { crate: { id: { equals: crate.id } } } }); + assert.strictEqual(owners.length, 2); + assert.strictEqual(owners[0].user.id, user.id); + assert.strictEqual(owners[0].team, null); + assert.strictEqual(owners[1].user, null); + assert.strictEqual(owners[1].team.id, user.id); + + let invites = db.crateOwnerInvitation.findMany({ where: { crate: { id: { equals: crate.id } } } }); + assert.strictEqual(invites.length, 2); + assert.strictEqual(invites[0].inviter.id, user.id); + assert.strictEqual(invites[0].invitee.id, user2.id); + assert.strictEqual(invites[1].inviter.id, user.id); + assert.strictEqual(invites[1].invitee.id, user3.id); +}); From b2aab7f7526c6ee4a944f75267bbd6d7a2df45ff Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 18:31:43 +0100 Subject: [PATCH 070/189] msw: Implement `DELETE /api/v1/crates/:name/owners` request handler --- packages/crates-io-msw/handlers/crates.js | 2 + .../handlers/crates/remove-owners.js | 29 ++++++ .../handlers/crates/remove-owners.test.js | 96 +++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 packages/crates-io-msw/handlers/crates/remove-owners.js create mode 100644 packages/crates-io-msw/handlers/crates/remove-owners.test.js diff --git a/packages/crates-io-msw/handlers/crates.js b/packages/crates-io-msw/handlers/crates.js index 40dfe561d5b..2c6d0b4dfb5 100644 --- a/packages/crates-io-msw/handlers/crates.js +++ b/packages/crates-io-msw/handlers/crates.js @@ -5,6 +5,7 @@ import followCrate from './crates/follow.js'; import following from './crates/following.js'; import getCrate from './crates/get.js'; import listCrates from './crates/list.js'; +import removeOwners from './crates/remove-owners.js'; import reverseDependencies from './crates/reverse-dependencies.js'; import teamOwners from './crates/team-owners.js'; import unfollowCrate from './crates/unfollow.js'; @@ -18,6 +19,7 @@ export default [ followCrate, unfollowCrate, addOwners, + removeOwners, userOwners, teamOwners, reverseDependencies, diff --git a/packages/crates-io-msw/handlers/crates/remove-owners.js b/packages/crates-io-msw/handlers/crates/remove-owners.js new file mode 100644 index 00000000000..a221967d4fb --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/remove-owners.js @@ -0,0 +1,29 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { notFound } from '../../utils/handlers.js'; +import { getSession } from '../../utils/session.js'; + +export default http.delete('/api/v1/crates/:name/owners', async ({ request, params }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) { + return notFound(); + } + + let body = await request.json(); + + for (let owner of body.owners) { + let ownership = db.crateOwnership.findFirst({ + where: owner.includes(':') ? { team: { login: { equals: owner } } } : { user: { login: { equals: owner } } }, + }); + if (!ownership) return notFound(); + db.crateOwnership.delete({ where: { id: { equals: ownership.id } } }); + } + + return HttpResponse.json({ ok: true, msg: 'owners successfully removed' }); +}); diff --git a/packages/crates-io-msw/handlers/crates/remove-owners.test.js b/packages/crates-io-msw/handlers/crates/remove-owners.test.js new file mode 100644 index 00000000000..d1c84998227 --- /dev/null +++ b/packages/crates-io-msw/handlers/crates/remove-owners.test.js @@ -0,0 +1,96 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +const REMOVE_USER_BODY = JSON.stringify({ owners: ['john-doe'] }); + +test('returns 403 if unauthenticated', async function () { + let response = await fetch('/api/v1/crates/foo/owners', { method: 'DELETE', body: REMOVE_USER_BODY }); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); + +test('returns 404 for unknown crates', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/foo/owners', { method: 'DELETE', body: REMOVE_USER_BODY }); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('can remove a user owner', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let crate = db.crate.create({ name: 'foo' }); + db.crateOwnership.create({ crate, user }); + + let user2 = db.user.create(); + db.crateOwnership.create({ crate, user: user2 }); + + let body = JSON.stringify({ owners: [user2.login] }); + let response = await fetch('/api/v1/crates/foo/owners', { method: 'DELETE', body }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { ok: true, msg: 'owners successfully removed' }); + + let owners = db.crateOwnership.findMany({ where: { crate: { id: { equals: crate.id } } } }); + assert.strictEqual(owners.length, 1); + assert.strictEqual(owners[0].user.id, user.id); + + let invites = db.crateOwnerInvitation.findMany({ where: { crate: { id: { equals: crate.id } } } }); + assert.strictEqual(invites.length, 0); +}); + +test('can remove a team owner', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let crate = db.crate.create({ name: 'foo' }); + db.crateOwnership.create({ crate, user }); + + let team = db.team.create(); + db.crateOwnership.create({ crate, team }); + + let body = JSON.stringify({ owners: [team.login] }); + let response = await fetch('/api/v1/crates/foo/owners', { method: 'DELETE', body }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { ok: true, msg: 'owners successfully removed' }); + + let owners = db.crateOwnership.findMany({ where: { crate: { id: { equals: crate.id } } } }); + assert.strictEqual(owners.length, 1); + assert.strictEqual(owners[0].user.id, user.id); + assert.strictEqual(owners[0].team, null); + + let invites = db.crateOwnerInvitation.findMany({ where: { crate: { id: { equals: crate.id } } } }); + assert.strictEqual(invites.length, 0); +}); + +test('can remove multiple owners', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let crate = db.crate.create({ name: 'foo' }); + db.crateOwnership.create({ crate, user }); + + let team = db.team.create(); + db.crateOwnership.create({ crate, team }); + + let user2 = db.user.create(); + db.crateOwnership.create({ crate, user: user2 }); + + let body = JSON.stringify({ owners: [user2.login, team.login] }); + let response = await fetch('/api/v1/crates/foo/owners', { method: 'DELETE', body }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { ok: true, msg: 'owners successfully removed' }); + + let owners = db.crateOwnership.findMany({ where: { crate: { id: { equals: crate.id } } } }); + assert.strictEqual(owners.length, 1); + assert.strictEqual(owners[0].user.id, user.id); + assert.strictEqual(owners[0].team, null); + + let invites = db.crateOwnerInvitation.findMany({ where: { crate: { id: { equals: crate.id } } } }); + assert.strictEqual(invites.length, 0); +}); From 1b168a82efc38132add2aa92864c2201541dfbfd Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 18:39:28 +0100 Subject: [PATCH 071/189] msw: Implement `DELETE /api/v1/crates/:name/:version/yank` request handler --- packages/crates-io-msw/handlers/versions.js | 3 +- .../crates-io-msw/handlers/versions/yank.js | 27 +++++++++++ .../handlers/versions/yank.test.js | 47 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/versions/yank.js create mode 100644 packages/crates-io-msw/handlers/versions/yank.test.js diff --git a/packages/crates-io-msw/handlers/versions.js b/packages/crates-io-msw/handlers/versions.js index 1306e9c4435..e620628b337 100644 --- a/packages/crates-io-msw/handlers/versions.js +++ b/packages/crates-io-msw/handlers/versions.js @@ -2,5 +2,6 @@ import dependencies from './versions/dependencies.js'; import downloads from './versions/downloads.js'; import getVersion from './versions/get.js'; import listVersions from './versions/list.js'; +import yankVersion from './versions/yank.js'; -export default [listVersions, getVersion, dependencies, downloads]; +export default [listVersions, getVersion, yankVersion, dependencies, downloads]; diff --git a/packages/crates-io-msw/handlers/versions/yank.js b/packages/crates-io-msw/handlers/versions/yank.js new file mode 100644 index 00000000000..e29092ebe87 --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/yank.js @@ -0,0 +1,27 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { notFound } from '../../utils/handlers.js'; +import { getSession } from '../../utils/session.js'; + +export default http.delete('/api/v1/crates/:name/:version/yank', async ({ params }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) return notFound(); + + let version = db.version.findFirst({ + where: { + crate: { id: { equals: crate.id } }, + num: { equals: params.version }, + }, + }); + if (!version) return notFound(); + + db.version.update({ where: { id: { equals: version.id } }, data: { yanked: true } }); + + return HttpResponse.json({ ok: true }); +}); diff --git a/packages/crates-io-msw/handlers/versions/yank.test.js b/packages/crates-io-msw/handlers/versions/yank.test.js new file mode 100644 index 00000000000..49d8caa00ae --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/yank.test.js @@ -0,0 +1,47 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 403 if unauthenticated', async function () { + let response = await fetch('/api/v1/crates/foo/1.0.0/yank', { method: 'DELETE' }); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); + +test('returns 404 for unknown crates', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/foo/1.0.0/yank', { method: 'DELETE' }); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('returns 404 for unknown versions', async function () { + db.crate.create({ name: 'foo' }); + + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/foo/1.0.0/yank', { method: 'DELETE' }); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('yanks the version', async function () { + let crate = db.crate.create({ name: 'foo' }); + let version = db.version.create({ crate, num: '1.0.0', yanked: false }); + assert.strictEqual(version.yanked, false); + + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/foo/1.0.0/yank', { method: 'DELETE' }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { ok: true }); + + version = db.version.findFirst({ where: { id: version.id } }); + assert.strictEqual(version.yanked, true); +}); From b15d71c85fb305af8b31ba028f06261fd627a30c Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 18:42:25 +0100 Subject: [PATCH 072/189] msw: Implement `PUT /api/v1/crates/:name/:version/unyank` request handler --- packages/crates-io-msw/handlers/versions.js | 3 +- .../crates-io-msw/handlers/versions/unyank.js | 27 ++++++++++ .../handlers/versions/unyank.test.js | 49 +++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/versions/unyank.js create mode 100644 packages/crates-io-msw/handlers/versions/unyank.test.js diff --git a/packages/crates-io-msw/handlers/versions.js b/packages/crates-io-msw/handlers/versions.js index e620628b337..ef5b0d7c53d 100644 --- a/packages/crates-io-msw/handlers/versions.js +++ b/packages/crates-io-msw/handlers/versions.js @@ -2,6 +2,7 @@ import dependencies from './versions/dependencies.js'; import downloads from './versions/downloads.js'; import getVersion from './versions/get.js'; import listVersions from './versions/list.js'; +import unyankVersion from './versions/unyank.js'; import yankVersion from './versions/yank.js'; -export default [listVersions, getVersion, yankVersion, dependencies, downloads]; +export default [listVersions, getVersion, yankVersion, unyankVersion, dependencies, downloads]; diff --git a/packages/crates-io-msw/handlers/versions/unyank.js b/packages/crates-io-msw/handlers/versions/unyank.js new file mode 100644 index 00000000000..4947744c7a8 --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/unyank.js @@ -0,0 +1,27 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { notFound } from '../../utils/handlers.js'; +import { getSession } from '../../utils/session.js'; + +export default http.put('/api/v1/crates/:name/:version/unyank', async ({ params }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) return notFound(); + + let version = db.version.findFirst({ + where: { + crate: { id: { equals: crate.id } }, + num: { equals: params.version }, + }, + }); + if (!version) return notFound(); + + db.version.update({ where: { id: { equals: version.id } }, data: { yanked: false, yank_message: null } }); + + return HttpResponse.json({ ok: true }); +}); diff --git a/packages/crates-io-msw/handlers/versions/unyank.test.js b/packages/crates-io-msw/handlers/versions/unyank.test.js new file mode 100644 index 00000000000..0e461269a8e --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/unyank.test.js @@ -0,0 +1,49 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 403 if unauthenticated', async function () { + let response = await fetch('/api/v1/crates/foo/1.0.0/unyank', { method: 'PUT' }); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); + +test('returns 404 for unknown crates', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/foo/1.0.0/unyank', { method: 'PUT' }); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('returns 404 for unknown versions', async function () { + db.crate.create({ name: 'foo' }); + + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/foo/1.0.0/unyank', { method: 'PUT' }); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('unyanks the version', async function () { + let crate = db.crate.create({ name: 'foo' }); + let version = db.version.create({ crate, num: '1.0.0', yanked: true, yank_message: 'some reason' }); + assert.strictEqual(version.yanked, true); + assert.strictEqual(version.yank_message, 'some reason'); + + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/foo/1.0.0/unyank', { method: 'PUT' }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { ok: true }); + + version = db.version.findFirst({ where: { id: version.id } }); + assert.strictEqual(version.yanked, false); + assert.strictEqual(version.yank_message, null); +}); From 4ff2177939c0b6e463dd56b2f99fa85256f3b02c Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 18:46:12 +0100 Subject: [PATCH 073/189] msw: Implement `GET /api/v1/crates/:name/:version/readme` request handler --- packages/crates-io-msw/handlers/versions.js | 3 +- .../crates-io-msw/handlers/versions/readme.js | 18 +++++++++ .../handlers/versions/readme.test.js | 37 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/versions/readme.js create mode 100644 packages/crates-io-msw/handlers/versions/readme.test.js diff --git a/packages/crates-io-msw/handlers/versions.js b/packages/crates-io-msw/handlers/versions.js index ef5b0d7c53d..1a8bc4feb1e 100644 --- a/packages/crates-io-msw/handlers/versions.js +++ b/packages/crates-io-msw/handlers/versions.js @@ -2,7 +2,8 @@ import dependencies from './versions/dependencies.js'; import downloads from './versions/downloads.js'; import getVersion from './versions/get.js'; import listVersions from './versions/list.js'; +import readme from './versions/readme.js'; import unyankVersion from './versions/unyank.js'; import yankVersion from './versions/yank.js'; -export default [listVersions, getVersion, yankVersion, unyankVersion, dependencies, downloads]; +export default [listVersions, getVersion, yankVersion, unyankVersion, dependencies, downloads, readme]; diff --git a/packages/crates-io-msw/handlers/versions/readme.js b/packages/crates-io-msw/handlers/versions/readme.js new file mode 100644 index 00000000000..925f486db75 --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/readme.js @@ -0,0 +1,18 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; + +export default http.get('/api/v1/crates/:name/:version/readme', async ({ params }) => { + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) return HttpResponse.html('', { status: 403 }); + + let version = db.version.findFirst({ + where: { + crate: { id: { equals: crate.id } }, + num: { equals: params.version }, + }, + }); + if (!version || !version.readme) return HttpResponse.html('', { status: 403 }); + + return HttpResponse.html(version.readme); +}); diff --git a/packages/crates-io-msw/handlers/versions/readme.test.js b/packages/crates-io-msw/handlers/versions/readme.test.js new file mode 100644 index 00000000000..3bedd28af9c --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/readme.test.js @@ -0,0 +1,37 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 404 for unknown crates', async function () { + let response = await fetch('/api/v1/crates/foo/1.0.0/readme'); + assert.strictEqual(response.status, 403); + assert.strictEqual(await response.text(), ''); +}); + +test('returns 404 for unknown versions', async function () { + db.crate.create({ name: 'rand' }); + + let response = await fetch('/api/v1/crates/rand/1.0.0/readme'); + assert.strictEqual(response.status, 403); + assert.strictEqual(await response.text(), ''); +}); + +test('returns 404 for versions without README', async function () { + let crate = db.crate.create({ name: 'rand' }); + db.version.create({ crate, num: '1.0.0' }); + + let response = await fetch('/api/v1/crates/rand/1.0.0/readme'); + assert.strictEqual(response.status, 403); + assert.strictEqual(await response.text(), ''); +}); + +test('returns the README as raw HTML', async function () { + let readme = 'lorem ipsum est dolor!'; + + let crate = db.crate.create({ name: 'rand' }); + db.version.create({ crate, num: '1.0.0', readme: readme }); + + let response = await fetch('/api/v1/crates/rand/1.0.0/readme'); + assert.strictEqual(response.status, 200); + assert.strictEqual(await response.text(), readme); +}); From 81573a1c9dd0be5e2eb0947003a329bb25744d64 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 18:54:05 +0100 Subject: [PATCH 074/189] msw: Implement `PATCH /api/v1/crates/:name/:version` request handler --- packages/crates-io-msw/handlers/versions.js | 3 +- .../crates-io-msw/handlers/versions/patch.js | 39 ++++++ .../handlers/versions/patch.test.js | 114 ++++++++++++++++++ 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/versions/patch.js create mode 100644 packages/crates-io-msw/handlers/versions/patch.test.js diff --git a/packages/crates-io-msw/handlers/versions.js b/packages/crates-io-msw/handlers/versions.js index 1a8bc4feb1e..5d18a25f9de 100644 --- a/packages/crates-io-msw/handlers/versions.js +++ b/packages/crates-io-msw/handlers/versions.js @@ -2,8 +2,9 @@ import dependencies from './versions/dependencies.js'; import downloads from './versions/downloads.js'; import getVersion from './versions/get.js'; import listVersions from './versions/list.js'; +import patchVersion from './versions/patch.js'; import readme from './versions/readme.js'; import unyankVersion from './versions/unyank.js'; import yankVersion from './versions/yank.js'; -export default [listVersions, getVersion, yankVersion, unyankVersion, dependencies, downloads, readme]; +export default [listVersions, getVersion, patchVersion, yankVersion, unyankVersion, dependencies, downloads, readme]; diff --git a/packages/crates-io-msw/handlers/versions/patch.js b/packages/crates-io-msw/handlers/versions/patch.js new file mode 100644 index 00000000000..efdbeb84f57 --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/patch.js @@ -0,0 +1,39 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeVersion } from '../../serializers/version.js'; +import { notFound } from '../../utils/handlers.js'; +import { getSession } from '../../utils/session.js'; + +export default http.patch('/api/v1/crates/:name/:version', async ({ request, params }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let crate = db.crate.findFirst({ where: { name: { equals: params.name } } }); + if (!crate) return notFound(); + + let version = db.version.findFirst({ + where: { + crate: { id: { equals: crate.id } }, + num: { equals: params.version }, + }, + }); + if (!version) return notFound(); + + let body = await request.json(); + + let yanked = body.version.yanked; + let yankMessage = body.version.yank_message; + + version = db.version.update({ + where: { id: { equals: version.id } }, + data: { + yanked: yanked, + yank_message: yanked ? yankMessage || null : null, + }, + }); + + return HttpResponse.json({ version: serializeVersion(version) }); +}); diff --git a/packages/crates-io-msw/handlers/versions/patch.test.js b/packages/crates-io-msw/handlers/versions/patch.test.js new file mode 100644 index 00000000000..a3ee8a368af --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/patch.test.js @@ -0,0 +1,114 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +const YANK_BODY = JSON.stringify({ + version: { + yanked: true, + yank_message: 'some reason', + }, +}); + +const UNYANK_BODY = JSON.stringify({ + version: { + yanked: false, + }, +}); + +test('returns 403 if unauthenticated', async function () { + let response = await fetch('/api/v1/crates/foo/1.0.0', { method: 'PATCH', body: YANK_BODY }); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); + +test('returns 404 for unknown crates', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/foo/1.0.0', { method: 'PATCH', body: YANK_BODY }); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('returns 404 for unknown versions', async function () { + db.crate.create({ name: 'foo' }); + + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/foo/1.0.0', { method: 'PATCH', body: YANK_BODY }); + assert.strictEqual(response.status, 404); + assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); +}); + +test('yanks the version', async function () { + let crate = db.crate.create({ name: 'foo' }); + let version = db.version.create({ crate, num: '1.0.0', yanked: false }); + assert.strictEqual(version.yanked, false); + assert.strictEqual(version.yank_message, null); + + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/crates/foo/1.0.0', { method: 'PATCH', body: YANK_BODY }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + version: { + crate: 'foo', + crate_size: 162_963, + created_at: '2010-06-16T21:30:45Z', + dl_path: '/api/v1/crates/foo/1.0.0/download', + downloads: 3702, + features: {}, + id: 1, + license: 'MIT', + links: { + dependencies: '/api/v1/crates/foo/1.0.0/dependencies', + version_downloads: '/api/v1/crates/foo/1.0.0/downloads', + }, + num: '1.0.0', + published_by: null, + readme_path: '/api/v1/crates/foo/1.0.0/readme', + rust_version: null, + updated_at: '2017-02-24T12:34:56Z', + yank_message: 'some reason', + yanked: true, + }, + }); + + version = db.version.findFirst({ where: { id: { equals: version.id } } }); + assert.strictEqual(version.yanked, true); + assert.strictEqual(version.yank_message, 'some reason'); + + response = await fetch('/api/v1/crates/foo/1.0.0', { method: 'PATCH', body: UNYANK_BODY }); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + version: { + crate: 'foo', + crate_size: 162_963, + created_at: '2010-06-16T21:30:45Z', + dl_path: '/api/v1/crates/foo/1.0.0/download', + downloads: 3702, + features: {}, + id: 1, + license: 'MIT', + links: { + dependencies: '/api/v1/crates/foo/1.0.0/dependencies', + version_downloads: '/api/v1/crates/foo/1.0.0/downloads', + }, + num: '1.0.0', + published_by: null, + readme_path: '/api/v1/crates/foo/1.0.0/readme', + rust_version: null, + updated_at: '2017-02-24T12:34:56Z', + yank_message: null, + yanked: false, + }, + }); + + version = db.version.findFirst({ where: { id: { equals: version.id } } }); + assert.strictEqual(version.yanked, false); + assert.strictEqual(version.yank_message, null); +}); From 26434dd7f0fd997f333911e37e1e42e42b232580 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 19:07:03 +0100 Subject: [PATCH 075/189] msw: Implement `GET /api/v1/me/updates` request handler --- packages/crates-io-msw/handlers/versions.js | 13 ++- .../handlers/versions/follow-updates.js | 28 +++++++ .../handlers/versions/follow-updates.test.js | 84 +++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 packages/crates-io-msw/handlers/versions/follow-updates.js create mode 100644 packages/crates-io-msw/handlers/versions/follow-updates.test.js diff --git a/packages/crates-io-msw/handlers/versions.js b/packages/crates-io-msw/handlers/versions.js index 5d18a25f9de..1eb9c05751f 100644 --- a/packages/crates-io-msw/handlers/versions.js +++ b/packages/crates-io-msw/handlers/versions.js @@ -1,5 +1,6 @@ import dependencies from './versions/dependencies.js'; import downloads from './versions/downloads.js'; +import followUpdates from './versions/follow-updates.js'; import getVersion from './versions/get.js'; import listVersions from './versions/list.js'; import patchVersion from './versions/patch.js'; @@ -7,4 +8,14 @@ import readme from './versions/readme.js'; import unyankVersion from './versions/unyank.js'; import yankVersion from './versions/yank.js'; -export default [listVersions, getVersion, patchVersion, yankVersion, unyankVersion, dependencies, downloads, readme]; +export default [ + listVersions, + getVersion, + patchVersion, + yankVersion, + unyankVersion, + dependencies, + downloads, + readme, + followUpdates, +]; diff --git a/packages/crates-io-msw/handlers/versions/follow-updates.js b/packages/crates-io-msw/handlers/versions/follow-updates.js new file mode 100644 index 00000000000..2007da5875c --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/follow-updates.js @@ -0,0 +1,28 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../../index.js'; +import { serializeVersion } from '../../serializers/version.js'; +import { pageParams } from '../../utils/handlers.js'; +import { getSession } from '../../utils/session.js'; + +export default http.get('/api/v1/me/updates', ({ request }) => { + let { user } = getSession(); + if (!user) { + return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 }); + } + + let allVersions = user.followedCrates + .flatMap(crate => db.version.findMany({ where: { crate: { id: { equals: crate.id } } } })) + .sort((a, b) => b.id - a.id); + + let { start, end, page, perPage } = pageParams(request); + + let versions = allVersions.slice(start, end); + let totalCount = allVersions.length; + let totalPages = Math.ceil(totalCount / perPage); + + return HttpResponse.json({ + versions: versions.map(v => serializeVersion(v)), + meta: { more: page < totalPages }, + }); +}); diff --git a/packages/crates-io-msw/handlers/versions/follow-updates.test.js b/packages/crates-io-msw/handlers/versions/follow-updates.test.js new file mode 100644 index 00000000000..26029165ee3 --- /dev/null +++ b/packages/crates-io-msw/handlers/versions/follow-updates.test.js @@ -0,0 +1,84 @@ +import { assert, test } from 'vitest'; + +import { db } from '../../index.js'; + +test('returns 403 for unauthenticated user', async function () { + let response = await fetch('/api/v1/me/updates'); + assert.strictEqual(response.status, 403); + assert.deepEqual(await response.json(), { + errors: [{ detail: 'must be logged in to perform that action' }], + }); +}); + +test('returns latest versions of followed crates', async function () { + let foo = db.crate.create({ name: 'foo' }); + db.version.create({ crate: foo, num: '1.2.3' }); + + let bar = db.crate.create({ name: 'bar' }); + db.version.create({ crate: bar, num: '0.8.6' }); + + let user = db.user.create({ followedCrates: [foo] }); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/me/updates'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + versions: [ + { + id: 1, + crate: 'foo', + crate_size: 162_963, + created_at: '2010-06-16T21:30:45Z', + dl_path: '/api/v1/crates/foo/1.2.3/download', + downloads: 3702, + features: {}, + license: 'MIT', + links: { + dependencies: '/api/v1/crates/foo/1.2.3/dependencies', + version_downloads: '/api/v1/crates/foo/1.2.3/downloads', + }, + num: '1.2.3', + published_by: null, + readme_path: '/api/v1/crates/foo/1.2.3/readme', + rust_version: null, + updated_at: '2017-02-24T12:34:56Z', + yanked: false, + yank_message: null, + }, + ], + meta: { + more: false, + }, + }); +}); + +test('empty case', async function () { + let user = db.user.create(); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/me/updates'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + versions: [], + meta: { more: false }, + }); +}); + +test('supports pagination', async function () { + let crate = db.crate.create({ name: 'foo' }); + Array.from({ length: 25 }, () => db.version.create({ crate })); + + let user = db.user.create({ followedCrates: [crate] }); + db.mswSession.create({ user }); + + let response = await fetch('/api/v1/me/updates?page=2'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.strictEqual(responsePayload.versions.length, 10); + assert.deepEqual( + responsePayload.versions.map(it => it.id), + [15, 14, 13, 12, 11, 10, 9, 8, 7, 6], + ); + assert.deepEqual(responsePayload.meta, { more: true }); +}); From 14d7481fb266ad4a3b89b71934c6257f3183ae83 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 23 Jan 2025 19:33:02 +0100 Subject: [PATCH 076/189] msw: Implement `GET /api/v1/summary` request handler --- packages/crates-io-msw/handlers/summary.js | 36 ++++ .../crates-io-msw/handlers/summary.test.js | 170 ++++++++++++++++++ packages/crates-io-msw/index.js | 2 + 3 files changed, 208 insertions(+) create mode 100644 packages/crates-io-msw/handlers/summary.js create mode 100644 packages/crates-io-msw/handlers/summary.test.js diff --git a/packages/crates-io-msw/handlers/summary.js b/packages/crates-io-msw/handlers/summary.js new file mode 100644 index 00000000000..3d1306eebdc --- /dev/null +++ b/packages/crates-io-msw/handlers/summary.js @@ -0,0 +1,36 @@ +import { http, HttpResponse } from 'msw'; + +import { db } from '../index.js'; +import { serializeCategory } from '../serializers/category.js'; +import { serializeCrate } from '../serializers/crate.js'; +import { serializeKeyword } from '../serializers/keyword.js'; +import { compareDates } from '../utils/dates.js'; + +export default [ + http.get('/api/v1/summary', () => { + let crates = db.crate.findMany({}); + + let just_updated = crates.sort((a, b) => compareDates(b.updated_at, a.updated_at)).slice(0, 10); + let most_downloaded = crates.sort((a, b) => b.downloads - a.downloads).slice(0, 10); + let new_crates = crates.sort((a, b) => b.id - a.id).slice(0, 10); + let most_recently_downloaded = crates.sort((a, b) => b.recent_downloads - a.recent_downloads).slice(0, 10); + + let num_crates = crates.length; + // eslint-disable-next-line unicorn/no-array-reduce + let num_downloads = crates.reduce((sum, crate) => sum + crate.downloads, 0); + + let popularCategories = db.category.findMany({ take: 10 }); + let popularKeywords = db.keyword.findMany({ take: 10 }); + + return HttpResponse.json({ + just_updated: just_updated.map(c => serializeCrate(c)), + most_downloaded: most_downloaded.map(c => serializeCrate(c)), + new_crates: new_crates.map(c => serializeCrate(c)), + most_recently_downloaded: most_recently_downloaded.map(c => serializeCrate(c)), + num_crates, + num_downloads, + popular_categories: popularCategories.map(it => serializeCategory(it)), + popular_keywords: popularKeywords.map(it => serializeKeyword(it)), + }); + }), +]; diff --git a/packages/crates-io-msw/handlers/summary.test.js b/packages/crates-io-msw/handlers/summary.test.js new file mode 100644 index 00000000000..806a74e6994 --- /dev/null +++ b/packages/crates-io-msw/handlers/summary.test.js @@ -0,0 +1,170 @@ +import { assert, test } from 'vitest'; + +import { db } from '../index.js'; + +test('empty case', async function () { + let response = await fetch('/api/v1/summary'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + just_updated: [], + most_downloaded: [], + most_recently_downloaded: [], + new_crates: [], + num_crates: 0, + num_downloads: 0, + popular_categories: [], + popular_keywords: [], + }); +}); + +test('returns the data for the front page', async function () { + Array.from({ length: 15 }, () => db.category.create()); + Array.from({ length: 25 }, () => db.keyword.create()); + let crates = Array.from({ length: 20 }, () => db.crate.create()); + crates.forEach(crate => db.version.create({ crate })); + + let response = await fetch('/api/v1/summary'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + + assert.strictEqual(responsePayload.just_updated.length, 10); + assert.deepEqual(responsePayload.just_updated[0], { + id: 'crate-1', + badges: [], + categories: null, + created_at: '2010-06-16T21:30:45Z', + default_version: '1.0.0', + description: 'This is the description for the crate called "crate-1"', + documentation: null, + downloads: 37_035, + homepage: null, + keywords: null, + links: { + owner_team: '/api/v1/crates/crate-1/owner_team', + owner_user: '/api/v1/crates/crate-1/owner_user', + reverse_dependencies: '/api/v1/crates/crate-1/reverse_dependencies', + version_downloads: '/api/v1/crates/crate-1/downloads', + versions: '/api/v1/crates/crate-1/versions', + }, + max_version: '1.0.0', + max_stable_version: '1.0.0', + name: 'crate-1', + newest_version: '1.0.0', + recent_downloads: 321, + repository: null, + updated_at: '2017-02-24T12:34:56Z', + versions: null, + yanked: false, + }); + + assert.strictEqual(responsePayload.most_downloaded.length, 10); + assert.deepEqual(responsePayload.most_downloaded[0], { + id: 'crate-4', + badges: [], + categories: null, + created_at: '2010-06-16T21:30:45Z', + default_version: '1.0.3', + description: 'This is the description for the crate called "crate-4"', + documentation: null, + downloads: 148_140, + homepage: null, + keywords: null, + links: { + owner_team: '/api/v1/crates/crate-4/owner_team', + owner_user: '/api/v1/crates/crate-4/owner_user', + reverse_dependencies: '/api/v1/crates/crate-4/reverse_dependencies', + version_downloads: '/api/v1/crates/crate-4/downloads', + versions: '/api/v1/crates/crate-4/versions', + }, + max_version: '1.0.3', + max_stable_version: '1.0.3', + name: 'crate-4', + newest_version: '1.0.3', + repository: null, + recent_downloads: 963, + updated_at: '2017-02-24T12:34:56Z', + versions: null, + yanked: false, + }); + + assert.strictEqual(responsePayload.most_recently_downloaded.length, 10); + assert.deepEqual(responsePayload.most_recently_downloaded[0], { + id: 'crate-11', + badges: [], + categories: null, + created_at: '2010-06-16T21:30:45Z', + default_version: '1.0.10', + description: 'This is the description for the crate called "crate-11"', + documentation: null, + downloads: 86_415, + homepage: null, + keywords: null, + links: { + owner_team: '/api/v1/crates/crate-11/owner_team', + owner_user: '/api/v1/crates/crate-11/owner_user', + reverse_dependencies: '/api/v1/crates/crate-11/reverse_dependencies', + version_downloads: '/api/v1/crates/crate-11/downloads', + versions: '/api/v1/crates/crate-11/versions', + }, + max_version: '1.0.10', + max_stable_version: '1.0.10', + name: 'crate-11', + newest_version: '1.0.10', + repository: null, + recent_downloads: 3852, + updated_at: '2017-02-24T12:34:56Z', + versions: null, + yanked: false, + }); + + assert.strictEqual(responsePayload.new_crates.length, 10); + assert.deepEqual(responsePayload.new_crates[0], { + id: 'crate-20', + badges: [], + categories: null, + created_at: '2010-06-16T21:30:45Z', + default_version: '1.0.19', + description: 'This is the description for the crate called "crate-20"', + documentation: null, + downloads: 98_760, + homepage: null, + keywords: null, + links: { + owner_team: '/api/v1/crates/crate-20/owner_team', + owner_user: '/api/v1/crates/crate-20/owner_user', + reverse_dependencies: '/api/v1/crates/crate-20/reverse_dependencies', + version_downloads: '/api/v1/crates/crate-20/downloads', + versions: '/api/v1/crates/crate-20/versions', + }, + max_version: '1.0.19', + max_stable_version: '1.0.19', + name: 'crate-20', + newest_version: '1.0.19', + repository: null, + recent_downloads: 1605, + updated_at: '2017-02-24T12:34:56Z', + versions: null, + yanked: false, + }); + + assert.strictEqual(responsePayload.num_crates, 20); + assert.strictEqual(responsePayload.num_downloads, 1_518_435); + + assert.strictEqual(responsePayload.popular_categories.length, 10); + assert.deepEqual(responsePayload.popular_categories[0], { + id: 'category-1', + category: 'Category 1', + crates_cnt: 0, + created_at: '2010-06-16T21:30:45Z', + description: 'This is the description for the category called "Category 1"', + slug: 'category-1', + }); + + assert.strictEqual(responsePayload.popular_keywords.length, 10); + assert.deepEqual(responsePayload.popular_keywords[0], { + id: 'keyword-1', + crates_cnt: 0, + keyword: 'keyword-1', + }); +}); diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index 1ecbb424b6c..fd2cd67806d 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -6,6 +6,7 @@ import inviteHandlers from './handlers/invites.js'; import keywordHandlers from './handlers/keywords.js'; import metadataHandlers from './handlers/metadata.js'; import sessionHandlers from './handlers/sessions.js'; +import summaryHandlers from './handlers/summary.js'; import teamHandlers from './handlers/teams.js'; import userHandlers from './handlers/users.js'; import versionHandlers from './handlers/versions.js'; @@ -32,6 +33,7 @@ export const handlers = [ ...keywordHandlers, ...metadataHandlers, ...sessionHandlers, + ...summaryHandlers, ...teamHandlers, ...userHandlers, ...versionHandlers, From f7ea6726f617bcc403052fa046361f4857c9329b Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 14:48:05 +0100 Subject: [PATCH 077/189] msw: Implement play.rust-lang.org request handler --- packages/crates-io-msw/handlers/playground.js | 3 +++ packages/crates-io-msw/handlers/playground.test.js | 7 +++++++ packages/crates-io-msw/index.js | 2 ++ 3 files changed, 12 insertions(+) create mode 100644 packages/crates-io-msw/handlers/playground.js create mode 100644 packages/crates-io-msw/handlers/playground.test.js diff --git a/packages/crates-io-msw/handlers/playground.js b/packages/crates-io-msw/handlers/playground.js new file mode 100644 index 00000000000..673c9eeac72 --- /dev/null +++ b/packages/crates-io-msw/handlers/playground.js @@ -0,0 +1,3 @@ +import { http, HttpResponse } from 'msw'; + +export default [http.get('https://play.rust-lang.org/meta/crates', () => HttpResponse.json([]))]; diff --git a/packages/crates-io-msw/handlers/playground.test.js b/packages/crates-io-msw/handlers/playground.test.js new file mode 100644 index 00000000000..f5c40b3aa76 --- /dev/null +++ b/packages/crates-io-msw/handlers/playground.test.js @@ -0,0 +1,7 @@ +import { assert, test } from 'vitest'; + +test('returns 200 OK and an empty array', async function () { + let response = await fetch('https://play.rust-lang.org/meta/crates'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), []); +}); diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js index fd2cd67806d..8f320f61f54 100644 --- a/packages/crates-io-msw/index.js +++ b/packages/crates-io-msw/index.js @@ -5,6 +5,7 @@ import docsRsHandlers from './handlers/docs-rs.js'; import inviteHandlers from './handlers/invites.js'; import keywordHandlers from './handlers/keywords.js'; import metadataHandlers from './handlers/metadata.js'; +import playgroundHandlers from './handlers/playground.js'; import sessionHandlers from './handlers/sessions.js'; import summaryHandlers from './handlers/summary.js'; import teamHandlers from './handlers/teams.js'; @@ -32,6 +33,7 @@ export const handlers = [ ...inviteHandlers, ...keywordHandlers, ...metadataHandlers, + ...playgroundHandlers, ...sessionHandlers, ...summaryHandlers, ...teamHandlers, From 583fe8e05a96cc91635fcac362fbf3cd9317fcc3 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 13:08:22 +0100 Subject: [PATCH 078/189] msw: Import existing fixtures --- packages/crates-io-msw/fixtures.js | 74 ++++ packages/crates-io-msw/fixtures.test.js | 8 + packages/crates-io-msw/fixtures/categories.js | 25 ++ .../fixtures/crate-ownerships.js | 18 + packages/crates-io-msw/fixtures/crates.js | 297 +++++++++++++ .../crates-io-msw/fixtures/dependencies.js | 57 +++ packages/crates-io-msw/fixtures/keywords.js | 72 +++ packages/crates-io-msw/fixtures/teams.js | 16 + packages/crates-io-msw/fixtures/users.js | 26 ++ .../fixtures/version-downloads.js | 17 + packages/crates-io-msw/fixtures/versions.js | 412 ++++++++++++++++++ 11 files changed, 1022 insertions(+) create mode 100644 packages/crates-io-msw/fixtures.js create mode 100644 packages/crates-io-msw/fixtures.test.js create mode 100644 packages/crates-io-msw/fixtures/categories.js create mode 100644 packages/crates-io-msw/fixtures/crate-ownerships.js create mode 100644 packages/crates-io-msw/fixtures/crates.js create mode 100644 packages/crates-io-msw/fixtures/dependencies.js create mode 100644 packages/crates-io-msw/fixtures/keywords.js create mode 100644 packages/crates-io-msw/fixtures/teams.js create mode 100644 packages/crates-io-msw/fixtures/users.js create mode 100644 packages/crates-io-msw/fixtures/version-downloads.js create mode 100644 packages/crates-io-msw/fixtures/versions.js diff --git a/packages/crates-io-msw/fixtures.js b/packages/crates-io-msw/fixtures.js new file mode 100644 index 00000000000..596b96d1f63 --- /dev/null +++ b/packages/crates-io-msw/fixtures.js @@ -0,0 +1,74 @@ +import CATEGORIES from './fixtures/categories.js'; +import CRATE_OWNERSHIPS from './fixtures/crate-ownerships.js'; +import CRATES from './fixtures/crates.js'; +import DEPENDENCIES from './fixtures/dependencies.js'; +import KEYWORDS from './fixtures/keywords.js'; +import TEAMS from './fixtures/teams.js'; +import USERS from './fixtures/users.js'; +import VERSION_DOWNLOADS from './fixtures/version-downloads.js'; +import VERSIONS from './fixtures/versions.js'; + +export function loadFixtures(db) { + CATEGORIES.forEach(it => db.category.create(it)); + let keywords = KEYWORDS.map(it => db.keyword.create(it)); + + let users = USERS.map(it => db.user.create(it)); + let teams = TEAMS.map(it => db.team.create(it)); + + let crates = CRATES.map(it => { + if (it.keywordIds) { + it.keywords = it.keywordIds.map(id => keywords.find(k => k.id === id)).filter(Boolean); + delete it.keywordIds; + } + + return db.crate.create(it); + }); + + CRATE_OWNERSHIPS.forEach(it => { + if (it.crateId) { + it.crate = crates.find(c => c.name === it.crateId); + delete it.crateId; + } + if (it.teamId) { + it.team = teams.find(t => t.id === it.teamId); + delete it.teamId; + } + if (it.userId) { + it.user = users.find(u => u.id === it.userId); + delete it.userId; + } + + return db.crateOwnership.create(it); + }); + + let versions = VERSIONS.map(it => { + if (it.crateId) { + it.crate = crates.find(c => c.name === it.crateId); + delete it.crateId; + } + + return db.version.create(it); + }); + + DEPENDENCIES.forEach(it => { + if (it.crateId) { + it.crate = crates.find(c => c.name === it.crateId); + delete it.crateId; + } + if (it.versionId) { + it.version = versions.find(v => v.id === it.versionId); + delete it.versionId; + } + + return db.dependency.create(it); + }); + + VERSION_DOWNLOADS.forEach(it => { + if (it.versionId) { + it.version = versions.find(v => v.id === it.versionId); + delete it.versionId; + } + + return db.versionDownload.create(it); + }); +} diff --git a/packages/crates-io-msw/fixtures.test.js b/packages/crates-io-msw/fixtures.test.js new file mode 100644 index 00000000000..300d0786ae3 --- /dev/null +++ b/packages/crates-io-msw/fixtures.test.js @@ -0,0 +1,8 @@ +import { test } from 'vitest'; + +import { loadFixtures } from './fixtures.js'; +import { db } from './index.js'; + +test('loadFixtures() succeeds', async function () { + loadFixtures(db); +}); diff --git a/packages/crates-io-msw/fixtures/categories.js b/packages/crates-io-msw/fixtures/categories.js new file mode 100644 index 00000000000..05b37f8d55a --- /dev/null +++ b/packages/crates-io-msw/fixtures/categories.js @@ -0,0 +1,25 @@ +export default [ + { + category: 'API bindings', + created_at: '2017-01-20T14:51:49Z', + description: + 'Idiomatic wrappers of specific APIs for convenient access from Rust. Includes HTTP API wrappers as well. Non-idiomatic or unsafe bindings can be found in External FFI bindings.', + id: 'api-bindings', + slug: 'api-bindings', + }, + { + category: 'Algorithms', + created_at: '2017-01-20T14:51:49Z', + description: 'Rust implementations of core algorithms such as hashing, sorting, searching, and more.', + id: 'algorithms', + slug: 'algorithms', + }, + { + category: 'Asynchronous', + created_at: '2017-01-20T14:51:49Z', + description: + 'Crates to help you deal with events independently of the main program flow, using techniques like futures, promises, waiting, or eventing.', + id: 'asynchronous', + slug: 'asynchronous', + }, +]; diff --git a/packages/crates-io-msw/fixtures/crate-ownerships.js b/packages/crates-io-msw/fixtures/crate-ownerships.js new file mode 100644 index 00000000000..83a87eafd18 --- /dev/null +++ b/packages/crates-io-msw/fixtures/crate-ownerships.js @@ -0,0 +1,18 @@ +export default [ + { + crateId: 'nanomsg', + teamId: 1, + }, + { + crateId: 'nanomsg', + teamId: 303, + }, + { + crateId: 'nanomsg', + userId: 2, + }, + { + crateId: 'nanomsg', + userId: 303, + }, +]; diff --git a/packages/crates-io-msw/fixtures/crates.js b/packages/crates-io-msw/fixtures/crates.js new file mode 100644 index 00000000000..b0431da0099 --- /dev/null +++ b/packages/crates-io-msw/fixtures/crates.js @@ -0,0 +1,297 @@ +export default [ + { + badges: [], + created_at: '2014-11-23T09:01:21Z', + description: 'A Kinetic protocol library written in Rust', + documentation: 'https://icorderi.github.io/kinetic-rust/doc/kinetic/', + downloads: 225, + recent_downloads: 125, + homepage: 'https://icorderi.github.io/icorderi/kinetic-rust', + id: 'kinetic-rust', + keywordIds: [], + name: 'kinetic-rust', + repository: 'https://github.com/icorderi/kinetic-rust/', + updated_at: '2015-04-21T00:15:49Z', + }, + { + badges: [], + created_at: '2014-12-08T02:08:06Z', + description: 'A high-level, Rust idiomatic wrapper around nanomsg.', + documentation: 'https://github.com/thehydroimpulse/nanomsg.rs', + downloads: 3888, + recent_downloads: 800, + homepage: 'https://github.com/thehydroimpulse/nanomsg.rs', + id: 'nanomsg', + keywordIds: ['network'], + name: 'nanomsg', + repository: 'https://github.com/thehydroimpulse/nanomsg.rs', + updated_at: '2016-12-28T08:40:00Z', + _extra_downloads: [ + { + date: '2017-02-02', + downloads: 41, + }, + { + date: '2017-02-07', + downloads: 14, + }, + ], + }, + { + created_at: '2015-02-27T11:52:13Z', + description: + 'Yo dawg, use Rust to generate Rust, right in your Rust. (See\n`external_mixin` to use scripting languages.)\n', + documentation: 'https://github.com/huonw/external_mixin#rust_mixin', + downloads: 477, + recent_downloads: 100, + exact_match: true, + homepage: 'https://github.com/huonw/external_mixin', + id: 'rust_mixin', + keywordIds: ['rust', 'plugin', 'code-generation'], + name: 'rust_mixin', + repository: 'https://github.com/huonw/external_mixin', + updated_at: '2015-02-27T11:52:13Z', + badges: [], + }, + { + created_at: '2015-02-27T11:51:58Z', + description: + 'Use your favourite interpreted language to generate your Rust, right\nin your Rust. Supports Python, Ruby and shell (`sh`) out of the box,\nwith an extensible macro to support any others. (See `rust_mixin` to\nbe able to use your all-time favourite language to generate your Rust.)\n', + documentation: 'https://github.com/huonw/external_mixin#external_mixin', + downloads: 497, + recent_downloads: 497, + homepage: 'https://github.com/huonw/external_mixin', + id: 'external_mixin', + keywordIds: ['python', 'ruby', 'shell', 'plugin', 'code-generation'], + name: 'external_mixin', + repository: 'https://github.com/huonw/external_mixin', + updated_at: '2015-02-27T11:51:58Z', + }, + { + created_at: '2015-02-27T11:51:40Z', + description: 'Backing library for `rust_mixin` and `external_mixin` to keep them\nDRY.\n', + documentation: 'https://github.com/huonw/external_mixin#external_mixin_base', + downloads: 989, + recent_downloads: 0, + homepage: 'https://github.com/huonw/external_mixin', + id: 'external_mixin_umbrella', + keywordIds: ['plugin', 'code-generation'], + name: 'external_mixin_umbrella', + repository: 'https://github.com/huonw/external_mixin', + updated_at: '2015-02-27T11:52:30Z', + }, + { + created_at: '2015-10-10T15:26:24Z', + description: + 'Adds String based inflections for Rust. Snake, kebab, camel, sentence, class, title, upper, and lower cases as well as ordinalize, deordinalize, demodulize, and foreign key are supported as both traits and pure functions acting on String types.\n', + documentation: 'http://whatisinternet.github.io/inflector/doc/inflector/', + downloads: 57, + recent_downloads: 1, + homepage: 'https://github.com/whatisinternet/inflector', + id: 'Inflector', + keywordIds: ['string', 'case', 'camel', 'snake', 'inflection'], + name: 'Inflector', + repository: 'https://github.com/whatisinternet/inflector', + updated_at: '2015-10-27T01:51:42Z', + }, + { + created_at: '2015-05-21T17:43:38Z', + description: 'Client for the ElasticSearch REST API', + documentation: 'http://benashford.github.io/rs-es/rs_es/index.html', + downloads: 321, + recent_downloads: 21, + homepage: null, + id: 'rs-es', + keywordIds: ['elasticsearch', 'elastic'], + name: 'rs-es', + repository: 'https://github.com/benashford/rs-es', + updated_at: '2015-09-09T15:34:50Z', + }, + { + created_at: '2014-11-21T05:12:08Z', + description: 'A (mostly) pure-Rust implementation of various common cryptographic algorithms.', + documentation: null, + downloads: 21_573, + recent_downloads: 2000, + homepage: 'https://github.com/DaGenix/rust-crypto/', + id: 'rust-crypto', + keywordIds: [], + name: 'rust-crypto', + repository: 'https://github.com/DaGenix/rust-crypto/', + updated_at: '2015-10-29T01:16:17Z', + }, + { + created_at: '2015-03-20T13:46:04Z', + description: 'This library provides HTSlib bindings and a high level Rust API for reading and writing BAM files.', + documentation: null, + downloads: 485, + recent_downloads: 85, + homepage: null, + id: 'rust-htslib', + keywordIds: [], + name: 'rust-htslib', + repository: 'https://github.com/rust-bio/rust-htslib.git', + updated_at: '2015-11-11T00:10:43Z', + }, + { + created_at: '2014-11-29T17:51:55Z', + description: 'Rustless is a REST-like API micro-framework for Rust.', + documentation: null, + downloads: 554, + recent_downloads: 500, + homepage: 'https://github.com/rustless/rustless', + id: 'rustless', + keywordIds: [], + name: 'rustless', + repository: 'https://crates.io/crates/rustless', + updated_at: '2015-10-31T11:49:29Z', + }, + { + created_at: '2014-12-05T20:20:39Z', + description: 'A generic serialization/deserialization framework', + documentation: 'https://serde-rs.github.io/serde/serde/serde/index.html', + downloads: 50_854, + recent_downloads: 854, + homepage: null, + id: 'serde', + keywordIds: [], + name: 'serde', + repository: 'https://github.com/serde-rs/serde', + updated_at: '2015-10-18T03:10:21Z', + }, + { + created_at: '2015-08-26T13:50:58Z', + description: 'Send cypher queries to a neo4j database', + documentation: 'http://livioribeiro.github.io/rusted_cypher/rusted_cypher/', + downloads: 156, + recent_downloads: 54, + homepage: 'https://github.com/livioribeiro/rusted-cypher', + id: 'rusted_cypher', + keywordIds: [], + name: 'rusted_cypher', + repository: 'https://github.com/livioribeiro/rusted-cypher', + updated_at: '2015-11-07T17:26:55Z', + }, + { + created_at: '2015-01-02T20:54:04Z', + description: + 'An (incomplete) port of zlib to Rust. The decompressor works, but the compressor has not yet been ported.', + documentation: null, + downloads: 223, + recent_downloads: 23, + homepage: null, + id: 'zlib', + keywordIds: [], + name: 'zlib', + repository: null, + updated_at: '2015-01-02T20:54:04Z', + }, + { + created_at: '2015-05-08T19:34:16Z', + description: + 'A light HTTP framework, with some REST-like features and the ambition of being simple, modular and non-intrusive.', + documentation: 'http://ogeon.github.io/docs/rustful/master/rustful/index.html', + downloads: 576, + recent_downloads: 76, + homepage: null, + id: 'rustful', + keywordIds: [], + name: 'rustful', + repository: 'https://github.com/Ogeon/rustful', + updated_at: '2015-09-19T21:10:27Z', + }, + { + created_at: '2014-11-24T02:34:44Z', + description: 'A native PostgreSQL driver', + documentation: 'https://sfackler.github.io/rust-postgres/doc/v0.10.1/postgres', + downloads: 13_449, + recent_downloads: 13, + homepage: null, + id: 'postgres', + keywordIds: [], + name: 'postgres', + repository: 'https://github.com/sfackler/rust-postgres', + updated_at: '2015-11-08T00:48:59Z', + }, + { + created_at: '2014-11-21T00:20:47Z', + description: 'Automatic property based testing with shrinking.', + documentation: 'http://burntsushi.net/rustdoc/quickcheck/', + downloads: 19_271, + recent_downloads: 143, + homepage: 'https://github.com/BurntSushi/quickcheck', + id: 'quickcheck', + keywordIds: [], + name: 'quickcheck', + repository: 'https://github.com/BurntSushi/quickcheck', + updated_at: '2015-09-20T21:53:38Z', + }, + { + created_at: '2014-11-21T00:21:04Z', + description: 'A macro attribute for quickcheck.', + documentation: 'http://burntsushi.net/rustdoc/quickcheck/', + downloads: 3796, + recent_downloads: 768, + homepage: 'https://github.com/BurntSushi/quickcheck', + id: 'quickcheck_macros', + keywordIds: [], + name: 'quickcheck_macros', + repository: 'https://github.com/BurntSushi/quickcheck', + updated_at: '2015-09-20T21:53:57Z', + }, + { + created_at: '2015-08-25T19:15:35Z', + description: + 'Lexical analysers generator for Rust, written in Rust (crate dedicated to rumblebars, divergences written by Nicolas Cherel)', + documentation: null, + downloads: 109, + recent_downloads: 0, + homepage: 'https://github.com/nicolas-cherel/rustlex', + id: 'unicorn-rpc', + keywordIds: [], + name: 'unicorn-rpc', + repository: 'https://github.com/nicolas-cherel/rustlex', + updated_at: '2015-08-25T19:15:35Z', + }, + { + created_at: '2015-01-17T17:47:52Z', + description: 'A byte-oriented, zero-copy, parser combinators library', + documentation: 'http://rust.unhandledexpression.com/nom/', + downloads: 5169, + recent_downloads: 69, + homepage: null, + id: 'nom', + keywordIds: [], + name: 'nom', + repository: 'https://github.com/Geal/nom', + updated_at: '2015-11-22T22:00:41Z', + }, + { + id: 'libc', + name: 'libc', + downloads: 5169, + recent_downloads: 69, + updated_at: '2015-04-21T00:15:49Z', + }, + { + id: 'nanomsg-sys', + name: 'nanomsg-sys', + downloads: 5169, + recent_downloads: 69, + updated_at: '2015-04-21T00:15:49Z', + }, + { + id: 'mock-build-deps', + name: 'mock-build-deps', + downloads: 5169, + recent_downloads: 69, + updated_at: '2015-04-21T00:15:49Z', + }, + { + id: 'mock-dev-deps', + name: 'mock-dev-deps', + downloads: 5169, + recent_downloads: 69, + updated_at: '2015-04-21T00:15:49Z', + }, +]; diff --git a/packages/crates-io-msw/fixtures/dependencies.js b/packages/crates-io-msw/fixtures/dependencies.js new file mode 100644 index 00000000000..ea9384d747f --- /dev/null +++ b/packages/crates-io-msw/fixtures/dependencies.js @@ -0,0 +1,57 @@ +export default [ + { + crateId: 'libc', + default_features: true, + features: '', + id: 146_231, + kind: 'normal', + optional: false, + req: '^0.2.18', + target: null, + versionId: 40_905, + }, + { + crateId: 'nanomsg-sys', + default_features: true, + features: '', + id: 146_232, + kind: 'normal', + optional: false, + req: '^0.6.1', + target: null, + versionId: 40_905, + }, + { + crateId: 'nanomsg', + default_features: true, + features: '', + id: 146_233, + kind: 'normal', + optional: false, + req: '^0.5.0', + target: null, + versionId: 28_674, + }, + { + crateId: 'mock-build-deps', + default_features: true, + features: '', + id: 146_234, + kind: 'build', + optional: false, + req: '^0.6.1', + target: null, + versionId: 40_905, + }, + { + crateId: 'mock-dev-deps', + default_features: true, + features: '', + id: 146_235, + kind: 'dev', + optional: true, + req: '^0.6.1', + target: null, + versionId: 40_905, + }, +]; diff --git a/packages/crates-io-msw/fixtures/keywords.js b/packages/crates-io-msw/fixtures/keywords.js new file mode 100644 index 00000000000..74b081c5cc0 --- /dev/null +++ b/packages/crates-io-msw/fixtures/keywords.js @@ -0,0 +1,72 @@ +export default [ + { + created_at: '2014-11-23T06:47:40Z', + id: 'network', + keyword: 'network', + }, + { + created_at: '2014-11-23T06:47:40Z', + id: 'rust', + keyword: 'rust', + }, + { + created_at: '2014-11-23T06:47:40Z', + id: 'plugin', + keyword: 'plugin', + }, + { + created_at: '2014-11-23T06:47:40Z', + id: 'code-generation', + keyword: 'code-generation', + }, + { + created_at: '2014-11-23T06:47:40Z', + id: 'python', + keyword: 'python', + }, + { + created_at: '2014-11-23T06:47:40Z', + id: 'ruby', + keyword: 'ruby', + }, + { + created_at: '2014-11-23T06:47:40Z', + id: 'shell', + keyword: 'shell', + }, + { + created_at: '2014-11-23T06:47:40Z', + id: 'string', + keyword: 'string', + }, + { + created_at: '2014-11-23T06:47:40Z', + id: 'case', + keyword: 'case', + }, + { + created_at: '2014-11-23T06:47:40Z', + id: 'camel', + keyword: 'camel', + }, + { + created_at: '2014-11-23T06:47:40Z', + id: 'snake', + keyword: 'snake', + }, + { + created_at: '2014-11-23T06:47:40Z', + id: 'inflection', + keyword: 'inflection', + }, + { + created_at: '2014-11-23T06:47:40Z', + id: 'elastic', + keyword: 'elastic', + }, + { + created_at: '2014-11-23T06:47:40Z', + id: 'elasticsearch', + keyword: 'elasticsearch', + }, +]; diff --git a/packages/crates-io-msw/fixtures/teams.js b/packages/crates-io-msw/fixtures/teams.js new file mode 100644 index 00000000000..0c753788449 --- /dev/null +++ b/packages/crates-io-msw/fixtures/teams.js @@ -0,0 +1,16 @@ +export default [ + { + avatar: 'https://avatars.githubusercontent.com/u/565790?v=3', + id: 1, + login: 'github:org:thehydroimpulse', + name: 'thehydroimpulseteam', + url: 'https://github.com/org_test', + }, + { + avatar: 'https://avatars.githubusercontent.com/u/9447137?v=3', + id: 303, + login: 'github:org:blabaere', + name: 'Team Benoît Labaere', + url: 'https://github.com/blabaere', + }, +]; diff --git a/packages/crates-io-msw/fixtures/users.js b/packages/crates-io-msw/fixtures/users.js new file mode 100644 index 00000000000..00243bdce19 --- /dev/null +++ b/packages/crates-io-msw/fixtures/users.js @@ -0,0 +1,26 @@ +export default [ + { + avatar: 'https://avatars0.githubusercontent.com/u/9447137?v=3', + email: null, + id: 303, + login: 'blabaere', + name: 'Benoît Labaere', + url: 'https://github.com/blabaere', + }, + { + avatar: 'https://avatars.githubusercontent.com/u/565790?v=3', + email: 'dnfagnan@gmail.com', + id: 2, + login: 'thehydroimpulse', + name: 'Daniel Fagnan', + url: 'https://github.com/thehydroimpulse', + }, + { + avatar: 'https://avatars3.githubusercontent.com/u/1179195?v=3', + email: 'iain@fastmail.com', + id: 10_982, + login: 'iain8', + name: 'Iain Buchanan', + url: 'https://github.com/iain8', + }, +]; diff --git a/packages/crates-io-msw/fixtures/version-downloads.js b/packages/crates-io-msw/fixtures/version-downloads.js new file mode 100644 index 00000000000..55aee7c1e6b --- /dev/null +++ b/packages/crates-io-msw/fixtures/version-downloads.js @@ -0,0 +1,17 @@ +export default [ + { + date: '2017-02-10T00:00:00Z', + downloads: 2, + versionId: 40_905, + }, + { + date: '2017-02-10T00:00:00Z', + downloads: 1, + versionId: 18_445, + }, + { + date: '2017-02-11T00:00:00Z', + downloads: 1, + versionId: 40_905, + }, +]; diff --git a/packages/crates-io-msw/fixtures/versions.js b/packages/crates-io-msw/fixtures/versions.js new file mode 100644 index 00000000000..87f37e27afc --- /dev/null +++ b/packages/crates-io-msw/fixtures/versions.js @@ -0,0 +1,412 @@ +export default [ + { + crateId: 'nanomsg', + created_at: '2016-12-20T07:30:00Z', + downloads: 260, + features: { + bundled: ['nanomsg-sys/bundled'], + }, + id: 40_906, + num: '0.7.0-alpha.1', + updated_at: '2016-12-20T07:30:00Z', + yanked: false, + license: 'MIT', + crate_size: 912_355, + }, + { + crateId: 'nanomsg', + created_at: '2016-12-27T08:40:00Z', + downloads: 260, + features: { + bundled: ['nanomsg-sys/bundled'], + }, + id: 40_905, + num: '0.6.1', + crate_size: 8_123_545, + updated_at: '2016-12-27T08:40:00Z', + yanked: false, + license: 'Apache-2.0', + }, + { + crateId: 'nanomsg', + created_at: '2016-06-10T20:03:55Z', + downloads: 904, + features: {}, + id: 28_431, + num: '0.6.0', + updated_at: '2016-06-10T20:03:55Z', + yanked: false, + license: 'Apache-2.0', + }, + { + crateId: 'nanomsg', + created_at: '2016-01-24T22:07:58Z', + downloads: 1217, + features: {}, + id: 21_273, + num: '0.5.0', + updated_at: '2016-01-24T22:07:58Z', + yanked: false, + license: 'MIT/Apache-2.0', + }, + { + crateId: 'nanomsg', + created_at: '2015-11-23T12:10:09Z', + downloads: 318, + features: {}, + id: 18_445, + num: '0.4.2', + updated_at: '2015-12-16T00:01:56Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'nanomsg', + created_at: '2015-10-29T22:13:45Z', + downloads: 168, + features: {}, + id: 17_384, + num: '0.4.1', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'nanomsg', + created_at: '2015-07-23T05:54:44Z', + downloads: 311, + features: {}, + id: 13_574, + num: '0.4.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'nanomsg', + created_at: '2015-04-18T20:45:03Z', + downloads: 237, + features: {}, + id: 9014, + num: '0.3.4', + updated_at: '2015-12-15T00:03:39Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'nanomsg', + created_at: '2015-04-06T18:57:47Z', + downloads: 99, + features: {}, + id: 8236, + num: '0.3.3', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'nanomsg', + created_at: '2015-03-26T06:51:10Z', + downloads: 98, + features: {}, + id: 7190, + num: '0.3.2', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'Apache-2.0', + }, + { + crateId: 'nanomsg', + created_at: '2015-02-12T20:20:32Z', + downloads: 95, + features: {}, + id: 4944, + num: '0.3.1', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT/Apache-2.0', + }, + { + crateId: 'nanomsg', + created_at: '2014-12-08T16:21:01Z', + downloads: 102, + features: {}, + id: 940, + num: '0.3.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'nanomsg', + created_at: '2014-12-08T02:08:06Z', + downloads: 79, + features: {}, + id: 924, + num: '0.2.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'unicorn-rpc', + created_at: '2014-12-08T02:08:06Z', + downloads: 79, + features: {}, + id: 28_674, + num: '0.2.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'external_mixin', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'external_mixin_umbrella', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'Inflector', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'kinetic-rust', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '0.0.16', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'libc', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'mock-build-deps', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'mock-dev-deps', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'nanomsg-sys', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'nom', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'postgres', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'quickcheck', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'quickcheck_macros', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'rs-es', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'rust-crypto', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'rust-htslib', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'rust_mixin', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'rusted_cypher', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'rustful', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'rustless', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'serde', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'zlib', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'rustful', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'rustful', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'rustful', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, + { + crateId: 'rustful', + created_at: '2014-12-08T02:08:06Z', + downloads: 0, + features: {}, + num: '1.0.0', + updated_at: '2015-12-11T23:54:29Z', + yanked: false, + license: 'MIT', + }, +]; From 4aac5e97eacce270a82c361a78f2797499b0ca60 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 16 Jan 2025 16:27:48 +0100 Subject: [PATCH 079/189] msw: Serve `mockServiceWorker.js` in dev/test mode --- ember-cli-build.js | 12 ++++++++++++ package.json | 2 ++ pnpm-lock.yaml | 6 ++++++ 3 files changed, 20 insertions(+) diff --git a/ember-cli-build.js b/ember-cli-build.js index 023ea06019f..f78034e195f 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -6,6 +6,17 @@ module.exports = function (defaults) { let env = EmberApp.env(); let isProd = env === 'production'; + let extraPublicTrees = []; + if (!isProd) { + const path = require('node:path'); + const funnel = require('broccoli-funnel'); + + let mswPath = require.resolve('msw/mockServiceWorker.js'); + let mswParentPath = path.dirname(mswPath); + + extraPublicTrees.push(funnel(mswParentPath, { include: ['mockServiceWorker.js'] })); + } + let browsers = require('./config/targets').browsers; let app = new EmberApp(defaults, { @@ -63,6 +74,7 @@ module.exports = function (defaults) { const { Webpack } = require('@embroider/webpack'); return require('@embroider/compat').compatBuild(app, Webpack, { + extraPublicTrees, staticAddonTrees: true, staticAddonTestSupportTrees: true, staticModifiers: true, diff --git a/package.json b/package.json index 54bb7406f55..d04d9f425b2 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@types/sinonjs__fake-timers": "8.1.5", "@zestia/ember-auto-focus": "5.1.0", "broccoli-asset-rev": "3.0.0", + "broccoli-funnel": "3.0.8", "ember-a11y-testing": "7.0.2", "ember-auto-import": "2.10.0", "ember-cli": "6.1.0", @@ -128,6 +129,7 @@ "loader.js": "4.7.0", "match-json": "1.3.7", "miragejs": "0.1.48", + "msw": "2.7.0", "normalize.css": "8.0.1", "nyc": "17.1.0", "postcss-preset-env": "10.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30771906887..782a952afbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,6 +118,9 @@ importers: broccoli-asset-rev: specifier: 3.0.0 version: 3.0.0 + broccoli-funnel: + specifier: 3.0.8 + version: 3.0.8 ember-a11y-testing: specifier: 7.0.2 version: 7.0.2(@babel/core@7.26.7)(@ember/test-helpers@4.0.5(@babel/core@7.26.7)(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1)))(qunit@2.24.1)(webpack@5.97.1) @@ -271,6 +274,9 @@ importers: miragejs: specifier: 0.1.48 version: 0.1.48 + msw: + specifier: 2.7.0 + version: 2.7.0(@types/node@22.10.10)(typescript@5.7.3) normalize.css: specifier: 8.0.1 version: 8.0.1 From 62c540c1830e3230213041dea47504e2b34c0f0e Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 17 Jan 2025 11:16:16 +0100 Subject: [PATCH 080/189] msw: Add `msw: true` option to `setupApplicationTest()` fn --- package.json | 1 + pnpm-lock.yaml | 3 +++ tests/helpers/index.js | 13 +++++++++++-- tests/helpers/setup-msw.js | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 tests/helpers/setup-msw.js diff --git a/package.json b/package.json index d04d9f425b2..34ecbe8d5ba 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@babel/core": "7.26.7", "@babel/eslint-parser": "7.26.5", "@babel/plugin-proposal-decorators": "7.25.9", + "@crates-io/msw": "workspace:*", "@ember/optional-features": "2.2.0", "@ember/render-modifiers": "2.1.0", "@ember/string": "3.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 782a952afbd..7e7d20dd7b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,6 +55,9 @@ importers: '@babel/plugin-proposal-decorators': specifier: 7.25.9 version: 7.25.9(@babel/core@7.26.7) + '@crates-io/msw': + specifier: workspace:* + version: link:packages/crates-io-msw '@ember/optional-features': specifier: 2.2.0 version: 2.2.0 diff --git a/tests/helpers/index.js b/tests/helpers/index.js index b33c551cdb2..3f948e689e0 100644 --- a/tests/helpers/index.js +++ b/tests/helpers/index.js @@ -2,14 +2,23 @@ import { setupApplicationTest as upstreamSetupApplicationTest } from 'ember-quni import { setupSentryMock } from './sentry'; import setupMirage from './setup-mirage'; +import setupMSW from './setup-msw'; export { default as setupMirage } from './setup-mirage'; export { setupTest, setupRenderingTest } from 'ember-qunit'; // see http://emberjs.github.io/rfcs/0637-customizable-test-setups.html -export function setupApplicationTest(hooks, options) { +export function setupApplicationTest(hooks, options = {}) { + let { msw } = options; + upstreamSetupApplicationTest(hooks, options); - setupMirage(hooks); + + if (msw) { + setupMSW(hooks); + } else { + setupMirage(hooks); + } + setupSentryMock(hooks); setupAppTestDataAttr(hooks); } diff --git a/tests/helpers/setup-msw.js b/tests/helpers/setup-msw.js new file mode 100644 index 00000000000..364a36de7ef --- /dev/null +++ b/tests/helpers/setup-msw.js @@ -0,0 +1,34 @@ +import { db, handlers } from '@crates-io/msw'; +import window from 'ember-window-mock'; +import { setupWindowMock } from 'ember-window-mock/test-support'; +import { http, passthrough } from 'msw'; +import { setupWorker } from 'msw/browser'; + +import { setupFakeTimers } from './fake-timers'; + +export default function (hooks) { + setupWindowMock(hooks); + setupFakeTimers(hooks, '2017-11-20T12:00:00'); + + let worker = setupWorker( + ...handlers, + http.get('/assets/*', passthrough), + http.all(/.*\/percy\/.*/, passthrough), + http.get('https://:avatars.githubusercontent.com/u/:id', passthrough), + ); + + hooks.before(() => worker.start({ quiet: true, onUnhandledRequest: 'error' })); + hooks.afterEach(() => worker.resetHandlers()); + hooks.afterEach(() => db.reset()); + hooks.after(() => worker.stop()); + + hooks.beforeEach(function () { + this.worker = worker; + this.db = db; + + this.authenticateAs = user => { + db.mswSession.create({ user }); + window.localStorage.setItem('isLoggedIn', '1'); + }; + }); +} From 2e3f7c353487f7abc24375b1440834709a7e241f Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 12:04:47 +0100 Subject: [PATCH 081/189] msw: Prevent `@mswjs/data` from bundling the `msw` package --- ember-cli-build.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ember-cli-build.js b/ember-cli-build.js index f78034e195f..fa2f9f30696 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -80,6 +80,19 @@ module.exports = function (defaults) { staticModifiers: true, packagerOptions: { webpackConfig: { + externals: ({ request, context }, callback) => { + // Prevent `@mswjs/data` from bundling the `msw` package. + // + // `@crates-io/msw` is importing the ESM build of the `msw` package, but + // `@mswjs/data` is trying to import the CJS build instead. This is causing + // a conflict within webpack. Since we don't need the functionality within + // `@mswjs/data` that requires the `msw` package, we can safely ignore this + // import. + if (request == 'msw' && context.includes('@mswjs/data')) { + return callback(null, request, 'global'); + } + callback(); + }, resolve: { fallback: { // disables `crypto` import warning in `axe-core` From d2474d95dfdd569e92c4508df99ab510d0053724 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 12:34:01 +0100 Subject: [PATCH 082/189] tests/acceptance/api-token: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/api-token-test.js | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/tests/acceptance/api-token-test.js b/tests/acceptance/api-token-test.js index 9a571fec206..68b5f0c421a 100644 --- a/tests/acceptance/api-token-test.js +++ b/tests/acceptance/api-token-test.js @@ -2,31 +2,31 @@ import { click, currentURL, fillIn, findAll } from '@ember/test-helpers'; import { module, test } from 'qunit'; import percySnapshot from '@percy/ember'; -import { Response } from 'miragejs'; +import { http, HttpResponse } from 'msw'; import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Acceptance | api-tokens', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); function prepare(context) { - let user = context.server.create('user', { + let user = context.db.user.create({ login: 'johnnydee', name: 'John Doe', email: 'john@doe.com', avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', }); - context.server.create('api-token', { + context.db.apiToken.create({ user, name: 'foo', createdAt: '2017-08-01T12:34:56', lastUsedAt: '2017-11-02T01:45:14', }); - context.server.create('api-token', { + context.db.apiToken.create({ user, name: 'BAR', createdAt: '2017-11-19T17:59:22', @@ -34,7 +34,7 @@ module('Acceptance | api-tokens', function (hooks) { expiredAt: '2017-12-19T17:59:22', }); - context.server.create('api-token', { + context.db.apiToken.create({ user, name: 'recently expired', createdAt: '2017-08-01T12:34:56', @@ -92,11 +92,7 @@ module('Acceptance | api-tokens', function (hooks) { assert.dom('[data-test-api-token]').exists({ count: 3 }); await click('[data-test-api-token="1"] [data-test-revoke-token-button]'); - assert.strictEqual( - this.server.schema.apiTokens.all().length, - 2, - 'API token has been deleted from the backend database', - ); + assert.strictEqual(this.db.apiToken.findMany({}).length, 2, 'API token has been deleted from the backend database'); assert.dom('[data-test-api-token]').exists({ count: 2 }); assert.dom('[data-test-api-token="2"]').exists(); @@ -117,9 +113,11 @@ module('Acceptance | api-tokens', function (hooks) { test('failed API tokens revocation shows an error', async function (assert) { prepare(this); - this.server.delete('/api/v1/me/tokens/:id', function () { - return new Response(500, {}, {}); - }); + this.worker.use( + http.delete('/api/v1/me/tokens/:id', function () { + return HttpResponse.json({}, { status: 500 }); + }), + ); await visit('/settings/tokens'); assert.strictEqual(currentURL(), '/settings/tokens'); @@ -150,7 +148,7 @@ module('Acceptance | api-tokens', function (hooks) { await click('[data-test-generate]'); - let token = this.server.schema.apiTokens.findBy({ name: 'the new token' }); + let token = this.db.apiToken.findFirst({ where: { name: { equals: 'the new token' } } }); assert.ok(Boolean(token), 'API token has been created in the backend database'); assert.dom('[data-test-api-token="4"] [data-test-name]').hasText('the new token'); @@ -170,7 +168,7 @@ module('Acceptance | api-tokens', function (hooks) { await click('[data-test-scope="publish-update"]'); await click('[data-test-generate]'); - let token = this.server.schema.apiTokens.findBy({ name: 'the new token' }); + let token = this.db.apiToken.findFirst({ where: { name: { equals: 'the new token' } } }); assert.dom('[data-test-token]').hasText(token.token); // leave the API tokens page From 8ff81d9318e2d5c3dd06ef12635bbbe598cfd7e2 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 12:36:28 +0100 Subject: [PATCH 083/189] tests/acceptance/readme-rendering: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/readme-rendering-test.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/tests/acceptance/readme-rendering-test.js b/tests/acceptance/readme-rendering-test.js index 946cd792671..db6d8cb48ce 100644 --- a/tests/acceptance/readme-rendering-test.js +++ b/tests/acceptance/readme-rendering-test.js @@ -2,7 +2,7 @@ import { click } from '@ember/test-helpers'; import { module, test } from 'qunit'; import percySnapshot from '@percy/ember'; -import { Response } from 'miragejs'; +import { http, HttpResponse } from 'msw'; import { setupApplicationTest } from 'crates-io/tests/helpers'; @@ -92,11 +92,11 @@ graph TD; `; module('Acceptance | README rendering', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('it works', async function (assert) { - let crate = this.server.create('crate', { name: 'serde' }); - this.server.create('version', { crate, num: '1.0.0', readme: README_HTML }); + let crate = this.db.crate.create({ name: 'serde' }); + this.db.version.create({ crate, num: '1.0.0', readme: README_HTML }); await visit('/crates/serde'); assert.dom('[data-test-readme]').exists(); @@ -108,29 +108,26 @@ module('Acceptance | README rendering', function (hooks) { }); test('it shows a fallback if no readme is available', async function (assert) { - let crate = this.server.create('crate', { name: 'serde' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'serde' }); + this.db.version.create({ crate, num: '1.0.0' }); await visit('/crates/serde'); assert.dom('[data-test-no-readme]').exists(); }); test('it shows an error message and retry button if loading fails', async function (assert) { - let crate = this.server.create('crate', { name: 'serde' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'serde' }); + this.db.version.create({ crate, num: '1.0.0' }); // Simulate a server error when fetching the README - this.server.get('/api/v1/crates/:name/:version/readme', {}, 500); + this.worker.use(http.get('/api/v1/crates/:name/:version/readme', () => HttpResponse.html('', { status: 500 }))); await visit('/crates/serde'); assert.dom('[data-test-readme-error]').exists(); assert.dom('[data-test-retry-button]').exists(); // Simulate a successful response when fetching the README - this.server.get( - '/api/v1/crates/:name/:version/readme', - () => new Response(200, { 'Content-Type': 'text/html' }, 'foo'), - ); + this.worker.use(http.get('/api/v1/crates/:name/:version/readme', () => HttpResponse.html('foo'))); await click('[data-test-retry-button]'); assert.dom('[data-test-readme]').hasText('foo'); From 6bcd3198ce328610638a7ca9fe3b435fdf2660e2 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 13:13:33 +0100 Subject: [PATCH 084/189] tests/acceptance/front-page: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/front-page-test.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/acceptance/front-page-test.js b/tests/acceptance/front-page-test.js index bb0f73d28ee..864f91b2e0b 100644 --- a/tests/acceptance/front-page-test.js +++ b/tests/acceptance/front-page-test.js @@ -3,22 +3,23 @@ import { module, test } from 'qunit'; import { defer } from 'rsvp'; +import { loadFixtures } from '@crates-io/msw/fixtures.js'; import percySnapshot from '@percy/ember'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { getPageTitle } from 'ember-page-title/test-support'; +import { http, HttpResponse } from 'msw'; import { setupApplicationTest } from 'crates-io/tests/helpers'; -import { summary } from '../../mirage/route-handlers/summary'; import axeConfig from '../axe-config'; module('Acceptance | front page', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('visiting /', async function (assert) { this.owner.lookup('service:intl').locale = 'en'; - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/'); @@ -32,8 +33,8 @@ module('Acceptance | front page', function (hooks) { assert.dom('[data-test-total-downloads] [data-test-value]').hasText('143,345'); assert.dom('[data-test-total-crates] [data-test-value]').hasText('23'); - assert.dom('[data-test-new-crates] [data-test-crate-link="0"]').hasText('Inflector v1.0.0'); - assert.dom('[data-test-new-crates] [data-test-crate-link="0"]').hasAttribute('href', '/crates/Inflector'); + assert.dom('[data-test-new-crates] [data-test-crate-link="0"]').hasText('serde v1.0.0'); + assert.dom('[data-test-new-crates] [data-test-crate-link="0"]').hasAttribute('href', '/crates/serde'); assert.dom('[data-test-most-downloaded] [data-test-crate-link="0"]').hasText('serde'); assert.dom('[data-test-most-downloaded] [data-test-crate-link="0"]').hasAttribute('href', '/crates/serde'); @@ -46,7 +47,7 @@ module('Acceptance | front page', function (hooks) { }); test('error handling', async function (assert) { - this.server.get('/api/v1/summary', {}, 500); + this.worker.use(http.get('/api/v1/summary', () => HttpResponse.json({}, { status: 500 }))); await visit('/'); assert.dom('[data-test-lists]').doesNotExist(); @@ -54,10 +55,8 @@ module('Acceptance | front page', function (hooks) { assert.dom('[data-test-try-again-button]').isEnabled(); let deferred = defer(); - this.server.get('/api/v1/summary', async function (schema, request) { - await deferred.promise; - return summary.call(this, schema, request); - }); + this.worker.resetHandlers(); + this.worker.use(http.get('/api/v1/summary', () => deferred.promise)); click('[data-test-try-again-button]'); await waitFor('[data-test-try-again-button] [data-test-spinner]'); From 0987e3607d2aa1021e07c64dc49756b68a0ca012 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 13:19:42 +0100 Subject: [PATCH 085/189] tests/acceptance/settings/add-owner: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/settings/add-owner-test.js | 30 ++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/acceptance/settings/add-owner-test.js b/tests/acceptance/settings/add-owner-test.js index 7f3285256a1..50ae4130701 100644 --- a/tests/acceptance/settings/add-owner-test.js +++ b/tests/acceptance/settings/add-owner-test.js @@ -4,22 +4,22 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'crates-io/tests/helpers'; module('Acceptance | Settings | Add Owner', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); function prepare(context) { - let { server } = context; + let { db } = context; - let user1 = server.create('user', { name: 'blabaere' }); - let user2 = server.create('user', { name: 'thehydroimpulse' }); - let team1 = server.create('team', { org: 'org', name: 'blabaere' }); - let team2 = server.create('team', { org: 'org', name: 'thehydroimpulse' }); + let user1 = db.user.create({ name: 'blabaere' }); + let user2 = db.user.create({ name: 'thehydroimpulse' }); + let team1 = db.team.create({ org: 'org', name: 'blabaere' }); + let team2 = db.team.create({ org: 'org', name: 'thehydroimpulse' }); - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '1.0.0' }); - server.create('crate-ownership', { crate, user: user1 }); - server.create('crate-ownership', { crate, user: user2 }); - server.create('crate-ownership', { crate, team: team1 }); - server.create('crate-ownership', { crate, team: team2 }); + let crate = db.crate.create({ name: 'nanomsg' }); + db.version.create({ crate, num: '1.0.0' }); + db.crateOwnership.create({ crate, user: user1 }); + db.crateOwnership.create({ crate, user: user2 }); + db.crateOwnership.create({ crate, team: team1 }); + db.crateOwnership.create({ crate, team: team2 }); context.authenticateAs(user1); @@ -51,7 +51,7 @@ module('Acceptance | Settings | Add Owner', function (hooks) { test('add a new owner', async function (assert) { prepare(this); - this.server.create('user', { name: 'iain8' }); + this.db.user.create({ name: 'iain8' }); await visit('/crates/nanomsg/settings'); await fillIn('input[name="username"]', 'iain8'); @@ -65,8 +65,8 @@ module('Acceptance | Settings | Add Owner', function (hooks) { test('add a team owner', async function (assert) { prepare(this); - this.server.create('user', { name: 'iain8' }); - this.server.create('team', { org: 'rust-lang', name: 'crates-io' }); + this.db.user.create({ name: 'iain8' }); + this.db.team.create({ org: 'rust-lang', name: 'crates-io' }); await visit('/crates/nanomsg/settings'); await fillIn('input[name="username"]', 'github:rust-lang:crates-io'); From 24e48efcc2978c7ccc713f0123a9a7f8d3a17a30 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 13:24:05 +0100 Subject: [PATCH 086/189] tests/acceptance/settings/remove-owner: Migrate from `mirage` to `@crates-io/msw` --- .../acceptance/settings/remove-owner-test.js | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/tests/acceptance/settings/remove-owner-test.js b/tests/acceptance/settings/remove-owner-test.js index 363f0bb2c3c..5bcb55973ed 100644 --- a/tests/acceptance/settings/remove-owner-test.js +++ b/tests/acceptance/settings/remove-owner-test.js @@ -1,25 +1,27 @@ import { click, visit } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import { http, HttpResponse } from 'msw'; + import { setupApplicationTest } from 'crates-io/tests/helpers'; module('Acceptance | Settings | Remove Owner', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); function prepare(context) { - let { server } = context; + let { db } = context; - let user1 = server.create('user', { name: 'blabaere' }); - let user2 = server.create('user', { name: 'thehydroimpulse' }); - let team1 = server.create('team', { org: 'org', name: 'blabaere' }); - let team2 = server.create('team', { org: 'org', name: 'thehydroimpulse' }); + let user1 = db.user.create({ name: 'blabaere' }); + let user2 = db.user.create({ name: 'thehydroimpulse' }); + let team1 = db.team.create({ org: 'org', name: 'blabaere' }); + let team2 = db.team.create({ org: 'org', name: 'thehydroimpulse' }); - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '1.0.0' }); - server.create('crate-ownership', { crate, user: user1 }); - server.create('crate-ownership', { crate, user: user2 }); - server.create('crate-ownership', { crate, team: team1 }); - server.create('crate-ownership', { crate, team: team2 }); + let crate = db.crate.create({ name: 'nanomsg' }); + db.version.create({ crate, num: '1.0.0' }); + db.crateOwnership.create({ crate, user: user1 }); + db.crateOwnership.create({ crate, user: user2 }); + db.crateOwnership.create({ crate, team: team1 }); + db.crateOwnership.create({ crate, team: team2 }); context.authenticateAs(user1); @@ -41,7 +43,9 @@ module('Acceptance | Settings | Remove Owner', function (hooks) { // we are intentionally returning a 200 response here, because is what // the real backend also returns due to legacy reasons - this.server.delete('/api/v1/crates/nanomsg/owners', { errors: [{ detail: 'nope' }] }); + this.worker.use( + http.delete('/api/v1/crates/nanomsg/owners', () => HttpResponse.json({ errors: [{ detail: 'nope' }] })), + ); await visit(`/crates/${crate.name}/settings`); await click(`[data-test-owner-user="${user2.login}"] [data-test-remove-owner-button]`); @@ -67,7 +71,9 @@ module('Acceptance | Settings | Remove Owner', function (hooks) { // we are intentionally returning a 200 response here, because is what // the real backend also returns due to legacy reasons - this.server.delete('/api/v1/crates/nanomsg/owners', { errors: [{ detail: 'nope' }] }); + this.worker.use( + http.delete('/api/v1/crates/nanomsg/owners', () => HttpResponse.json({ errors: [{ detail: 'nope' }] })), + ); await visit(`/crates/${crate.name}/settings`); await click(`[data-test-owner-team="${team1.login}"] [data-test-remove-owner-button]`); From 2baf29d2ff9a682c80fa3f0d074b60eae6d3ab8d Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 13:25:13 +0100 Subject: [PATCH 087/189] tests/acceptance/404: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/404-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/acceptance/404-test.js b/tests/acceptance/404-test.js index cc612721d6a..c57f72e5ed9 100644 --- a/tests/acceptance/404-test.js +++ b/tests/acceptance/404-test.js @@ -6,7 +6,7 @@ import percySnapshot from '@percy/ember'; import { setupApplicationTest } from 'crates-io/tests/helpers'; module('Acceptance | 404', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('/unknown-route shows a 404 page', async function (assert) { await visit('/unknown-route'); From 3371f2986c84451ef26930d12f1d1ac41e861c23 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 13:30:38 +0100 Subject: [PATCH 088/189] tests/acceptance/categories: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/categories-test.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/acceptance/categories-test.js b/tests/acceptance/categories-test.js index 11f14f273a3..aa909073e3b 100644 --- a/tests/acceptance/categories-test.js +++ b/tests/acceptance/categories-test.js @@ -9,17 +9,17 @@ import { setupApplicationTest } from 'crates-io/tests/helpers'; import axeConfig from '../axe-config'; module('Acceptance | categories', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('listing categories', async function (assert) { this.owner.lookup('service:intl').locale = 'en'; - this.server.create('category', { category: 'API bindings' }); - this.server.create('category', { category: 'Algorithms' }); - this.server.createList('crate', 1, { categoryIds: ['algorithms'] }); - this.server.create('category', { category: 'Asynchronous' }); - this.server.createList('crate', 15, { categoryIds: ['asynchronous'] }); - this.server.create('category', { category: 'Everything', crates_cnt: 1234 }); + this.db.category.create({ category: 'API bindings' }); + let algos = this.db.category.create({ category: 'Algorithms' }); + this.db.crate.create({ categories: [algos] }); + let async = this.db.category.create({ category: 'Asynchronous' }); + Array.from({ length: 15 }, () => this.db.crate.create({ categories: [async] })); + this.db.category.create({ category: 'Everything', crates_cnt: 1234 }); await visit('/categories'); @@ -35,14 +35,14 @@ module('Acceptance | categories', function (hooks) { test('listing categories (locale: de)', async function (assert) { this.owner.lookup('service:intl').locale = 'de'; - this.server.create('category', { category: 'Everything', crates_cnt: 1234 }); + this.db.category.create({ category: 'Everything', crates_cnt: 1234 }); await visit('/categories'); assert.dom('[data-test-category="everything"] [data-test-crate-count]').hasText('1.234 crates'); }); test('category/:category_id index default sort is recent-downloads', async function (assert) { - this.server.create('category', { category: 'Algorithms' }); + this.db.category.create({ category: 'Algorithms' }); await visit('/categories/algorithms'); @@ -53,8 +53,8 @@ module('Acceptance | categories', function (hooks) { }); test('listing category slugs', async function (assert) { - this.server.create('category', { category: 'Algorithms', description: 'Crates for algorithms' }); - this.server.create('category', { category: 'Asynchronous', description: 'Async crates' }); + this.db.category.create({ category: 'Algorithms', description: 'Crates for algorithms' }); + this.db.category.create({ category: 'Asynchronous', description: 'Async crates' }); await visit('/category_slugs'); From a8ba731cbacb0fe45d44c283907cb609c18c75dd Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 13:32:01 +0100 Subject: [PATCH 089/189] tests/acceptance/crate-deletion: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/crate-deletion-test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/acceptance/crate-deletion-test.js b/tests/acceptance/crate-deletion-test.js index c3664f57d9c..22356f80dba 100644 --- a/tests/acceptance/crate-deletion-test.js +++ b/tests/acceptance/crate-deletion-test.js @@ -6,15 +6,15 @@ import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Acceptance | crate deletion', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('happy path', async function (assert) { - let user = this.server.create('user'); + let user = this.db.user.create(); this.authenticateAs(user); - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate }); - this.server.create('crate-ownership', { crate, user }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate }); + this.db.crateOwnership.create({ crate, user }); await visit('/crates/foo'); assert.strictEqual(currentURL(), '/crates/foo'); @@ -39,7 +39,7 @@ module('Acceptance | crate deletion', function (hooks) { let message = 'Crate foo has been successfully deleted.'; assert.dom('[data-test-notification-message="success"]').hasText(message); - crate = this.server.schema.crates.findBy({ name: 'foo' }); + crate = this.db.crate.findFirst({ where: { name: { equals: 'foo' } } }); assert.strictEqual(crate, null); }); }); From a377ba1d22ef10f8891e62b0b3fa6c1a61799a3b Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 13:38:46 +0100 Subject: [PATCH 090/189] tests/acceptance/crate-dependencies: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/crate-dependencies-test.js | 48 +++++++++++---------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/tests/acceptance/crate-dependencies-test.js b/tests/acceptance/crate-dependencies-test.js index c009c89c223..29fb4ef6be9 100644 --- a/tests/acceptance/crate-dependencies-test.js +++ b/tests/acceptance/crate-dependencies-test.js @@ -1,9 +1,11 @@ import { currentURL } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import { loadFixtures } from '@crates-io/msw/fixtures.js'; import percySnapshot from '@percy/ember'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { getPageTitle } from 'ember-page-title/test-support'; +import { http, HttpResponse } from 'msw'; import { setupApplicationTest } from 'crates-io/tests/helpers'; @@ -11,10 +13,10 @@ import axeConfig from '../axe-config'; import { visit } from '../helpers/visit-ignoring-abort'; module('Acceptance | crate dependencies page', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('shows the lists of dependencies', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/crates/nanomsg/dependencies'); assert.strictEqual(currentURL(), '/crates/nanomsg/0.6.1/dependencies'); @@ -29,8 +31,8 @@ module('Acceptance | crate dependencies page', function (hooks) { }); test('empty list case', async function (assert) { - let crate = this.server.create('crate', { name: 'nanomsg' }); - this.server.create('version', { crate, num: '0.6.1' }); + let crate = this.db.crate.create({ name: 'nanomsg' }); + this.db.version.create({ crate, num: '0.6.1' }); await visit('/crates/nanomsg/dependencies'); @@ -50,7 +52,7 @@ module('Acceptance | crate dependencies page', function (hooks) { }); test('shows an error page if crate fails to load', async function (assert) { - this.server.get('/api/v1/crates/:crate_name', {}, 500); + this.worker.use(http.get('/api/v1/crates/:crate_name', () => HttpResponse.json({}, { status: 500 }))); await visit('/crates/foo/1.0.0/dependencies'); assert.strictEqual(currentURL(), '/crates/foo/1.0.0/dependencies'); @@ -61,8 +63,8 @@ module('Acceptance | crate dependencies page', function (hooks) { }); test('shows an error page if version is not found', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '2.0.0' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '2.0.0' }); await visit('/crates/foo/1.0.0/dependencies'); assert.strictEqual(currentURL(), '/crates/foo/1.0.0/dependencies'); @@ -73,10 +75,10 @@ module('Acceptance | crate dependencies page', function (hooks) { }); test('shows an error page if versions fail to load', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '2.0.0' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '2.0.0' }); - this.server.get('/api/v1/crates/:crate_name/versions', {}, 500); + this.worker.use(http.get('/api/v1/crates/:crate_name/versions', () => HttpResponse.json({}, { status: 500 }))); await visit('/crates/foo/1.0.0/dependencies'); assert.strictEqual(currentURL(), '/crates/foo/1.0.0/dependencies'); @@ -87,10 +89,12 @@ module('Acceptance | crate dependencies page', function (hooks) { }); test('shows error message if loading of dependencies fails', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0' }); - this.server.get('/api/v1/crates/:crate_name/:version_num/dependencies', {}, 500); + this.worker.use( + http.get('/api/v1/crates/:crate_name/:version_num/dependencies', () => HttpResponse.json({}, { status: 500 })), + ); await visit('/crates/foo/1.0.0/dependencies'); assert.strictEqual(currentURL(), '/crates/foo/1.0.0/dependencies'); @@ -101,18 +105,18 @@ module('Acceptance | crate dependencies page', function (hooks) { }); test('hides description if loading of dependency details fails', async function (assert) { - let crate = this.server.create('crate', { name: 'nanomsg' }); - let version = this.server.create('version', { crate, num: '0.6.1' }); + let crate = this.db.crate.create({ name: 'nanomsg' }); + let version = this.db.version.create({ crate, num: '0.6.1' }); - let foo = this.server.create('crate', { name: 'foo', description: 'This is the foo crate' }); - this.server.create('version', { crate: foo, num: '1.0.0' }); - this.server.create('dependency', { crate: foo, version, req: '^1.0.0', kind: 'normal' }); + let foo = this.db.crate.create({ name: 'foo', description: 'This is the foo crate' }); + this.db.version.create({ crate: foo, num: '1.0.0' }); + this.db.dependency.create({ crate: foo, version, req: '^1.0.0', kind: 'normal' }); - let bar = this.server.create('crate', { name: 'bar', description: 'This is the bar crate' }); - this.server.create('version', { crate: bar, num: '2.3.4' }); - this.server.create('dependency', { crate: bar, version, req: '^2.0.0', kind: 'normal' }); + let bar = this.db.crate.create({ name: 'bar', description: 'This is the bar crate' }); + this.db.version.create({ crate: bar, num: '2.3.4' }); + this.db.dependency.create({ crate: bar, version, req: '^2.0.0', kind: 'normal' }); - this.server.get('/api/v1/crates', {}, 500); + this.worker.use(http.get('/api/v1/crates', () => HttpResponse.json({}, { status: 500 }))); await visit('/crates/nanomsg/dependencies'); assert.strictEqual(currentURL(), '/crates/nanomsg/0.6.1/dependencies'); From e5583423ed80563c471670c9edeea284a89fa50f Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 13:41:54 +0100 Subject: [PATCH 091/189] tests/acceptance/crate-following: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/crate-following-test.js | 30 +++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/tests/acceptance/crate-following-test.js b/tests/acceptance/crate-following-test.js index bc5c05cf00f..9145f062706 100644 --- a/tests/acceptance/crate-following-test.js +++ b/tests/acceptance/crate-following-test.js @@ -3,20 +3,22 @@ import { module, test } from 'qunit'; import { defer } from 'rsvp'; +import { http, HttpResponse } from 'msw'; + import { setupApplicationTest } from 'crates-io/tests/helpers'; module('Acceptance | Crate following', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); function prepare(context, { loggedIn = true, following = false } = {}) { - let server = context.server; + let { db } = context; - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '0.6.0' }); + let crate = db.crate.create({ name: 'nanomsg' }); + db.version.create({ crate, num: '0.6.0' }); if (loggedIn) { let followedCrates = following ? [crate] : []; - let user = server.create('user', { followedCrates }); + let user = db.user.create({ followedCrates }); context.authenticateAs(user); } } @@ -32,40 +34,40 @@ module('Acceptance | Crate following', function (hooks) { prepare(this); let followingDeferred = defer(); - this.server.get('/api/v1/crates/:crate_id/following', followingDeferred.promise); + this.worker.use(http.get('/api/v1/crates/:crate_id/following', () => followingDeferred.promise)); visit('/crates/nanomsg'); await waitFor('[data-test-follow-button] [data-test-spinner]'); assert.dom('[data-test-follow-button]').hasText('Loading…').isDisabled(); assert.dom('[data-test-follow-button] [data-test-spinner]').exists(); - followingDeferred.resolve({ following: false }); + followingDeferred.resolve(); await settled(); assert.dom('[data-test-follow-button]').hasText('Follow').isEnabled(); assert.dom('[data-test-follow-button] [data-test-spinner]').doesNotExist(); let followDeferred = defer(); - this.server.put('/api/v1/crates/:crate_id/follow', followDeferred.promise); + this.worker.use(http.put('/api/v1/crates/:crate_id/follow', () => followDeferred.promise)); click('[data-test-follow-button]'); await waitFor('[data-test-follow-button] [data-test-spinner]'); assert.dom('[data-test-follow-button]').hasText('Loading…').isDisabled(); assert.dom('[data-test-follow-button] [data-test-spinner]').exists(); - followDeferred.resolve({ ok: true }); + followDeferred.resolve(); await settled(); assert.dom('[data-test-follow-button]').hasText('Unfollow').isEnabled(); assert.dom('[data-test-follow-button] [data-test-spinner]').doesNotExist(); let unfollowDeferred = defer(); - this.server.delete('/api/v1/crates/:crate_id/follow', unfollowDeferred.promise); + this.worker.use(http.delete('/api/v1/crates/:crate_id/follow', () => unfollowDeferred.promise)); click('[data-test-follow-button]'); await waitFor('[data-test-follow-button] [data-test-spinner]'); assert.dom('[data-test-follow-button]').hasText('Loading…').isDisabled(); assert.dom('[data-test-follow-button] [data-test-spinner]').exists(); - unfollowDeferred.resolve({ ok: true }); + unfollowDeferred.resolve(); await settled(); assert.dom('[data-test-follow-button]').hasText('Follow').isEnabled(); assert.dom('[data-test-follow-button] [data-test-spinner]').doesNotExist(); @@ -74,7 +76,7 @@ module('Acceptance | Crate following', function (hooks) { test('error handling when loading following state fails', async function (assert) { prepare(this); - this.server.get('/api/v1/crates/:crate_id/following', {}, 500); + this.worker.use(http.get('/api/v1/crates/:crate_id/following', () => HttpResponse.json({}, { status: 500 }))); await visit('/crates/nanomsg'); assert.dom('[data-test-follow-button]').hasText('Follow').isDisabled(); @@ -88,7 +90,7 @@ module('Acceptance | Crate following', function (hooks) { test('error handling when follow fails', async function (assert) { prepare(this); - this.server.put('/api/v1/crates/:crate_id/follow', {}, 500); + this.worker.use(http.put('/api/v1/crates/:crate_id/follow', () => HttpResponse.json({}, { status: 500 }))); await visit('/crates/nanomsg'); await click('[data-test-follow-button]'); @@ -100,7 +102,7 @@ module('Acceptance | Crate following', function (hooks) { test('error handling when unfollow fails', async function (assert) { prepare(this, { following: true }); - this.server.del('/api/v1/crates/:crate_id/follow', {}, 500); + this.worker.use(http.delete('/api/v1/crates/:crate_id/follow', () => HttpResponse.json({}, { status: 500 }))); await visit('/crates/nanomsg'); await click('[data-test-follow-button]'); From 4515e1ad7814b1bb2d83d7adb42a154661c1287d Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 13:43:41 +0100 Subject: [PATCH 092/189] tests/acceptance/crate-navtabs: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/crate-navtabs-test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/acceptance/crate-navtabs-test.js b/tests/acceptance/crate-navtabs-test.js index 5c2a9892de5..cd68834dab4 100644 --- a/tests/acceptance/crate-navtabs-test.js +++ b/tests/acceptance/crate-navtabs-test.js @@ -10,11 +10,11 @@ const TAB_REV_DEPS = '[data-test-rev-deps-tab] a'; const TAB_SETTINGS = '[data-test-settings-tab] a'; module('Acceptance | crate navigation tabs', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('basic navigation between tabs works as expected', async function (assert) { - let crate = this.server.create('crate', { name: 'nanomsg' }); - this.server.create('version', { crate, num: '0.6.1' }); + let crate = this.db.crate.create({ name: 'nanomsg' }); + this.db.version.create({ crate, num: '0.6.1' }); await visit('/crates/nanomsg'); assert.strictEqual(currentURL(), '/crates/nanomsg'); From d6c7a06b5884e35fb9a028b521f7854a518756c1 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 14:46:03 +0100 Subject: [PATCH 093/189] tests/acceptance/crate: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/crate-test.js | 74 +++++++++++++++++----------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/tests/acceptance/crate-test.js b/tests/acceptance/crate-test.js index ab6f22d53fe..b7937482432 100644 --- a/tests/acceptance/crate-test.js +++ b/tests/acceptance/crate-test.js @@ -1,9 +1,11 @@ import { click, currentRouteName, currentURL, waitFor } from '@ember/test-helpers'; import { module, skip, test } from 'qunit'; +import { loadFixtures } from '@crates-io/msw/fixtures.js'; import percySnapshot from '@percy/ember'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { getPageTitle } from 'ember-page-title/test-support'; +import { http, HttpResponse } from 'msw'; import { setupApplicationTest } from 'crates-io/tests/helpers'; @@ -11,11 +13,11 @@ import axeConfig from '../axe-config'; import { visit } from '../helpers/visit-ignoring-abort'; module('Acceptance | crate page', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('visiting a crate page from the front page', async function (assert) { - let crate = this.server.create('crate', { name: 'nanomsg', newest_version: '0.6.1' }); - this.server.create('version', { crate, num: '0.6.1' }); + let crate = this.db.crate.create({ name: 'nanomsg', newest_version: '0.6.1' }); + this.db.version.create({ crate, num: '0.6.1' }); await visit('/'); await click('[data-test-just-updated] [data-test-crate-link="0"]'); @@ -28,9 +30,9 @@ module('Acceptance | crate page', function (hooks) { }); test('visiting /crates/nanomsg', async function (assert) { - let crate = this.server.create('crate', { name: 'nanomsg' }); - this.server.create('version', { crate, num: '0.6.0' }); - this.server.create('version', { crate, num: '0.6.1', rust_version: '1.69' }); + let crate = this.db.crate.create({ name: 'nanomsg' }); + this.db.version.create({ crate, num: '0.6.0' }); + this.db.version.create({ crate, num: '0.6.1', rust_version: '1.69' }); await visit('/crates/nanomsg'); @@ -47,9 +49,9 @@ module('Acceptance | crate page', function (hooks) { }); test('visiting /crates/nanomsg/', async function (assert) { - let crate = this.server.create('crate', { name: 'nanomsg' }); - this.server.create('version', { crate, num: '0.6.0' }); - this.server.create('version', { crate, num: '0.6.1' }); + let crate = this.db.crate.create({ name: 'nanomsg' }); + this.db.version.create({ crate, num: '0.6.0' }); + this.db.version.create({ crate, num: '0.6.1' }); await visit('/crates/nanomsg/'); @@ -63,9 +65,9 @@ module('Acceptance | crate page', function (hooks) { }); test('visiting /crates/nanomsg/0.6.0', async function (assert) { - let crate = this.server.create('crate', { name: 'nanomsg' }); - this.server.create('version', { crate, num: '0.6.0' }); - this.server.create('version', { crate, num: '0.6.1' }); + let crate = this.db.crate.create({ name: 'nanomsg' }); + this.db.version.create({ crate, num: '0.6.0' }); + this.db.version.create({ crate, num: '0.6.1' }); await visit('/crates/nanomsg/0.6.0'); @@ -91,7 +93,7 @@ module('Acceptance | crate page', function (hooks) { }); test('other crate loading error shows an error message', async function (assert) { - this.server.get('/api/v1/crates/:crate_name', {}, 500); + this.worker.use(http.get('/api/v1/crates/:crate_name', () => HttpResponse.json({}, { status: 500 }))); await visit('/crates/nanomsg'); assert.strictEqual(currentURL(), '/crates/nanomsg'); @@ -102,9 +104,9 @@ module('Acceptance | crate page', function (hooks) { }); test('unknown versions fall back to latest version and show an error message', async function (assert) { - let crate = this.server.create('crate', { name: 'nanomsg' }); - this.server.create('version', { crate, num: '0.6.0' }); - this.server.create('version', { crate, num: '0.6.1' }); + let crate = this.db.crate.create({ name: 'nanomsg' }); + this.db.version.create({ crate, num: '0.6.0' }); + this.db.version.create({ crate, num: '0.6.1' }); await visit('/crates/nanomsg/0.7.0'); @@ -116,11 +118,11 @@ module('Acceptance | crate page', function (hooks) { }); test('other versions loading error shows an error message', async function (assert) { - let crate = this.server.create('crate', { name: 'nanomsg' }); - this.server.create('version', { crate, num: '0.6.0' }); - this.server.create('version', { crate, num: '0.6.1' }); + let crate = this.db.crate.create({ name: 'nanomsg' }); + this.db.version.create({ crate, num: '0.6.0' }); + this.db.version.create({ crate, num: '0.6.1' }); - this.server.get('/api/v1/crates/:crate_name/versions', {}, 500); + this.worker.use(http.get('/api/v1/crates/:crate_name/versions', () => HttpResponse.json({}, { status: 500 }))); await visit('/'); await click('[data-test-just-updated] [data-test-crate-link="0"]'); @@ -132,8 +134,8 @@ module('Acceptance | crate page', function (hooks) { }); test('works for non-canonical names', async function (assert) { - let crate = this.server.create('crate', { name: 'foo-bar' }); - this.server.create('version', { crate }); + let crate = this.db.crate.create({ name: 'foo-bar' }); + this.db.version.create({ crate }); await visit('/crates/foo_bar'); @@ -145,7 +147,7 @@ module('Acceptance | crate page', function (hooks) { }); test('navigating to the all versions page', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/crates/nanomsg'); await click('[data-test-versions-tab] a'); @@ -154,7 +156,7 @@ module('Acceptance | crate page', function (hooks) { }); test('navigating to the reverse dependencies page', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/crates/nanomsg'); await click('[data-test-rev-deps-tab] a'); @@ -164,7 +166,7 @@ module('Acceptance | crate page', function (hooks) { }); test('navigating to a user page', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/crates/nanomsg'); await click('[data-test-owners] [data-test-owner-link="blabaere"]'); @@ -174,7 +176,7 @@ module('Acceptance | crate page', function (hooks) { }); test('navigating to a team page', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/crates/nanomsg'); await click('[data-test-owners] [data-test-owner-link="github:org:thehydroimpulse"]'); @@ -184,7 +186,7 @@ module('Acceptance | crate page', function (hooks) { }); test('crates having user-owners', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/crates/nanomsg'); @@ -196,7 +198,7 @@ module('Acceptance | crate page', function (hooks) { }); test('crates having team-owners', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/crates/nanomsg'); @@ -205,7 +207,7 @@ module('Acceptance | crate page', function (hooks) { }); test('crates license is supplied by version', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/crates/nanomsg'); assert.dom('[data-test-license]').hasText('Apache-2.0'); @@ -215,9 +217,9 @@ module('Acceptance | crate page', function (hooks) { }); skip('crates can be yanked by owner', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); - let user = this.server.schema.users.findBy({ login: 'thehydroimpulse' }); + let user = this.db.user.findFirst({ where: { login: { equals: 'thehydroimpulse' } } }); this.authenticateAs(user); await visit('/crates/nanomsg/0.5.0'); @@ -234,7 +236,7 @@ module('Acceptance | crate page', function (hooks) { }); test('navigating to the owners page when not logged in', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/crates/nanomsg'); @@ -242,9 +244,9 @@ module('Acceptance | crate page', function (hooks) { }); test('navigating to the owners page when not an owner', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); - let user = this.server.schema.users.findBy({ login: 'iain8' }); + let user = this.db.user.findFirst({ where: { login: { equals: 'iain8' } } }); this.authenticateAs(user); await visit('/crates/nanomsg'); @@ -253,9 +255,9 @@ module('Acceptance | crate page', function (hooks) { }); test('navigating to the settings page', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); - let user = this.server.schema.users.findBy({ login: 'thehydroimpulse' }); + let user = this.db.user.findFirst({ where: { login: { equals: 'thehydroimpulse' } } }); this.authenticateAs(user); await visit('/crates/nanomsg'); From 52afee6dcb20b81c4127a574fef76f4ecaeb3673 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 14:51:04 +0100 Subject: [PATCH 094/189] tests/acceptance/crates: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/crates-test.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/acceptance/crates-test.js b/tests/acceptance/crates-test.js index fb26477ab6c..ada72e6e100 100644 --- a/tests/acceptance/crates-test.js +++ b/tests/acceptance/crates-test.js @@ -1,6 +1,7 @@ import { click, currentURL, visit } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import { loadFixtures } from '@crates-io/msw/fixtures.js'; import percySnapshot from '@percy/ember'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { getPageTitle } from 'ember-page-title/test-support'; @@ -10,13 +11,13 @@ import { setupApplicationTest } from 'crates-io/tests/helpers'; import axeConfig from '../axe-config'; module('Acceptance | crates page', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); // should match the default set in the crates controller const per_page = 50; test('visiting the crates page from the front page', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/'); await click('[data-test-all-crates-link]'); @@ -29,7 +30,7 @@ module('Acceptance | crates page', function (hooks) { }); test('visiting the crates page directly', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/crates'); await click('[data-test-all-crates-link]'); @@ -40,8 +41,8 @@ module('Acceptance | crates page', function (hooks) { test('listing crates', async function (assert) { for (let i = 1; i <= per_page; i++) { - let crate = this.server.create('crate'); - this.server.create('version', { crate }); + let crate = this.db.crate.create(); + this.db.version.create({ crate }); } await visit('/crates'); @@ -52,8 +53,8 @@ module('Acceptance | crates page', function (hooks) { test('navigating to next page of crates', async function (assert) { for (let i = 1; i <= per_page + 2; i++) { - let crate = this.server.create('crate'); - this.server.create('version', { crate }); + let crate = this.db.crate.create(); + this.db.version.create({ crate }); } const page_start = per_page + 1; const total = per_page + 2; @@ -67,7 +68,7 @@ module('Acceptance | crates page', function (hooks) { }); test('crates default sort is by recent downloads', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/crates'); @@ -75,7 +76,7 @@ module('Acceptance | crates page', function (hooks) { }); test('downloads appears for each crate on crate list', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/crates'); @@ -84,7 +85,7 @@ module('Acceptance | crates page', function (hooks) { }); test('recent downloads appears for each crate on crate list', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/crates'); From bdd6b7add6999376b2e585689cd839bbba99c9c4 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 14:57:19 +0100 Subject: [PATCH 095/189] tests/acceptance/dashboard: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/dashboard-test.js | 49 ++++++++++++++++-------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/tests/acceptance/dashboard-test.js b/tests/acceptance/dashboard-test.js index a4b88fbcfee..0bda0593957 100644 --- a/tests/acceptance/dashboard-test.js +++ b/tests/acceptance/dashboard-test.js @@ -2,13 +2,14 @@ import { currentURL } from '@ember/test-helpers'; import { module, test } from 'qunit'; import percySnapshot from '@percy/ember'; +import { http, HttpResponse } from 'msw'; import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Acceptance | Dashboard', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('shows "page requires authentication" error when not logged in', async function (assert) { await visit('/dashboard'); @@ -18,7 +19,7 @@ module('Acceptance | Dashboard', function (hooks) { }); test('shows the dashboard when logged in', async function (assert) { - let user = this.server.create('user', { + let user = this.db.user.create({ login: 'johnnydee', name: 'John Doe', email: 'john@doe.com', @@ -28,31 +29,35 @@ module('Acceptance | Dashboard', function (hooks) { this.authenticateAs(user); { - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { crate, num: '0.5.0' }); - this.server.create('version', { crate, num: '0.6.0' }); - this.server.create('version', { crate, num: '0.7.0' }); - this.server.create('version', { crate, num: '0.7.1' }); - this.server.create('version', { crate, num: '0.7.2' }); - this.server.create('version', { crate, num: '0.7.3' }); - this.server.create('version', { crate, num: '0.8.0' }); - this.server.create('version', { crate, num: '0.8.1' }); - this.server.create('version', { crate, num: '0.9.0' }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.1.0' }); - user.followedCrates.add(crate); + let crate = this.db.crate.create({ name: 'rand' }); + this.db.version.create({ crate, num: '0.5.0' }); + this.db.version.create({ crate, num: '0.6.0' }); + this.db.version.create({ crate, num: '0.7.0' }); + this.db.version.create({ crate, num: '0.7.1' }); + this.db.version.create({ crate, num: '0.7.2' }); + this.db.version.create({ crate, num: '0.7.3' }); + this.db.version.create({ crate, num: '0.8.0' }); + this.db.version.create({ crate, num: '0.8.1' }); + this.db.version.create({ crate, num: '0.9.0' }); + this.db.version.create({ crate, num: '1.0.0' }); + this.db.version.create({ crate, num: '1.1.0' }); + user = this.db.user.update({ + where: { id: { equals: user.id } }, + data: { followedCrates: [...user.followedCrates, crate] }, + }); } { - let crate = this.server.create('crate', { name: 'nanomsg' }); - this.server.create('crate-ownership', { crate, user }); - this.server.create('version', { crate, num: '0.1.0' }); - user.followedCrates.add(crate); + let crate = this.db.crate.create({ name: 'nanomsg' }); + this.db.crateOwnership.create({ crate, user }); + this.db.version.create({ crate, num: '0.1.0' }); + user = this.db.user.update({ + where: { id: { equals: user.id } }, + data: { followedCrates: [...user.followedCrates, crate] }, + }); } - user.save(); - - this.server.get(`/api/v1/users/${user.id}/stats`, { total_downloads: 3892 }); + this.worker.use(http.get(`/api/v1/users/${user.id}/stats`, () => HttpResponse.json({ total_downloads: 3892 }))); await visit('/dashboard'); assert.strictEqual(currentURL(), '/dashboard'); From b7e8b4ea38befcfbdbc08242a0002d9db7530719 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 14:59:48 +0100 Subject: [PATCH 096/189] tests/acceptance/dev-mode: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/dev-mode-test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/acceptance/dev-mode-test.js b/tests/acceptance/dev-mode-test.js index 253ca7dd7f9..3072244247a 100644 --- a/tests/acceptance/dev-mode-test.js +++ b/tests/acceptance/dev-mode-test.js @@ -11,18 +11,18 @@ if (s.has('devmode')) { * @link http://localhost:4200/tests/?notrycatch&devmode&filter=Development%20Mode */ module('Development Mode', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('authenticated', async function () { - let user = this.server.create('user'); + let user = this.db.user.create(); this.authenticateAs(user); - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '0.1.0' }); - this.server.create('crate-ownership', { crate, user }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '0.1.0' }); + this.db.crateOwnership.create({ crate, user }); - crate = this.server.create('crate', { name: 'bar' }); - this.server.create('version', { crate, num: '1.0.0' }); + crate = this.db.crate.create({ name: 'bar' }); + this.db.version.create({ crate, num: '1.0.0' }); let router = this.owner.lookup('service:router'); router.on('routeDidChange', () => { From 7ad14f99c417611e2e0656a7b0289ea46613bd97 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:01:56 +0100 Subject: [PATCH 097/189] tests/acceptance/email-change: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/email-change-test.js | 28 ++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/acceptance/email-change-test.js b/tests/acceptance/email-change-test.js index ee4bda20bee..40d39b255ac 100644 --- a/tests/acceptance/email-change-test.js +++ b/tests/acceptance/email-change-test.js @@ -1,15 +1,17 @@ import { click, currentURL, fillIn } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import { http, HttpResponse } from 'msw'; + import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Acceptance | Email Change', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('happy path', async function (assert) { - let user = this.server.create('user', { email: 'old@email.com' }); + let user = this.db.user.create({ email: 'old@email.com' }); this.authenticateAs(user); @@ -43,14 +45,14 @@ module('Acceptance | Email Change', function (hooks) { assert.dom('[data-test-email-input] [data-test-verification-sent]').exists(); assert.dom('[data-test-email-input] [data-test-resend-button]').isEnabled(); - user.reload(); + user = this.db.user.findFirst({ where: { id: { equals: user.id } } }); assert.strictEqual(user.email, 'new@email.com'); assert.false(user.emailVerified); assert.ok(user.emailVerificationToken); }); test('happy path with `email: null`', async function (assert) { - let user = this.server.create('user', { email: undefined }); + let user = this.db.user.create({ email: undefined }); this.authenticateAs(user); @@ -80,14 +82,14 @@ module('Acceptance | Email Change', function (hooks) { assert.dom('[data-test-email-input] [data-test-verification-sent]').exists(); assert.dom('[data-test-email-input] [data-test-resend-button]').isEnabled(); - user.reload(); + user = this.db.user.findFirst({ where: { id: { equals: user.id } } }); assert.strictEqual(user.email, 'new@email.com'); assert.false(user.emailVerified); assert.ok(user.emailVerificationToken); }); test('cancel button', async function (assert) { - let user = this.server.create('user', { email: 'old@email.com' }); + let user = this.db.user.create({ email: 'old@email.com' }); this.authenticateAs(user); @@ -102,18 +104,18 @@ module('Acceptance | Email Change', function (hooks) { assert.dom('[data-test-email-input] [data-test-not-verified]').doesNotExist(); assert.dom('[data-test-email-input] [data-test-verification-sent]').doesNotExist(); - user.reload(); + user = this.db.user.findFirst({ where: { id: { equals: user.id } } }); assert.strictEqual(user.email, 'old@email.com'); assert.true(user.emailVerified); assert.notOk(user.emailVerificationToken); }); test('server error', async function (assert) { - let user = this.server.create('user', { email: 'old@email.com' }); + let user = this.db.user.create({ email: 'old@email.com' }); this.authenticateAs(user); - this.server.put('/api/v1/users/:user_id', {}, 500); + this.worker.use(http.put('/api/v1/users/:user_id', () => HttpResponse.json({}, { status: 500 }))); await visit('/settings/profile'); await click('[data-test-email-input] [data-test-edit-button]'); @@ -126,7 +128,7 @@ module('Acceptance | Email Change', function (hooks) { .dom('[data-test-notification-message="error"]') .hasText('Error in saving email: An unknown error occurred while saving this email.'); - user.reload(); + user = this.db.user.findFirst({ where: { id: { equals: user.id } } }); assert.strictEqual(user.email, 'old@email.com'); assert.true(user.emailVerified); assert.notOk(user.emailVerificationToken); @@ -134,7 +136,7 @@ module('Acceptance | Email Change', function (hooks) { module('Resend button', function () { test('happy path', async function (assert) { - let user = this.server.create('user', { email: 'john@doe.com', emailVerificationToken: 'secret123' }); + let user = this.db.user.create({ email: 'john@doe.com', emailVerificationToken: 'secret123' }); this.authenticateAs(user); @@ -152,11 +154,11 @@ module('Acceptance | Email Change', function (hooks) { }); test('server error', async function (assert) { - let user = this.server.create('user', { email: 'john@doe.com', emailVerificationToken: 'secret123' }); + let user = this.db.user.create({ email: 'john@doe.com', emailVerificationToken: 'secret123' }); this.authenticateAs(user); - this.server.put('/api/v1/users/:user_id/resend', {}, 500); + this.worker.use(http.put('/api/v1/users/:user_id/resend', () => HttpResponse.json({}, { status: 500 }))); await visit('/settings/profile'); assert.strictEqual(currentURL(), '/settings/profile'); From 5d55d78dc901f9a4fa74a3e280610001f428ad82 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:03:05 +0100 Subject: [PATCH 098/189] tests/acceptance/email-confirmation: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/email-confirmation-test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/acceptance/email-confirmation-test.js b/tests/acceptance/email-confirmation-test.js index 43bf7833e91..ca3bd25851f 100644 --- a/tests/acceptance/email-confirmation-test.js +++ b/tests/acceptance/email-confirmation-test.js @@ -6,22 +6,22 @@ import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Acceptance | Email Confirmation', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('unauthenticated happy path', async function (assert) { - let user = this.server.create('user', { emailVerificationToken: 'badc0ffee' }); + let user = this.db.user.create({ emailVerificationToken: 'badc0ffee' }); assert.false(user.emailVerified); await visit('/confirm/badc0ffee'); assert.strictEqual(currentURL(), '/'); assert.dom('[data-test-notification-message="success"]').exists(); - user.reload(); + user = this.db.user.findFirst({ where: { id: { equals: user.id } } }); assert.true(user.emailVerified); }); test('authenticated happy path', async function (assert) { - let user = this.server.create('user', { emailVerificationToken: 'badc0ffee' }); + let user = this.db.user.create({ emailVerificationToken: 'badc0ffee' }); assert.false(user.emailVerified); this.authenticateAs(user); @@ -33,7 +33,7 @@ module('Acceptance | Email Confirmation', function (hooks) { let { currentUser } = this.owner.lookup('service:session'); assert.true(currentUser.email_verified); - user.reload(); + user = this.db.user.findFirst({ where: { id: { equals: user.id } } }); assert.true(user.emailVerified); }); From 489577da7db80d28c57e9b60238e45af79b9dc08 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:09:55 +0100 Subject: [PATCH 099/189] tests/acceptance/invites: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/invites-test.js | 106 +++++++++++++++++++++++-------- 1 file changed, 80 insertions(+), 26 deletions(-) diff --git a/tests/acceptance/invites-test.js b/tests/acceptance/invites-test.js index f29b508af15..f85c0cbef34 100644 --- a/tests/acceptance/invites-test.js +++ b/tests/acceptance/invites-test.js @@ -2,33 +2,33 @@ import { click, currentURL } from '@ember/test-helpers'; import { module, test } from 'qunit'; import percySnapshot from '@percy/ember'; -import { Response } from 'miragejs'; +import { http, HttpResponse } from 'msw'; import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Acceptance | /me/pending-invites', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); function prepare(context) { - let inviter = context.server.create('user', { name: 'janed' }); - let inviter2 = context.server.create('user', { name: 'wycats' }); + let inviter = context.db.user.create({ name: 'janed' }); + let inviter2 = context.db.user.create({ name: 'wycats' }); - let user = context.server.create('user'); + let user = context.db.user.create(); - let nanomsg = context.server.create('crate', { name: 'nanomsg' }); - context.server.create('version', { crate: nanomsg }); - context.server.create('crate-owner-invitation', { + let nanomsg = context.db.crate.create({ name: 'nanomsg' }); + context.db.version.create({ crate: nanomsg }); + context.db.crateOwnerInvitation.create({ crate: nanomsg, createdAt: '2016-12-24T12:34:56Z', invitee: user, inviter, }); - let ember = context.server.create('crate', { name: 'ember-rs' }); - context.server.create('version', { crate: ember }); - context.server.create('crate-owner-invitation', { + let ember = context.db.crate.create({ name: 'ember-rs' }); + context.db.version.create({ crate: ember }); + context.db.crateOwnerInvitation.create({ crate: ember, createdAt: '2020-12-31T12:34:56Z', invitee: user, @@ -73,7 +73,7 @@ module('Acceptance | /me/pending-invites', function (hooks) { test('shows empty list message', async function (assert) { prepare(this); - this.server.schema.crateOwnerInvitations.all().destroy(); + this.db.crateOwnerInvitation.deleteMany({}); await visit('/me/pending-invites'); assert.strictEqual(currentURL(), '/me/pending-invites'); @@ -84,9 +84,22 @@ module('Acceptance | /me/pending-invites', function (hooks) { test('invites can be declined', async function (assert) { let { nanomsg, user } = prepare(this); - let { crateOwnerInvitations, crateOwnerships } = this.server.schema; - assert.strictEqual(crateOwnerInvitations.where({ crateId: nanomsg.id, inviteeId: user.id }).length, 1); - assert.strictEqual(crateOwnerships.where({ crateId: nanomsg.id, userId: user.id }).length, 0); + let { crateOwnerInvitation, crateOwnership } = this.db; + let invites = crateOwnerInvitation.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + invitee: { id: { equals: user.id } }, + }, + }); + assert.strictEqual(invites.length, 1); + + let owners = crateOwnership.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + user: { id: { equals: user.id } }, + }, + }); + assert.strictEqual(owners.length, 0); await visit('/me/pending-invites'); assert.strictEqual(currentURL(), '/me/pending-invites'); @@ -100,14 +113,28 @@ module('Acceptance | /me/pending-invites', function (hooks) { assert.dom('[data-test-invite="nanomsg"] [data-test-crate-link]').doesNotExist(); assert.dom('[data-test-invite="nanomsg"] [data-test-inviter-link]').doesNotExist(); - assert.strictEqual(crateOwnerInvitations.where({ crateId: nanomsg.id, inviteeId: user.id }).length, 0); - assert.strictEqual(crateOwnerships.where({ crateId: nanomsg.id, userId: user.id }).length, 0); + invites = crateOwnerInvitation.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + invitee: { id: { equals: user.id } }, + }, + }); + assert.strictEqual(invites.length, 0); + + owners = crateOwnership.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + user: { id: { equals: user.id } }, + }, + }); + assert.strictEqual(owners.length, 0); }); test('error message is shown if decline request fails', async function (assert) { prepare(this); - this.server.put('/api/v1/me/crate_owner_invitations/:crate_id', () => new Response(500)); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.put('/api/v1/me/crate_owner_invitations/:crate_id', () => error)); await visit('/me/pending-invites'); assert.strictEqual(currentURL(), '/me/pending-invites'); @@ -121,9 +148,22 @@ module('Acceptance | /me/pending-invites', function (hooks) { test('invites can be accepted', async function (assert) { let { nanomsg, user } = prepare(this); - let { crateOwnerInvitations, crateOwnerships } = this.server.schema; - assert.strictEqual(crateOwnerInvitations.where({ crateId: nanomsg.id, inviteeId: user.id }).length, 1); - assert.strictEqual(crateOwnerships.where({ crateId: nanomsg.id, userId: user.id }).length, 0); + let { crateOwnerInvitation, crateOwnership } = this.db; + let invites = crateOwnerInvitation.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + invitee: { id: { equals: user.id } }, + }, + }); + assert.strictEqual(invites.length, 1); + + let owners = crateOwnership.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + user: { id: { equals: user.id } }, + }, + }); + assert.strictEqual(owners.length, 0); await visit('/me/pending-invites'); assert.strictEqual(currentURL(), '/me/pending-invites'); @@ -139,14 +179,28 @@ module('Acceptance | /me/pending-invites', function (hooks) { await percySnapshot(assert); - assert.strictEqual(crateOwnerInvitations.where({ crateId: nanomsg.id, inviteeId: user.id }).length, 0); - assert.strictEqual(crateOwnerships.where({ crateId: nanomsg.id, userId: user.id }).length, 1); + invites = crateOwnerInvitation.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + invitee: { id: { equals: user.id } }, + }, + }); + assert.strictEqual(invites.length, 0); + + owners = crateOwnership.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + user: { id: { equals: user.id } }, + }, + }); + assert.strictEqual(owners.length, 1); }); test('error message is shown if accept request fails', async function (assert) { prepare(this); - this.server.put('/api/v1/me/crate_owner_invitations/:crate_id', () => new Response(500)); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.put('/api/v1/me/crate_owner_invitations/:crate_id', () => error)); await visit('/me/pending-invites'); assert.strictEqual(currentURL(), '/me/pending-invites'); @@ -162,8 +216,8 @@ module('Acceptance | /me/pending-invites', function (hooks) { let errorMessage = 'The invitation to become an owner of the demo_crate crate expired. Please reach out to an owner of the crate to request a new invitation.'; - let payload = { errors: [{ detail: errorMessage }] }; - this.server.put('/api/v1/me/crate_owner_invitations/:crate_id', payload, 410); + let error = HttpResponse.json({ errors: [{ detail: errorMessage }] }, { status: 410 }); + this.worker.use(http.put('/api/v1/me/crate_owner_invitations/:crate_id', () => error)); await visit('/me/pending-invites'); assert.strictEqual(currentURL(), '/me/pending-invites'); From 5ac2ac70b89d3544a3671bd9d39a3e7f78123e15 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:10:29 +0100 Subject: [PATCH 100/189] tests/acceptance/keyword: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/keyword-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/acceptance/keyword-test.js b/tests/acceptance/keyword-test.js index caf0d206de9..b5cd7246aa2 100644 --- a/tests/acceptance/keyword-test.js +++ b/tests/acceptance/keyword-test.js @@ -9,10 +9,10 @@ import { setupApplicationTest } from 'crates-io/tests/helpers'; import axeConfig from '../axe-config'; module('Acceptance | keywords', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('keyword/:keyword_id index default sort is recent-downloads', async function (assert) { - this.server.create('keyword', { keyword: 'network' }); + this.db.keyword.create({ keyword: 'network' }); await visit('/keywords/network'); From 2e5d70465fba04921f76f7c9fba067d5cef87777 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:15:44 +0100 Subject: [PATCH 101/189] tests/acceptance/login: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/login-test.js | 64 +++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/tests/acceptance/login-test.js b/tests/acceptance/login-test.js index 5d793098bca..7d000efe358 100644 --- a/tests/acceptance/login-test.js +++ b/tests/acceptance/login-test.js @@ -5,14 +5,16 @@ import { defer } from 'rsvp'; import window from 'ember-window-mock'; import { setupWindowMock } from 'ember-window-mock/test-support'; +import { http, HttpResponse } from 'msw'; import { setupApplicationTest } from 'crates-io/tests/helpers'; module('Acceptance | Login', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); setupWindowMock(hooks); test('successful login', async function (assert) { + let { db } = this; let deferred = defer(); window.open = (url, target, features) => { @@ -26,30 +28,32 @@ module('Acceptance | Login', function (hooks) { return { document: { write() {}, close() {} }, close() {} }; }; - this.server.get('/api/private/session/begin', { url: 'url-to-github-including-state-secret' }); - - this.server.get('/api/private/session/authorize', (schema, request) => { - assert.deepEqual(request.queryParams, { - code: '901dd10e07c7e9fa1cd5', - state: 'fYcUY3FMdUUz00FC7vLT7A', - }); - - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - return { ok: true }; - }); - - this.server.get('/api/v1/me', () => ({ - user: { - id: 42, - login: 'johnnydee', - name: 'John Doe', - email: 'john@doe.name', - avatar: 'https://avatars2.githubusercontent.com/u/12345?v=4', - url: 'https://github.com/johnnydee', - }, - owned_crates: [], - })); + this.worker.use( + http.get('/api/private/session/begin', () => HttpResponse.json({ url: 'url-to-github-including-state-secret' })), + http.get('/api/private/session/authorize', ({ request }) => { + let url = new URL(request.url); + assert.deepEqual([...url.searchParams.keys()], ['code', 'state']); + assert.strictEqual(url.searchParams.get('code'), '901dd10e07c7e9fa1cd5'); + assert.strictEqual(url.searchParams.get('state'), 'fYcUY3FMdUUz00FC7vLT7A'); + + let user = db.user.create(); + db.mswSession.create({ user }); + return HttpResponse.json({ ok: true }); + }), + http.get('/api/v1/me', () => + HttpResponse.json({ + user: { + id: 42, + login: 'johnnydee', + name: 'John Doe', + email: 'john@doe.name', + avatar: 'https://avatars2.githubusercontent.com/u/12345?v=4', + url: 'https://github.com/johnnydee', + }, + owned_crates: [], + }), + ), + ); await visit('/'); assert.strictEqual(currentURL(), '/'); @@ -78,10 +82,12 @@ module('Acceptance | Login', function (hooks) { return { document: { write() {}, close() {} }, close() {} }; }; - this.server.get('/api/private/session/begin', { url: 'url-to-github-including-state-secret' }); - - let payload = { errors: [{ detail: 'Forbidden' }] }; - this.server.get('/api/private/session/authorize', payload, 403); + this.worker.use( + http.get('/api/private/session/begin', () => HttpResponse.json({ url: 'url-to-github-including-state-secret' })), + http.get('/api/private/session/authorize', () => + HttpResponse.json({ errors: [{ detail: 'Forbidden' }] }, { status: 403 }), + ), + ); await visit('/'); assert.strictEqual(currentURL(), '/'); From f4e1feca52e41e13687735df447561ceaa91af7a Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:16:17 +0100 Subject: [PATCH 102/189] tests/acceptance/logout: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/logout-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/acceptance/logout-test.js b/tests/acceptance/logout-test.js index 1c0957fdbd4..b40a8dbb64a 100644 --- a/tests/acceptance/logout-test.js +++ b/tests/acceptance/logout-test.js @@ -7,11 +7,11 @@ import { setupWindowMock } from 'ember-window-mock/test-support'; import { setupApplicationTest } from 'crates-io/tests/helpers'; module('Acceptance | Logout', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); setupWindowMock(hooks); test('successful logout', async function (assert) { - let user = this.server.create('user', { name: 'John Doe' }); + let user = this.db.user.create({ name: 'John Doe' }); this.authenticateAs(user); await visit('/crates'); From d613e247327952caf2bac244760d624de33e700e Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:20:52 +0100 Subject: [PATCH 103/189] tests/acceptance/publish-notifications: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/publish-notifications-test.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/acceptance/publish-notifications-test.js b/tests/acceptance/publish-notifications-test.js index 513e7420ce9..129680b5588 100644 --- a/tests/acceptance/publish-notifications-test.js +++ b/tests/acceptance/publish-notifications-test.js @@ -3,15 +3,15 @@ import { module, test } from 'qunit'; import { defer } from 'rsvp'; -import { Response } from 'miragejs'; +import { http, HttpResponse } from 'msw'; import { setupApplicationTest } from 'crates-io/tests/helpers'; module('Acceptance | publish notifications', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('unsubscribe and resubscribe', async function (assert) { - let user = this.server.create('user'); + let user = this.db.user.create(); this.authenticateAs(user); assert.true(user.publishNotifications); @@ -24,20 +24,22 @@ module('Acceptance | publish notifications', function (hooks) { assert.dom('[data-test-notifications] input[type=checkbox]').isNotChecked(); await click('[data-test-notifications] button'); - assert.false(user.reload().publishNotifications); + user = this.db.user.findFirst({ where: { id: { equals: user.id } } }); + assert.false(user.publishNotifications); await click('[data-test-notifications] input[type=checkbox]'); assert.dom('[data-test-notifications] input[type=checkbox]').isChecked(); await click('[data-test-notifications] button'); - assert.true(user.reload().publishNotifications); + user = this.db.user.findFirst({ where: { id: { equals: user.id } } }); + assert.true(user.publishNotifications); }); test('loading and error state', async function (assert) { - let user = this.server.create('user'); + let user = this.db.user.create(); let deferred = defer(); - this.server.put('/api/v1/users/:user_id', deferred.promise); + this.worker.use(http.put('/api/v1/users/:user_id', () => deferred.promise)); this.authenticateAs(user); assert.true(user.publishNotifications); @@ -52,7 +54,7 @@ module('Acceptance | publish notifications', function (hooks) { assert.dom('[data-test-notifications] input[type=checkbox]').isDisabled(); assert.dom('[data-test-notifications] button').isDisabled(); - deferred.resolve(new Response(500)); + deferred.resolve(HttpResponse.json({}, { status: 500 })); await clickPromise; assert From ad2f7b3c637f93d2d06e0578e0668f84816dcc56 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:24:41 +0100 Subject: [PATCH 104/189] tests/acceptance/read-only-mode: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/read-only-mode-test.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/acceptance/read-only-mode-test.js b/tests/acceptance/read-only-mode-test.js index 72cede4b163..25a97861e58 100644 --- a/tests/acceptance/read-only-mode-test.js +++ b/tests/acceptance/read-only-mode-test.js @@ -1,12 +1,14 @@ import { visit } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import { http, HttpResponse } from 'msw'; + import { setupApplicationTest } from 'crates-io/tests/helpers'; import { AjaxError } from '../../utils/ajax'; module('Acceptance | Read-only Mode', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('notification is not shown for read-write mode', async function (assert) { await visit('/'); @@ -14,14 +16,14 @@ module('Acceptance | Read-only Mode', function (hooks) { }); test('notification is shown for read-only mode', async function (assert) { - this.server.get('/api/v1/site_metadata', { read_only: true }); + this.worker.use(http.get('/api/v1/site_metadata', () => HttpResponse.json({ read_only: true }))); await visit('/'); assert.dom('[data-test-notification-message="info"]').includesText('read-only mode'); }); test('server errors are handled gracefully', async function (assert) { - this.server.get('/api/v1/site_metadata', {}, 500); + this.worker.use(http.get('/api/v1/site_metadata', () => HttpResponse.json({}, { status: 500 }))); await visit('/'); assert.dom('[data-test-notification-message="info"]').doesNotExist(); @@ -29,7 +31,7 @@ module('Acceptance | Read-only Mode', function (hooks) { }); test('client errors are reported on sentry', async function (assert) { - this.server.get('/api/v1/site_metadata', {}, 404); + this.worker.use(http.get('/api/v1/site_metadata', () => HttpResponse.json({}, { status: 404 }))); await visit('/'); assert.dom('[data-test-notification-message="info"]').doesNotExist(); From cbbf385cb4974d8fbd9bde0d3d688b77ef32b3cd Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:28:11 +0100 Subject: [PATCH 105/189] tests/acceptance/reverse-dependencies: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/reverse-dependencies-test.js | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/tests/acceptance/reverse-dependencies-test.js b/tests/acceptance/reverse-dependencies-test.js index 093e9460843..e6c746d0a76 100644 --- a/tests/acceptance/reverse-dependencies-test.js +++ b/tests/acceptance/reverse-dependencies-test.js @@ -1,25 +1,27 @@ import { click, currentURL } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import { http, HttpResponse } from 'msw'; + import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Acceptance | /crates/:crate_id/reverse_dependencies', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); - function prepare({ server }) { - let foo = server.create('crate', { name: 'foo' }); - server.create('version', { crate: foo }); + function prepare({ db }) { + let foo = db.crate.create({ name: 'foo' }); + db.version.create({ crate: foo }); - let bar = server.create('crate', { name: 'bar' }); - server.create('version', { crate: bar }); + let bar = db.crate.create({ name: 'bar' }); + let barVersion = db.version.create({ crate: bar }); - let baz = server.create('crate', { name: 'baz' }); - server.create('version', { crate: baz }); + let baz = db.crate.create({ name: 'baz' }); + let bazVersion = db.version.create({ crate: baz }); - server.create('dependency', { crate: foo, version: bar.versions.models[0] }); - server.create('dependency', { crate: foo, version: baz.versions.models[0] }); + db.dependency.create({ crate: foo, version: barVersion }); + db.dependency.create({ crate: foo, version: bazVersion }); return { foo, bar, baz }; } @@ -30,19 +32,19 @@ module('Acceptance | /crates/:crate_id/reverse_dependencies', function (hooks) { await visit(`/crates/${foo.name}/reverse_dependencies`); assert.strictEqual(currentURL(), `/crates/${foo.name}/reverse_dependencies`); assert.dom('[data-test-row]').exists({ count: 2 }); - assert.dom('[data-test-row="0"] [data-test-crate-name]').hasText(bar.name); - assert.dom('[data-test-row="0"] [data-test-description]').hasText(bar.description); - assert.dom('[data-test-row="1"] [data-test-crate-name]').hasText(baz.name); - assert.dom('[data-test-row="1"] [data-test-description]').hasText(baz.description); + assert.dom('[data-test-row="0"] [data-test-crate-name]').hasText(baz.name); + assert.dom('[data-test-row="0"] [data-test-description]').hasText(baz.description); + assert.dom('[data-test-row="1"] [data-test-crate-name]').hasText(bar.name); + assert.dom('[data-test-row="1"] [data-test-description]').hasText(bar.description); }); test('supports pagination', async function (assert) { let { foo } = prepare(this); for (let i = 0; i < 20; i++) { - let crate = this.server.create('crate'); - let version = this.server.create('version', { crate }); - this.server.create('dependency', { crate: foo, version }); + let crate = this.db.crate.create(); + let version = this.db.version.create({ crate }); + this.db.dependency.create({ crate: foo, version }); } await visit(`/crates/${foo.name}/reverse_dependencies`); @@ -67,7 +69,8 @@ module('Acceptance | /crates/:crate_id/reverse_dependencies', function (hooks) { test('shows a generic error if the server is broken', async function (assert) { let { foo } = prepare(this); - this.server.get('/api/v1/crates/:crate_id/reverse_dependencies', {}, 500); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.get('/api/v1/crates/:crate_id/reverse_dependencies', () => error)); await visit(`/crates/${foo.name}/reverse_dependencies`); assert.strictEqual(currentURL(), '/'); @@ -79,8 +82,8 @@ module('Acceptance | /crates/:crate_id/reverse_dependencies', function (hooks) { test('shows a detailed error if available', async function (assert) { let { foo } = prepare(this); - let payload = { errors: [{ detail: 'cannot request more than 100 items' }] }; - this.server.get('/api/v1/crates/:crate_id/reverse_dependencies', payload, 400); + let error = HttpResponse.json({ errors: [{ detail: 'cannot request more than 100 items' }] }, { status: 400 }); + this.worker.use(http.get('/api/v1/crates/:crate_id/reverse_dependencies', () => error)); await visit(`/crates/${foo.name}/reverse_dependencies`); assert.strictEqual(currentURL(), '/'); From df2da968548483734a59c2b09020d6380152891c Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:34:37 +0100 Subject: [PATCH 106/189] tests/acceptance/search: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/search-test.js | 130 +++++++++++++++++--------------- 1 file changed, 69 insertions(+), 61 deletions(-) diff --git a/tests/acceptance/search-test.js b/tests/acceptance/search-test.js index 9839d633136..d1ed6bf7389 100644 --- a/tests/acceptance/search-test.js +++ b/tests/acceptance/search-test.js @@ -3,21 +3,22 @@ import { module, test } from 'qunit'; import { defer } from 'rsvp'; +import { loadFixtures } from '@crates-io/msw/fixtures.js'; import percySnapshot from '@percy/ember'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { keyDown } from 'ember-keyboard/test-support/test-helpers'; import { getPageTitle } from 'ember-page-title/test-support'; +import { http, HttpResponse } from 'msw'; import { setupApplicationTest } from 'crates-io/tests/helpers'; -import { list as listCrates } from '../../mirage/route-handlers/crates'; import axeConfig from '../axe-config'; module('Acceptance | search', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('searching for "rust"', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/'); await fillIn('[data-test-search-input]', 'rust'); @@ -45,7 +46,7 @@ module('Acceptance | search', function (hooks) { }); test('searching for "rust" from query', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/search?q=rust'); @@ -58,7 +59,7 @@ module('Acceptance | search', function (hooks) { }); test('clearing search results', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/search?q=rust'); @@ -72,7 +73,7 @@ module('Acceptance | search', function (hooks) { }); test('pressing S key to focus the search bar', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/'); @@ -98,7 +99,7 @@ module('Acceptance | search', function (hooks) { }); test('check search results are by default displayed by relevance', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/'); await fillIn('[data-test-search-input]', 'rust'); @@ -108,10 +109,11 @@ module('Acceptance | search', function (hooks) { }); test('error handling when searching from the frontpage', async function (assert) { - let crate = this.server.create('crate', { name: 'rust' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'rust' }); + this.db.version.create({ crate, num: '1.0.0' }); - this.server.get('/api/v1/crates', {}, 500); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.get('/api/v1/crates', () => error)); await visit('/'); await fillIn('[data-test-search-input]', 'rust'); @@ -121,10 +123,8 @@ module('Acceptance | search', function (hooks) { assert.dom('[data-test-try-again-button]').isEnabled(); let deferred = defer(); - this.server.get('/api/v1/crates', async function (schema, request) { - await deferred.promise; - return listCrates.call(this, schema, request); - }); + this.worker.resetHandlers(); + this.worker.use(http.get('/api/v1/crates', () => deferred.promise)); click('[data-test-try-again-button]'); await waitFor('[data-test-page-header] [data-test-spinner]'); @@ -140,15 +140,16 @@ module('Acceptance | search', function (hooks) { }); test('error handling when searching from the search page', async function (assert) { - let crate = this.server.create('crate', { name: 'rust' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'rust' }); + this.db.version.create({ crate, num: '1.0.0' }); await visit('/search?q=rust'); assert.dom('[data-test-crate-row]').exists({ count: 1 }); assert.dom('[data-test-error-message]').doesNotExist(); assert.dom('[data-test-try-again-button]').doesNotExist(); - this.server.get('/api/v1/crates', {}, 500); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.get('/api/v1/crates', () => error)); await fillIn('[data-test-search-input]', 'ru'); await triggerEvent('[data-test-search-form]', 'submit'); @@ -157,10 +158,8 @@ module('Acceptance | search', function (hooks) { assert.dom('[data-test-try-again-button]').isEnabled(); let deferred = defer(); - this.server.get('/api/v1/crates', async function (schema, request) { - await deferred.promise; - return listCrates.call(this, schema, request); - }); + this.worker.resetHandlers(); + this.worker.use(http.get('/api/v1/crates', () => deferred.promise)); click('[data-test-try-again-button]'); await waitFor('[data-test-page-header] [data-test-spinner]'); @@ -174,64 +173,73 @@ module('Acceptance | search', function (hooks) { }); test('passes query parameters to the backend', async function (assert) { - this.server.get('/api/v1/crates', function (schema, request) { - assert.step('/api/v1/crates'); - - assert.deepEqual(request.queryParams, { - all_keywords: 'fire ball', - page: '3', - per_page: '15', - q: 'rust', - sort: 'new', - }); - - return { crates: [], meta: { total: 0 } }; - }); + this.worker.use( + http.get('/api/v1/crates', function ({ request }) { + assert.step('/api/v1/crates'); + + let url = new URL(request.url); + assert.deepEqual(Object.fromEntries(url.searchParams.entries()), { + all_keywords: 'fire ball', + page: '3', + per_page: '15', + q: 'rust', + sort: 'new', + }); + + return HttpResponse.json({ crates: [], meta: { total: 0 } }); + }), + ); await visit('/search?q=rust&page=3&per_page=15&sort=new&all_keywords=fire ball'); assert.verifySteps(['/api/v1/crates']); }); test('supports `keyword:bla` filters', async function (assert) { - this.server.get('/api/v1/crates', function (schema, request) { - assert.step('/api/v1/crates'); - - assert.deepEqual(request.queryParams, { - all_keywords: 'fire ball', - page: '3', - per_page: '15', - q: 'rust', - sort: 'new', - }); - - return { crates: [], meta: { total: 0 } }; - }); + this.worker.use( + http.get('/api/v1/crates', function ({ request }) { + assert.step('/api/v1/crates'); + + let url = new URL(request.url); + assert.deepEqual(Object.fromEntries(url.searchParams.entries()), { + all_keywords: 'fire ball', + page: '3', + per_page: '15', + q: 'rust', + sort: 'new', + }); + + return HttpResponse.json({ crates: [], meta: { total: 0 } }); + }), + ); await visit('/search?q=rust keyword:fire keyword:ball&page=3&per_page=15&sort=new'); assert.verifySteps(['/api/v1/crates']); }); test('`all_keywords` query parameter takes precedence over `keyword` filters', async function (assert) { - this.server.get('/api/v1/crates', function (schema, request) { - assert.step('/api/v1/crates'); - - assert.deepEqual(request.queryParams, { - all_keywords: 'fire ball', - page: '3', - per_page: '15', - q: 'rust keywords:foo', - sort: 'new', - }); - - return { crates: [], meta: { total: 0 } }; - }); + this.worker.use( + http.get('/api/v1/crates', function ({ request }) { + assert.step('/api/v1/crates'); + + let url = new URL(request.url); + assert.deepEqual(Object.fromEntries(url.searchParams.entries()), { + all_keywords: 'fire ball', + page: '3', + per_page: '15', + q: 'rust keywords:foo', + sort: 'new', + }); + + return HttpResponse.json({ crates: [], meta: { total: 0 } }); + }), + ); await visit('/search?q=rust keywords:foo&page=3&per_page=15&sort=new&all_keywords=fire ball'); assert.verifySteps(['/api/v1/crates']); }); test('visiting without query parameters works', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/search'); From 6d86ab9e82b2aae7962e9f933f11c98e424720b1 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:36:25 +0100 Subject: [PATCH 107/189] tests/acceptance/sudo: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/sudo-test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/acceptance/sudo-test.js b/tests/acceptance/sudo-test.js index 621f25d82d4..274fc7ae412 100644 --- a/tests/acceptance/sudo-test.js +++ b/tests/acceptance/sudo-test.js @@ -8,10 +8,10 @@ import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Acceptance | sudo', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); function prepare(context, isAdmin) { - const user = context.server.create('user', { + const user = context.db.user.create({ login: 'johnnydee', name: 'John Doe', email: 'john@doe.com', @@ -19,12 +19,12 @@ module('Acceptance | sudo', function (hooks) { isAdmin, }); - const crate = context.server.create('crate', { + const crate = context.db.crate.create({ name: 'foo', newest_version: '0.1.0', }); - const version = context.server.create('version', { + const version = context.db.version.create({ crate, num: '0.1.0', }); @@ -104,12 +104,12 @@ module('Acceptance | sudo', function (hooks) { await click('[data-test-version-yank-button="0.1.0"]'); await waitFor('[data-test-version-unyank-button="0.1.0"]'); - const crate = this.server.schema.crates.findBy({ name: 'foo' }); - const version = this.server.schema.versions.findBy({ crateId: crate.id, num: '0.1.0' }); + const crate = this.db.crate.findFirst({ where: { name: { equals: 'foo' } } }); + const version = this.db.version.findFirst({ crate: { id: { equals: crate.id } }, num: { equals: '0.1.0' } }); assert.true(version.yanked, 'The version should be yanked'); assert.dom('[data-test-version-unyank-button="0.1.0"]').exists(); await click('[data-test-version-unyank-button="0.1.0"]'); - const updatedVersion = this.server.schema.versions.findBy({ crateId: crate.id, num: '0.1.0' }); + const updatedVersion = this.db.version.findFirst({ crate: { id: { equals: crate.id } }, num: { equals: '0.1.0' } }); assert.false(updatedVersion.yanked, 'The version should be unyanked'); await waitFor('[data-test-version-yank-button="0.1.0"]'); From b29f320e8cbba0fa6d7fde97c9cff7bfd4988c1b Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:37:26 +0100 Subject: [PATCH 108/189] tests/acceptance/support: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/support-test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/acceptance/support-test.js b/tests/acceptance/support-test.js index 9b8c9491765..c3ea810ee3c 100644 --- a/tests/acceptance/support-test.js +++ b/tests/acceptance/support-test.js @@ -12,7 +12,7 @@ import axeConfig from '../axe-config'; import { visit } from '../helpers/visit-ignoring-abort'; module('Acceptance | support', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('shows an inquire list', async function (assert) { await visit('/support'); @@ -49,9 +49,9 @@ module('Acceptance | support', function (hooks) { setupWindowMock(hooks); async function prepare(context, assert) { - let server = context.server; - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '0.6.0' }); + let { db } = context; + let crate = db.crate.create({ name: 'nanomsg' }); + db.version.create({ crate, num: '0.6.0' }); window.open = (url, target, features) => { window.openKwargs = { url, target, features }; @@ -195,9 +195,9 @@ test detail setupWindowMock(hooks); async function prepare(context, assert) { - let server = context.server; - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '0.6.0' }); + let { db } = context; + let crate = db.crate.create({ name: 'nanomsg' }); + db.version.create({ crate, num: '0.6.0' }); window.open = (url, target, features) => { window.openKwargs = { url, target, features }; From 5786e2dc49d8d3cb657eddec6f9ffc0fa7188b61 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:37:59 +0100 Subject: [PATCH 109/189] tests/acceptance/team-page: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/team-page-test.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/acceptance/team-page-test.js b/tests/acceptance/team-page-test.js index 16ea87ecdd9..8196acfbc9d 100644 --- a/tests/acceptance/team-page-test.js +++ b/tests/acceptance/team-page-test.js @@ -1,6 +1,7 @@ import { visit } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import { loadFixtures } from '@crates-io/msw/fixtures.js'; import percySnapshot from '@percy/ember'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; @@ -9,10 +10,10 @@ import { setupApplicationTest } from 'crates-io/tests/helpers'; import axeConfig from '../axe-config'; module('Acceptance | team page', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('has team organization display', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/teams/github:org:thehydroimpulse'); @@ -24,7 +25,7 @@ module('Acceptance | team page', function (hooks) { }); test('has link to github in team header', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/teams/github:org:thehydroimpulse'); @@ -32,7 +33,7 @@ module('Acceptance | team page', function (hooks) { }); test('team organization details has github profile icon', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/teams/github:org:thehydroimpulse'); From 36422786d09033c458db8608001328e5292079e8 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:48:30 +0100 Subject: [PATCH 110/189] tests/acceptance/token-invites: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/token-invites-test.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/acceptance/token-invites-test.js b/tests/acceptance/token-invites-test.js index 0e534bca50e..9fdbe574ebc 100644 --- a/tests/acceptance/token-invites-test.js +++ b/tests/acceptance/token-invites-test.js @@ -2,13 +2,14 @@ import { currentURL } from '@ember/test-helpers'; import { module, test } from 'qunit'; import percySnapshot from '@percy/ember'; +import { http, HttpResponse } from 'msw'; import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Acceptance | /accept-invite/:token', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('visiting to /accept-invite shows 404 page', async function (assert) { await visit('/accept-invite'); @@ -25,6 +26,9 @@ module('Acceptance | /accept-invite/:token', function (hooks) { }); test('shows error for unknown token', async function (assert) { + let error = HttpResponse.json({}, { status: 404 }); + this.worker.use(http.put('/api/v1/me/crate_owner_invitations/accept/:token', () => error)); + await visit('/accept-invite/unknown'); assert.strictEqual(currentURL(), '/accept-invite/unknown'); assert.dom('[data-test-error-message]').hasText('You may want to visit crates.io/me/pending-invites to try again.'); @@ -34,7 +38,8 @@ module('Acceptance | /accept-invite/:token', function (hooks) { let errorMessage = 'The invitation to become an owner of the demo_crate crate expired. Please reach out to an owner of the crate to request a new invitation.'; let payload = { errors: [{ detail: errorMessage }] }; - this.server.put('/api/v1/me/crate_owner_invitations/accept/:token', payload, 410); + let error = HttpResponse.json(payload, { status: 410 }); + this.worker.use(http.put('/api/v1/me/crate_owner_invitations/accept/:token', () => error)); await visit('/accept-invite/secret123'); assert.strictEqual(currentURL(), '/accept-invite/secret123'); @@ -42,12 +47,12 @@ module('Acceptance | /accept-invite/:token', function (hooks) { }); test('shows success for known token', async function (assert) { - let inviter = this.server.create('user'); - let invitee = this.server.create('user'); + let inviter = this.db.user.create(); + let invitee = this.db.user.create(); - let crate = this.server.create('crate', { name: 'nanomsg' }); - this.server.create('version', { crate }); - let invite = this.server.create('crate-owner-invitation', { crate, invitee, inviter }); + let crate = this.db.crate.create({ name: 'nanomsg' }); + this.db.version.create({ crate }); + let invite = this.db.crateOwnerInvitation.create({ crate, invitee, inviter }); await visit(`/accept-invite/${invite.token}`); assert.strictEqual(currentURL(), `/accept-invite/${invite.token}`); From bbcbcdfbe131eff5d7971306a6fbeca935be6da4 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:49:08 +0100 Subject: [PATCH 111/189] tests/acceptance/user-page: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/user-page-test.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/acceptance/user-page-test.js b/tests/acceptance/user-page-test.js index 24792389604..63f955af181 100644 --- a/tests/acceptance/user-page-test.js +++ b/tests/acceptance/user-page-test.js @@ -1,6 +1,7 @@ import { visit } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import { loadFixtures } from '@crates-io/msw/fixtures.js'; import percySnapshot from '@percy/ember'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; @@ -9,10 +10,10 @@ import { setupApplicationTest } from 'crates-io/tests/helpers'; import axeConfig from '../axe-config'; module('Acceptance | user page', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('has user display', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/users/thehydroimpulse'); @@ -23,7 +24,7 @@ module('Acceptance | user page', function (hooks) { }); test('has link to github in user header', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/users/thehydroimpulse'); @@ -31,7 +32,7 @@ module('Acceptance | user page', function (hooks) { }); test('user details has github profile icon', async function (assert) { - this.server.loadFixtures(); + loadFixtures(this.db); await visit('/users/thehydroimpulse'); From 68a5ef8dc8f7f2db6065ffc0a9e74e0c2295c4e7 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 15:49:45 +0100 Subject: [PATCH 112/189] tests/acceptance/versions: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/versions-test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/acceptance/versions-test.js b/tests/acceptance/versions-test.js index 1f5bcedd602..d29fd50ae6a 100644 --- a/tests/acceptance/versions-test.js +++ b/tests/acceptance/versions-test.js @@ -6,14 +6,14 @@ import percySnapshot from '@percy/ember'; import { setupApplicationTest } from 'crates-io/tests/helpers'; module('Acceptance | crate versions page', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('show versions sorted by date', async function (assert) { - let crate = this.server.create('crate', { name: 'nanomsg' }); - this.server.create('version', { crate, num: '0.1.0', created_at: '2017-01-01' }); - this.server.create('version', { crate, num: '0.2.0', created_at: '2018-01-01' }); - this.server.create('version', { crate, num: '0.3.0', created_at: '2019-01-01', rust_version: '1.69' }); - this.server.create('version', { crate, num: '0.2.1', created_at: '2020-01-01' }); + let crate = this.db.crate.create({ name: 'nanomsg' }); + this.db.version.create({ crate, num: '0.1.0', created_at: '2017-01-01' }); + this.db.version.create({ crate, num: '0.2.0', created_at: '2018-01-01' }); + this.db.version.create({ crate, num: '0.3.0', created_at: '2019-01-01', rust_version: '1.69' }); + this.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' }); await visit('/crates/nanomsg/versions'); assert.strictEqual(currentURL(), '/crates/nanomsg/versions'); From c8a64a000d1e930fe8de5a2132f57f3350fa8f54 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:20:07 +0100 Subject: [PATCH 113/189] tests/acceptance/settings: Migrate from `mirage` to `@crates-io/msw` --- tests/acceptance/settings/settings-test.js | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/acceptance/settings/settings-test.js b/tests/acceptance/settings/settings-test.js index 7f3073e9015..ba5d3bcade0 100644 --- a/tests/acceptance/settings/settings-test.js +++ b/tests/acceptance/settings/settings-test.js @@ -9,22 +9,22 @@ import { setupApplicationTest } from 'crates-io/tests/helpers'; import axeConfig from '../../axe-config'; module('Acceptance | Settings', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); function prepare(context) { - let { server } = context; - - let user1 = server.create('user', { name: 'blabaere' }); - let user2 = server.create('user', { name: 'thehydroimpulse' }); - let team1 = server.create('team', { org: 'org', name: 'blabaere' }); - let team2 = server.create('team', { org: 'org', name: 'thehydroimpulse' }); - - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '1.0.0' }); - server.create('crate-ownership', { crate, user: user1 }); - server.create('crate-ownership', { crate, user: user2 }); - server.create('crate-ownership', { crate, team: team1 }); - server.create('crate-ownership', { crate, team: team2 }); + let { db } = context; + + let user1 = db.user.create({ name: 'blabaere' }); + let user2 = db.user.create({ name: 'thehydroimpulse' }); + let team1 = db.team.create({ org: 'org', name: 'blabaere' }); + let team2 = db.team.create({ org: 'org', name: 'thehydroimpulse' }); + + let crate = db.crate.create({ name: 'nanomsg' }); + db.version.create({ crate, num: '1.0.0' }); + db.crateOwnership.create({ crate, user: user1 }); + db.crateOwnership.create({ crate, user: user2 }); + db.crateOwnership.create({ crate, team: team1 }); + db.crateOwnership.create({ crate, team: team2 }); context.authenticateAs(user1); From 72481d99afe89f7a06c307b20d7bc86b082afd73 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:22:56 +0100 Subject: [PATCH 114/189] tests/bugs/2329: Migrate from `mirage` to `@crates-io/msw` --- tests/bugs/2329-test.js | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/bugs/2329-test.js b/tests/bugs/2329-test.js index 4f3d486fbfa..5c7869796bd 100644 --- a/tests/bugs/2329-test.js +++ b/tests/bugs/2329-test.js @@ -3,32 +3,36 @@ import { module, test } from 'qunit'; import window from 'ember-window-mock'; import { setupWindowMock } from 'ember-window-mock/test-support'; +import { http, HttpResponse } from 'msw'; import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Bug #2329', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); setupWindowMock(hooks); test('is fixed', async function (assert) { - let user = this.server.create('user'); + let { db } = this; - let foobar = this.server.create('crate', { name: 'foo-bar' }); - this.server.create('crate-ownership', { crate: foobar, user, emailNotifications: true }); - this.server.create('version', { crate: foobar }); + let user = this.db.user.create(); - let bar = this.server.create('crate', { name: 'barrrrr' }); - this.server.create('crate-ownership', { crate: bar, user, emailNotifications: false }); - this.server.create('version', { crate: bar }); + let foobar = this.db.crate.create({ name: 'foo-bar' }); + this.db.crateOwnership.create({ crate: foobar, user, emailNotifications: true }); + this.db.version.create({ crate: foobar }); - this.server.get('/api/private/session/begin', { url: 'url-to-github-including-state-secret' }); + let bar = this.db.crate.create({ name: 'barrrrr' }); + this.db.crateOwnership.create({ crate: bar, user, emailNotifications: false }); + this.db.version.create({ crate: bar }); - this.server.get('/api/private/session/authorize', () => { - this.server.create('mirage-session', { user }); - return { ok: true }; - }); + this.worker.use( + http.get('/api/private/session/begin', () => HttpResponse.json({ url: 'url-to-github-including-state-secret' })), + http.get('/api/private/session/authorize', () => { + db.mswSession.create({ user }); + return HttpResponse.json({ ok: true }); + }), + ); let fakeWindow = { document: { write() {}, close() {} }, close() {} }; window.open = () => fakeWindow; From 679cc4512180732e537408c6bb758600c4cf64c1 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:24:19 +0100 Subject: [PATCH 115/189] tests/bugs/4506: Migrate from `mirage` to `@crates-io/msw` --- tests/bugs/4506-test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/bugs/4506-test.js b/tests/bugs/4506-test.js index 38e606aab8d..9f3734cc775 100644 --- a/tests/bugs/4506-test.js +++ b/tests/bugs/4506-test.js @@ -6,18 +6,18 @@ import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Bug #4506', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); function prepare(context) { - let { server } = context; + let { db } = context; - server.create('keyword', { keyword: 'no-std' }); + let noStd = db.keyword.create({ keyword: 'no-std' }); - let foo = server.create('crate', { name: 'foo', keywordIds: ['no-std'] }); - server.create('version', { crate: foo }); + let foo = db.crate.create({ name: 'foo', keywords: [noStd] }); + db.version.create({ crate: foo }); - let bar = server.create('crate', { name: 'bar', keywordIds: ['no-std'] }); - server.create('version', { crate: bar }); + let bar = db.crate.create({ name: 'bar', keywords: [noStd] }); + db.version.create({ crate: bar }); } test('is fixed', async function (assert) { From b4b458c527bad0c772bccbf11ae32532499b63e2 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:25:50 +0100 Subject: [PATCH 116/189] tests/routes/crate/version/crate-links: Migrate from `mirage` to `@crates-io/msw` --- tests/routes/crate/version/crate-links-test.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/routes/crate/version/crate-links-test.js b/tests/routes/crate/version/crate-links-test.js index e6e8a32fda6..2d60dc92ea1 100644 --- a/tests/routes/crate/version/crate-links-test.js +++ b/tests/routes/crate/version/crate-links-test.js @@ -4,16 +4,16 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'crates-io/tests/helpers'; module('Route | crate.version | crate links', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('shows all external crate links', async function (assert) { - let crate = this.server.create('crate', { + let crate = this.db.crate.create({ name: 'foo', homepage: 'https://crates.io/', documentation: 'https://doc.rust-lang.org/cargo/getting-started/', repository: 'https://github.com/rust-lang/crates.io.git', }); - this.server.create('version', { crate, num: '1.0.0' }); + this.db.version.create({ crate, num: '1.0.0' }); await visit('/crates/foo'); @@ -31,8 +31,8 @@ module('Route | crate.version | crate links', function (hooks) { }); test('shows no external crate links if none are set', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0' }); await visit('/crates/foo'); @@ -42,12 +42,12 @@ module('Route | crate.version | crate links', function (hooks) { }); test('hide the homepage link if it is the same as the repository', async function (assert) { - let crate = this.server.create('crate', { + let crate = this.db.crate.create({ name: 'foo', homepage: 'https://github.com/rust-lang/crates.io', repository: 'https://github.com/rust-lang/crates.io', }); - this.server.create('version', { crate, num: '1.0.0' }); + this.db.version.create({ crate, num: '1.0.0' }); await visit('/crates/foo'); @@ -61,12 +61,12 @@ module('Route | crate.version | crate links', function (hooks) { }); test('hide the homepage link if it is the same as the repository plus `.git`', async function (assert) { - let crate = this.server.create('crate', { + let crate = this.db.crate.create({ name: 'foo', homepage: 'https://github.com/rust-lang/crates.io/', repository: 'https://github.com/rust-lang/crates.io.git', }); - this.server.create('version', { crate, num: '1.0.0' }); + this.db.version.create({ crate, num: '1.0.0' }); await visit('/crates/foo'); From a10fda0ae6cc32371f38b6d2812d8bbbd390aac1 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:29:59 +0100 Subject: [PATCH 117/189] tests/routes/crate/version/docs-link: Migrate from `mirage` to `@crates-io/msw` --- tests/routes/crate/version/docs-link-test.js | 56 ++++++++++---------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/tests/routes/crate/version/docs-link-test.js b/tests/routes/crate/version/docs-link-test.js index fe4a5c0cfd2..efa26feb8c8 100644 --- a/tests/routes/crate/version/docs-link-test.js +++ b/tests/routes/crate/version/docs-link-test.js @@ -1,80 +1,82 @@ import { visit } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import { http, HttpResponse } from 'msw'; + import { setupApplicationTest } from 'crates-io/tests/helpers'; module('Route | crate.version | docs link', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('shows regular documentation link', async function (assert) { - let crate = this.server.create('crate', { name: 'foo', documentation: 'https://foo.io/docs' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'foo', documentation: 'https://foo.io/docs' }); + this.db.version.create({ crate, num: '1.0.0' }); await visit('/crates/foo'); assert.dom('[data-test-docs-link] a').hasAttribute('href', 'https://foo.io/docs'); }); test('show no docs link if `documentation` is unspecified and there are no related docs.rs builds', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0' }); - this.server.get('https://docs.rs/crate/:crate/:version/status.json', 'not found', 404); + let error = HttpResponse.text('not found', { status: 404 }); + this.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => error)); await visit('/crates/foo'); assert.dom('[data-test-docs-link] a').doesNotExist(); }); test('show docs link if `documentation` is unspecified and there are related docs.rs builds', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0' }); - this.server.get('https://docs.rs/crate/:crate/:version/status.json', { - doc_status: true, - version: '1.0.0', - }); + let response = HttpResponse.json({ doc_status: true, version: '1.0.0' }); + this.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => response)); await visit('/crates/foo'); assert.dom('[data-test-docs-link] a').hasAttribute('href', 'https://docs.rs/foo/1.0.0'); }); test('show original docs link if `documentation` points to docs.rs and there are no related docs.rs builds', async function (assert) { - let crate = this.server.create('crate', { name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); + this.db.version.create({ crate, num: '1.0.0' }); - this.server.get('https://docs.rs/crate/:crate/:version/status.json', 'not found', 404); + let error = HttpResponse.text('not found', { status: 404 }); + this.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => error)); await visit('/crates/foo'); assert.dom('[data-test-docs-link] a').hasAttribute('href', 'https://docs.rs/foo/0.6.2'); }); test('show updated docs link if `documentation` points to docs.rs and there are related docs.rs builds', async function (assert) { - let crate = this.server.create('crate', { name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); + this.db.version.create({ crate, num: '1.0.0' }); - this.server.get('https://docs.rs/crate/:crate/:version/status.json', { - doc_status: true, - version: '1.0.0', - }); + let response = HttpResponse.json({ doc_status: true, version: '1.0.0' }); + this.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => response)); await visit('/crates/foo'); assert.dom('[data-test-docs-link] a').hasAttribute('href', 'https://docs.rs/foo/1.0.0'); }); test('ajax errors are ignored', async function (assert) { - let crate = this.server.create('crate', { name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); + this.db.version.create({ crate, num: '1.0.0' }); - this.server.get('https://docs.rs/crate/:crate/:version/status.json', 'error', 500); + let error = HttpResponse.text('error', { status: 500 }); + this.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => error)); await visit('/crates/foo'); assert.dom('[data-test-docs-link] a').hasAttribute('href', 'https://docs.rs/foo/0.6.2'); }); test('empty docs.rs responses are ignored', async function (assert) { - let crate = this.server.create('crate', { name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); - this.server.create('version', { crate, num: '0.6.2' }); + let crate = this.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); + this.db.version.create({ crate, num: '0.6.2' }); - this.server.get('https://docs.rs/crate/:crate/:version/status.json', {}); + let response = HttpResponse.json({}); + this.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => response)); await visit('/crates/foo'); assert.dom('[data-test-docs-link] a').hasAttribute('href', 'https://docs.rs/foo/0.6.2'); From 581a82fab1913ad30c6201c6ceff45ddb619c225 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:30:54 +0100 Subject: [PATCH 118/189] tests/routes/crate/version/model: Migrate from `mirage` to `@crates-io/msw` --- tests/routes/crate/version/model-test.js | 56 ++++++++++++------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/routes/crate/version/model-test.js b/tests/routes/crate/version/model-test.js index 9a34b64cd1e..078a3fdf769 100644 --- a/tests/routes/crate/version/model-test.js +++ b/tests/routes/crate/version/model-test.js @@ -6,14 +6,14 @@ import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../../../helpers/visit-ignoring-abort'; module('Route | crate.version | model() hook', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); module('with explicit version number in the URL', function () { test('shows yanked versions', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.2.3', yanked: true }); - this.server.create('version', { crate, num: '2.0.0-beta.1' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0' }); + this.db.version.create({ crate, num: '1.2.3', yanked: true }); + this.db.version.create({ crate, num: '2.0.0-beta.1' }); await visit('/crates/foo/1.2.3'); assert.strictEqual(currentURL(), `/crates/foo/1.2.3`); @@ -26,10 +26,10 @@ module('Route | crate.version | model() hook', function (hooks) { }); test('shows error page for unknown versions', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.2.3', yanked: true }); - this.server.create('version', { crate, num: '2.0.0-beta.1' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0' }); + this.db.version.create({ crate, num: '1.2.3', yanked: true }); + this.db.version.create({ crate, num: '2.0.0-beta.1' }); await visit('/crates/foo/2.0.0'); assert.strictEqual(currentURL(), `/crates/foo/2.0.0`); @@ -42,11 +42,11 @@ module('Route | crate.version | model() hook', function (hooks) { module('without version number in the URL', function () { test('defaults to the highest stable version', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.2.3', yanked: true }); - this.server.create('version', { crate, num: '2.0.0-beta.1' }); - this.server.create('version', { crate, num: '2.0.0' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0' }); + this.db.version.create({ crate, num: '1.2.3', yanked: true }); + this.db.version.create({ crate, num: '2.0.0-beta.1' }); + this.db.version.create({ crate, num: '2.0.0' }); await visit('/crates/foo'); assert.strictEqual(currentURL(), `/crates/foo`); @@ -59,10 +59,10 @@ module('Route | crate.version | model() hook', function (hooks) { }); test('defaults to the highest stable version, even if there are higher prereleases', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.2.3', yanked: true }); - this.server.create('version', { crate, num: '2.0.0-beta.1' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0' }); + this.db.version.create({ crate, num: '1.2.3', yanked: true }); + this.db.version.create({ crate, num: '2.0.0-beta.1' }); await visit('/crates/foo'); assert.strictEqual(currentURL(), `/crates/foo`); @@ -75,12 +75,12 @@ module('Route | crate.version | model() hook', function (hooks) { }); test('defaults to the highest not-yanked version', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0', yanked: true }); - this.server.create('version', { crate, num: '1.2.3', yanked: true }); - this.server.create('version', { crate, num: '2.0.0-beta.1' }); - this.server.create('version', { crate, num: '2.0.0-beta.2' }); - this.server.create('version', { crate, num: '2.0.0', yanked: true }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0', yanked: true }); + this.db.version.create({ crate, num: '1.2.3', yanked: true }); + this.db.version.create({ crate, num: '2.0.0-beta.1' }); + this.db.version.create({ crate, num: '2.0.0-beta.2' }); + this.db.version.create({ crate, num: '2.0.0', yanked: true }); await visit('/crates/foo'); assert.strictEqual(currentURL(), `/crates/foo`); @@ -93,10 +93,10 @@ module('Route | crate.version | model() hook', function (hooks) { }); test('if there are only yanked versions, it defaults to the latest version', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0', yanked: true }); - this.server.create('version', { crate, num: '1.2.3', yanked: true }); - this.server.create('version', { crate, num: '2.0.0-beta.1', yanked: true }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0', yanked: true }); + this.db.version.create({ crate, num: '1.2.3', yanked: true }); + this.db.version.create({ crate, num: '2.0.0-beta.1', yanked: true }); await visit('/crates/foo'); assert.strictEqual(currentURL(), `/crates/foo`); From e879541a6cf918635a309734014270e2e763be39 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:35:29 +0100 Subject: [PATCH 119/189] tests/routes/crate/delete: Migrate from `mirage` to `@crates-io/msw` --- tests/routes/crate/delete-test.js | 35 ++++++++++++++++--------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/tests/routes/crate/delete-test.js b/tests/routes/crate/delete-test.js index dcf232b86da..d6a80481bba 100644 --- a/tests/routes/crate/delete-test.js +++ b/tests/routes/crate/delete-test.js @@ -4,21 +4,21 @@ import { module, test } from 'qunit'; import { defer } from 'rsvp'; import percySnapshot from '@percy/ember'; -import { Response } from 'miragejs'; +import { http, HttpResponse } from 'msw'; import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../../helpers/visit-ignoring-abort'; module('Route: crate.delete', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); function prepare(context) { - let user = context.server.create('user'); + let user = context.db.user.create(); - let crate = context.server.create('crate', { name: 'foo' }); - context.server.create('version', { crate }); - context.server.create('crate-ownership', { crate, user }); + let crate = context.db.crate.create({ name: 'foo' }); + context.db.version.create({ crate }); + context.db.crateOwnership.create({ crate, user }); context.authenticateAs(user); @@ -26,8 +26,8 @@ module('Route: crate.delete', function (hooks) { } test('unauthenticated', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate }); await visit('/crates/foo/delete'); assert.strictEqual(currentURL(), '/crates/foo/delete'); @@ -36,13 +36,13 @@ module('Route: crate.delete', function (hooks) { }); test('not an owner', async function (assert) { - let user1 = this.server.create('user'); + let user1 = this.db.user.create(); this.authenticateAs(user1); - let user2 = this.server.create('user'); - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate }); - this.server.create('crate-ownership', { crate, user: user2 }); + let user2 = this.db.user.create(); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate }); + this.db.crateOwnership.create({ crate, user: user2 }); await visit('/crates/foo/delete'); assert.strictEqual(currentURL(), '/crates/foo/delete'); @@ -69,7 +69,7 @@ module('Route: crate.delete', function (hooks) { let message = 'Crate foo has been successfully deleted.'; assert.dom('[data-test-notification-message="success"]').hasText(message); - let crate = this.server.schema.crates.findBy({ name: 'foo' }); + let crate = this.db.crate.findFirst({ where: { name: { equals: 'foo' } } }); assert.strictEqual(crate, null); }); @@ -77,7 +77,7 @@ module('Route: crate.delete', function (hooks) { prepare(this); let deferred = defer(); - this.server.delete('/api/v1/crates/foo', deferred.promise); + this.worker.use(http.delete('/api/v1/crates/foo', () => deferred.promise)); await visit('/crates/foo/delete'); await fillIn('[data-test-reason]', "I don't need this crate anymore"); @@ -87,7 +87,7 @@ module('Route: crate.delete', function (hooks) { assert.dom('[data-test-confirmation-checkbox]').isDisabled(); assert.dom('[data-test-delete-button]').isDisabled(); - deferred.resolve(new Response(204)); + deferred.resolve(); await clickPromise; assert.strictEqual(currentURL(), '/'); @@ -97,7 +97,8 @@ module('Route: crate.delete', function (hooks) { prepare(this); let payload = { errors: [{ detail: 'only crates without reverse dependencies can be deleted after 72 hours' }] }; - this.server.delete('/api/v1/crates/foo', payload, 422); + let error = HttpResponse.json(payload, { status: 422 }); + this.worker.use(http.delete('/api/v1/crates/foo', () => error)); await visit('/crates/foo/delete'); await fillIn('[data-test-reason]', "I don't need this crate anymore"); From 090faf86912d9a0bfcfd45b41342e76f57ae619b Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:38:19 +0100 Subject: [PATCH 120/189] tests/routes/crate/range: Migrate from `mirage` to `@crates-io/msw` --- tests/routes/crate/range-test.js | 74 +++++++++++++++++--------------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/tests/routes/crate/range-test.js b/tests/routes/crate/range-test.js index 421658a157b..8d0ab4933c8 100644 --- a/tests/routes/crate/range-test.js +++ b/tests/routes/crate/range-test.js @@ -1,19 +1,21 @@ import { currentURL } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import { http, HttpResponse } from 'msw'; + import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../../helpers/visit-ignoring-abort'; module('Route | crate.range', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('happy path', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.1.0' }); - this.server.create('version', { crate, num: '1.2.0' }); - this.server.create('version', { crate, num: '1.2.3' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0' }); + this.db.version.create({ crate, num: '1.1.0' }); + this.db.version.create({ crate, num: '1.2.0' }); + this.db.version.create({ crate, num: '1.2.3' }); await visit('/crates/foo/range/^1.1.0'); assert.strictEqual(currentURL(), `/crates/foo/1.2.3`); @@ -23,11 +25,11 @@ module('Route | crate.range', function (hooks) { }); test('happy path with tilde range', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.1.0' }); - this.server.create('version', { crate, num: '1.1.1' }); - this.server.create('version', { crate, num: '1.2.0' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0' }); + this.db.version.create({ crate, num: '1.1.0' }); + this.db.version.create({ crate, num: '1.1.1' }); + this.db.version.create({ crate, num: '1.2.0' }); await visit('/crates/foo/range/~1.1.0'); assert.strictEqual(currentURL(), `/crates/foo/1.1.1`); @@ -37,11 +39,11 @@ module('Route | crate.range', function (hooks) { }); test('happy path with cargo style and', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.4.2' }); - this.server.create('version', { crate, num: '1.3.4' }); - this.server.create('version', { crate, num: '1.3.3' }); - this.server.create('version', { crate, num: '1.2.6' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.4.2' }); + this.db.version.create({ crate, num: '1.3.4' }); + this.db.version.create({ crate, num: '1.3.3' }); + this.db.version.create({ crate, num: '1.2.6' }); await visit('/crates/foo/range/>=1.3.0, <1.4.0'); assert.strictEqual(currentURL(), `/crates/foo/1.3.4`); @@ -51,11 +53,11 @@ module('Route | crate.range', function (hooks) { }); test('ignores yanked versions if possible', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.1.0' }); - this.server.create('version', { crate, num: '1.1.1' }); - this.server.create('version', { crate, num: '1.2.0', yanked: true }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0' }); + this.db.version.create({ crate, num: '1.1.0' }); + this.db.version.create({ crate, num: '1.1.1' }); + this.db.version.create({ crate, num: '1.2.0', yanked: true }); await visit('/crates/foo/range/^1.0.0'); assert.strictEqual(currentURL(), `/crates/foo/1.1.1`); @@ -65,11 +67,11 @@ module('Route | crate.range', function (hooks) { }); test('falls back to yanked version if necessary', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0', yanked: true }); - this.server.create('version', { crate, num: '1.1.0', yanked: true }); - this.server.create('version', { crate, num: '1.1.1', yanked: true }); - this.server.create('version', { crate, num: '2.0.0' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0', yanked: true }); + this.db.version.create({ crate, num: '1.1.0', yanked: true }); + this.db.version.create({ crate, num: '1.1.1', yanked: true }); + this.db.version.create({ crate, num: '2.0.0' }); await visit('/crates/foo/range/^1.0.0'); assert.strictEqual(currentURL(), `/crates/foo/1.1.1`); @@ -88,7 +90,8 @@ module('Route | crate.range', function (hooks) { }); test('shows an error page if crate fails to load', async function (assert) { - this.server.get('/api/v1/crates/:crate_name', {}, 500); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.get('/api/v1/crates/:crate_name', () => error)); await visit('/crates/foo/range/^3'); assert.strictEqual(currentURL(), '/crates/foo/range/%5E3'); @@ -99,11 +102,11 @@ module('Route | crate.range', function (hooks) { }); test('shows an error page if no match found', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.1.0' }); - this.server.create('version', { crate, num: '1.1.1' }); - this.server.create('version', { crate, num: '2.0.0' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0' }); + this.db.version.create({ crate, num: '1.1.0' }); + this.db.version.create({ crate, num: '1.1.1' }); + this.db.version.create({ crate, num: '2.0.0' }); await visit('/crates/foo/range/^3'); assert.strictEqual(currentURL(), '/crates/foo/range/%5E3'); @@ -114,10 +117,11 @@ module('Route | crate.range', function (hooks) { }); test('shows an error page if versions fail to load', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '3.2.1' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '3.2.1' }); - this.server.get('/api/v1/crates/:crate_name/versions', {}, 500); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.get('/api/v1/crates/:crate_name/versions', () => error)); await visit('/crates/foo/range/^3'); assert.strictEqual(currentURL(), '/crates/foo/range/%5E3'); From e13525d2baabfa156d6d194806f2715f52762386 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:42:51 +0100 Subject: [PATCH 121/189] tests/routes/settings/tokens: Migrate from `mirage` to `@crates-io/msw` --- tests/routes/settings/tokens/index-test.js | 6 +++--- tests/routes/settings/tokens/new-test.js | 24 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/routes/settings/tokens/index-test.js b/tests/routes/settings/tokens/index-test.js index 26f1948fdab..042442a7d14 100644 --- a/tests/routes/settings/tokens/index-test.js +++ b/tests/routes/settings/tokens/index-test.js @@ -6,10 +6,10 @@ import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../../../helpers/visit-ignoring-abort'; module('/settings/tokens', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); function prepare(context) { - let user = context.server.create('user', { + let user = context.db.user.create({ login: 'johnnydee', name: 'John Doe', email: 'john@doe.com', @@ -24,7 +24,7 @@ module('/settings/tokens', function (hooks) { test('reloads all tokens from the server', async function (assert) { let { user } = prepare(this); - this.server.create('api-token', { user, name: 'token-1' }); + this.db.apiToken.create({ user, name: 'token-1' }); await visit('/settings/tokens/new'); assert.strictEqual(currentURL(), '/settings/tokens/new'); diff --git a/tests/routes/settings/tokens/new-test.js b/tests/routes/settings/tokens/new-test.js index b70a08f70c5..f1126c0ac34 100644 --- a/tests/routes/settings/tokens/new-test.js +++ b/tests/routes/settings/tokens/new-test.js @@ -3,17 +3,17 @@ import { module, test } from 'qunit'; import { defer } from 'rsvp'; -import { Response } from 'miragejs'; +import { http, HttpResponse } from 'msw'; import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../../../helpers/visit-ignoring-abort'; module('/settings/tokens/new', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); function prepare(context) { - let user = context.server.create('user', { + let user = context.db.user.create({ login: 'johnnydee', name: 'John Doe', email: 'john@doe.com', @@ -60,7 +60,7 @@ module('/settings/tokens/new', function (hooks) { await click('[data-test-scope="publish-update"]'); await click('[data-test-generate]'); - let token = this.server.schema.apiTokens.findBy({ name: 'token-name' }); + let token = this.db.apiToken.findFirst({ where: { name: { equals: 'token-name' } } }); assert.ok(Boolean(token), 'API token has been created in the backend database'); assert.strictEqual(token.name, 'token-name'); assert.strictEqual(token.expiredAt, null); @@ -133,7 +133,7 @@ module('/settings/tokens/new', function (hooks) { await click('[data-test-generate]'); - let token = this.server.schema.apiTokens.findBy({ name: 'token-name' }); + let token = this.db.apiToken.findFirst({ where: { name: { equals: 'token-name' } } }); assert.ok(Boolean(token), 'API token has been created in the backend database'); assert.strictEqual(token.name, 'token-name'); assert.deepEqual(token.crateScopes, ['serde-*', 'serde']); @@ -173,7 +173,7 @@ module('/settings/tokens/new', function (hooks) { await click('[data-test-scope="publish-update"]'); await click('[data-test-generate]'); - let token = this.server.schema.apiTokens.findBy({ name: 'token-name' }); + let token = this.db.apiToken.findFirst({ where: { name: { equals: 'token-name' } } }); assert.ok(Boolean(token), 'API token has been created in the backend database'); assert.strictEqual(token.name, 'token-name'); assert.strictEqual(token.expiredAt.slice(0, 10), '2017-12-20'); @@ -209,7 +209,7 @@ module('/settings/tokens/new', function (hooks) { await click('[data-test-generate]'); - let token = this.server.schema.apiTokens.findBy({ name: 'token-name' }); + let token = this.db.apiToken.findFirst({ where: { name: { equals: 'token-name' } } }); assert.ok(Boolean(token), 'API token has been created in the backend database'); assert.strictEqual(token.name, 'token-name'); assert.strictEqual(token.expiredAt.slice(0, 10), '2024-05-04'); @@ -228,7 +228,7 @@ module('/settings/tokens/new', function (hooks) { prepare(this); let deferred = defer(); - this.server.put('/api/v1/me/tokens', deferred.promise); + this.worker.use(http.put('/api/v1/me/tokens', () => deferred.promise)); await visit('/settings/tokens/new'); assert.strictEqual(currentURL(), '/settings/tokens/new'); @@ -240,7 +240,7 @@ module('/settings/tokens/new', function (hooks) { assert.dom('[data-test-name]').isDisabled(); assert.dom('[data-test-generate]').isDisabled(); - deferred.resolve(new Response(500)); + deferred.resolve(HttpResponse.json({}, { status: 500 })); await clickPromise; let message = 'An error has occurred while generating your API token. Please try again later!'; @@ -289,7 +289,7 @@ module('/settings/tokens/new', function (hooks) { test('prefill with the exist token', async function (assert) { let { user } = prepare(this); - let token = this.server.create('api-token', { + let token = this.db.apiToken.create({ user, name: 'foo', createdAt: '2017-08-01T12:34:56', @@ -310,7 +310,7 @@ module('/settings/tokens/new', function (hooks) { await click('[data-test-generate]'); assert.strictEqual(currentURL(), '/settings/tokens'); - let tokens = this.server.schema.apiTokens.where({ name: 'foo' }); + let tokens = this.db.apiToken.findMany({ where: { name: { equals: 'foo' } } }); assert.strictEqual(tokens.length, 2, 'New API token has been created in the backend database'); // It should reset the token ID query parameter. @@ -321,7 +321,7 @@ module('/settings/tokens/new', function (hooks) { test('prefilled: crate scoped can be added', async function (assert) { let { user } = prepare(this); - let token = this.server.create('api-token', { + let token = this.db.apiToken.create({ user, name: 'serde', crateScopes: ['serde', 'serde-*'], From 7fbfb754661b384f8f47bfffbed1c41b3df1902b Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:43:59 +0100 Subject: [PATCH 122/189] tests/routes/category: Migrate from `mirage` to `@crates-io/msw` --- tests/routes/category-test.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/routes/category-test.js b/tests/routes/category-test.js index a72fa4a7f37..e2ce3b9f65b 100644 --- a/tests/routes/category-test.js +++ b/tests/routes/category-test.js @@ -1,12 +1,14 @@ import { currentURL } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import { http, HttpResponse } from 'msw'; + import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Route | category', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test("shows an error message if the category can't be found", async function (assert) { await visit('/categories/foo'); @@ -18,7 +20,8 @@ module('Route | category', function (hooks) { }); test('server error causes the error page to be shown', async function (assert) { - this.server.get('/api/v1/categories/:categoryId', {}, 500); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.get('/api/v1/categories/:categoryId', () => error)); await visit('/categories/foo'); assert.strictEqual(currentURL(), '/categories/foo'); @@ -29,7 +32,7 @@ module('Route | category', function (hooks) { }); test('updates the search field when the categories route is accessed', async function (assert) { - this.server.create('category', { category: 'foo' }); + this.db.category.create({ category: 'foo' }); await visit('/'); assert.dom('[data-test-search-input]').hasValue(''); From 733eb4fb16020f5cb03dcb4ec550dcacfb8d24aa Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:44:54 +0100 Subject: [PATCH 123/189] tests/routes/keyword: Migrate from `mirage` to `@crates-io/msw` --- tests/routes/keyword-test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/routes/keyword-test.js b/tests/routes/keyword-test.js index 9ce414abe5b..91af4afe55b 100644 --- a/tests/routes/keyword-test.js +++ b/tests/routes/keyword-test.js @@ -1,12 +1,14 @@ import { currentURL } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import { http, HttpResponse } from 'msw'; + import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Route | keyword', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('shows an empty list if the keyword does not exist on the server', async function (assert) { await visit('/keywords/foo'); @@ -15,7 +17,8 @@ module('Route | keyword', function (hooks) { }); test('server error causes the error page to be shown', async function (assert) { - this.server.get('/api/v1/crates', {}, 500); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.get('/api/v1/crates', () => error)); await visit('/keywords/foo'); assert.strictEqual(currentURL(), '/keywords/foo'); From cb20fb82eec3ad29b18e3e1532f524aefa2b86fc Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:45:37 +0100 Subject: [PATCH 124/189] tests/routes/support: Migrate from `mirage` to `@crates-io/msw` --- tests/routes/support-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/routes/support-test.js b/tests/routes/support-test.js index dc930d32340..2b34317b8ee 100644 --- a/tests/routes/support-test.js +++ b/tests/routes/support-test.js @@ -8,7 +8,7 @@ import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Route | support', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test('should not retain query params when exiting and then returning', async function (assert) { await visit('/support?inquire=crate-violation'); From 38c1e48c22dd96284d594aed8d1d188187dcfffd Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:46:13 +0100 Subject: [PATCH 125/189] tests/routes/team: Migrate from `mirage` to `@crates-io/msw` --- tests/routes/team-test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/routes/team-test.js b/tests/routes/team-test.js index fe90e7d5331..2837cce4eff 100644 --- a/tests/routes/team-test.js +++ b/tests/routes/team-test.js @@ -1,12 +1,14 @@ import { currentURL } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import { http, HttpResponse } from 'msw'; + import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Route | team', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test("shows an error message if the user can't be found", async function (assert) { await visit('/teams/foo'); @@ -18,7 +20,8 @@ module('Route | team', function (hooks) { }); test('server error causes the error page to be shown', async function (assert) { - this.server.get('/api/v1/teams/:id', {}, 500); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.get('/api/v1/teams/:id', () => error)); await visit('/teams/foo'); assert.strictEqual(currentURL(), '/teams/foo'); From bec3ce7dd688094ebfbfe1c6145495af21d4b5ea Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:46:54 +0100 Subject: [PATCH 126/189] tests/routes/user: Migrate from `mirage` to `@crates-io/msw` --- tests/routes/user-test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/routes/user-test.js b/tests/routes/user-test.js index 9379dd66887..c7136a93646 100644 --- a/tests/routes/user-test.js +++ b/tests/routes/user-test.js @@ -1,12 +1,14 @@ import { currentURL } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import { http, HttpResponse } from 'msw'; + import { setupApplicationTest } from 'crates-io/tests/helpers'; import { visit } from '../helpers/visit-ignoring-abort'; module('Route | user', function (hooks) { - setupApplicationTest(hooks); + setupApplicationTest(hooks, { msw: true }); test("shows an error message if the user can't be found", async function (assert) { await visit('/users/foo'); @@ -18,7 +20,8 @@ module('Route | user', function (hooks) { }); test('server error causes the error page to be shown', async function (assert) { - this.server.get('/api/v1/users/:id', {}, 500); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.get('/api/v1/users/:id', () => error)); await visit('/users/foo'); assert.strictEqual(currentURL(), '/users/foo'); From e270add1416467b1dc98dbfaab4b0735103e1574 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:51:04 +0100 Subject: [PATCH 127/189] tests/adapters/crate: Migrate from `mirage` to `@crates-io/msw` --- tests/adapters/crate-test.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/adapters/crate-test.js b/tests/adapters/crate-test.js index 2dcf265ce20..86e84e97d13 100644 --- a/tests/adapters/crate-test.js +++ b/tests/adapters/crate-test.js @@ -1,20 +1,24 @@ import { module, test } from 'qunit'; -import { setupMirage, setupTest } from 'crates-io/tests/helpers'; +import { http, HttpResponse } from 'msw'; + +import { setupTest } from 'crates-io/tests/helpers'; +import setupMsw from 'crates-io/tests/helpers/setup-msw'; module('Adapter | crate', function (hooks) { setupTest(hooks); - setupMirage(hooks); + setupMsw(hooks); test('findRecord requests are coalesced', async function (assert) { - let _foo = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate: _foo }); - let _bar = this.server.create('crate', { name: 'bar' }); - this.server.create('version', { crate: _bar }); + let _foo = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate: _foo }); + let _bar = this.db.crate.create({ name: 'bar' }); + this.db.version.create({ crate: _bar }); // if request coalescing works correctly, then this regular API endpoint // should not be hit in this case - this.server.get('/api/v1/crates/:crate_name', {}, 500); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.get('/api/v1/crates/:crate_name', () => error)); let store = this.owner.lookup('service:store'); @@ -24,8 +28,8 @@ module('Adapter | crate', function (hooks) { }); test('findRecord requests do not include versions by default', async function (assert) { - let _foo = this.server.create('crate', { name: 'foo' }); - let version = this.server.create('version', { crate: _foo }); + let _foo = this.db.crate.create({ name: 'foo' }); + let version = this.db.version.create({ crate: _foo }); let store = this.owner.lookup('service:store'); @@ -37,6 +41,6 @@ module('Adapter | crate', function (hooks) { assert.deepEqual(versionsRef.ids(), []); await versionsRef.load(); - assert.deepEqual(versionsRef.ids(), [version.id]); + assert.deepEqual(versionsRef.ids(), [`${version.id}`]); }); }); From c2474e9a41ab78b742244d50f3a2311d25bc2f4b Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:54:17 +0100 Subject: [PATCH 128/189] tests/components/crate-sidebar/playground-button: Migrate from `mirage` to `@crates-io/msw` --- .../crate-sidebar/playground-button-test.js | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/tests/components/crate-sidebar/playground-button-test.js b/tests/components/crate-sidebar/playground-button-test.js index 48f7207d30d..fe161e985d8 100644 --- a/tests/components/crate-sidebar/playground-button-test.js +++ b/tests/components/crate-sidebar/playground-button-test.js @@ -4,14 +4,14 @@ import { module, test } from 'qunit'; import { defer } from 'rsvp'; import { hbs } from 'ember-cli-htmlbars'; +import { http, HttpResponse } from 'msw'; import { setupRenderingTest } from 'crates-io/tests/helpers'; - -import setupMirage from '../../helpers/setup-mirage'; +import setupMsw from 'crates-io/tests/helpers/setup-msw'; module('Component | CrateSidebar | Playground Button', function (hooks) { setupRenderingTest(hooks); - setupMirage(hooks); + setupMsw(hooks); hooks.beforeEach(function () { let crates = [ @@ -24,12 +24,13 @@ module('Component | CrateSidebar | Playground Button', function (hooks) { { name: 'ansi_term', version: '0.11.0', id: 'ansi_term_0_11_0' }, ]; - this.server.get('https://play.rust-lang.org/meta/crates', { crates }); + let response = HttpResponse.json({ crates }); + this.worker.use(http.get('https://play.rust-lang.org/meta/crates', () => response)); }); test('button is hidden for unavailable crates', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0' }); let store = this.owner.lookup('service:store'); this.crate = await store.findRecord('crate', crate.name); @@ -41,8 +42,8 @@ module('Component | CrateSidebar | Playground Button', function (hooks) { }); test('button is visible for available crates', async function (assert) { - let crate = this.server.create('crate', { name: 'aho-corasick' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'aho-corasick' }); + this.db.version.create({ crate, num: '1.0.0' }); let store = this.owner.lookup('service:store'); this.crate = await store.findRecord('crate', crate.name); @@ -57,11 +58,11 @@ module('Component | CrateSidebar | Playground Button', function (hooks) { }); test('button is hidden while Playground request is pending', async function (assert) { - let crate = this.server.create('crate', { name: 'aho-corasick' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'aho-corasick' }); + this.db.version.create({ crate, num: '1.0.0' }); let deferred = defer(); - this.server.get('https://play.rust-lang.org/meta/crates', deferred.promise); + this.worker.use(http.get('https://play.rust-lang.org/meta/crates', () => deferred.promise)); let store = this.owner.lookup('service:store'); this.crate = await store.findRecord('crate', crate.name); @@ -72,15 +73,16 @@ module('Component | CrateSidebar | Playground Button', function (hooks) { await waitFor('[data-test-owners]'); assert.dom('[data-test-playground-button]').doesNotExist(); - deferred.resolve({ crates: [] }); + deferred.resolve(); await settled(); }); test('button is hidden if the Playground request fails', async function (assert) { - let crate = this.server.create('crate', { name: 'aho-corasick' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'aho-corasick' }); + this.db.version.create({ crate, num: '1.0.0' }); - this.server.get('https://play.rust-lang.org/meta/crates', {}, 500); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.get('https://play.rust-lang.org/meta/crates', () => error)); let store = this.owner.lookup('service:store'); this.crate = await store.findRecord('crate', crate.name); From 03dd0feedd4a6b58216ff9db3ed7ca2edf8f8b28 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:55:13 +0100 Subject: [PATCH 129/189] tests/components/crate-sidebar/toml-snippet: Migrate from `mirage` to `@crates-io/msw` --- .../crate-sidebar/toml-snippet-test.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/components/crate-sidebar/toml-snippet-test.js b/tests/components/crate-sidebar/toml-snippet-test.js index 33cb47ae654..cf735968887 100644 --- a/tests/components/crate-sidebar/toml-snippet-test.js +++ b/tests/components/crate-sidebar/toml-snippet-test.js @@ -4,16 +4,15 @@ import { module, test } from 'qunit'; import { hbs } from 'ember-cli-htmlbars'; import { setupRenderingTest } from 'crates-io/tests/helpers'; - -import setupMirage from '../../helpers/setup-mirage'; +import setupMsw from 'crates-io/tests/helpers/setup-msw'; module('Component | CrateSidebar | toml snippet', function (hooks) { setupRenderingTest(hooks); - setupMirage(hooks); + setupMsw(hooks); test('show version number with `=` prefix', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0' }); let store = this.owner.lookup('service:store'); this.crate = await store.findRecord('crate', crate.name); @@ -30,8 +29,8 @@ module('Component | CrateSidebar | toml snippet', function (hooks) { }); test('show version number without build metadata', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0+abcdef' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0+abcdef' }); let store = this.owner.lookup('service:store'); this.crate = await store.findRecord('crate', crate.name); @@ -43,8 +42,8 @@ module('Component | CrateSidebar | toml snippet', function (hooks) { }); test('show pre-release version number without build', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0-alpha+abcdef' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0-alpha+abcdef' }); let store = this.owner.lookup('service:store'); this.crate = await store.findRecord('crate', crate.name); From 088f84134249d3e31e65769271c15d07ef59ddd5 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:56:03 +0100 Subject: [PATCH 130/189] tests/components/crate-row: Migrate from `mirage` to `@crates-io/msw` --- tests/components/crate-row-test.js | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/tests/components/crate-row-test.js b/tests/components/crate-row-test.js index ef152ea7c7d..a70d66dc328 100644 --- a/tests/components/crate-row-test.js +++ b/tests/components/crate-row-test.js @@ -4,19 +4,18 @@ import { module, test } from 'qunit'; import { hbs } from 'ember-cli-htmlbars'; import { setupRenderingTest } from 'crates-io/tests/helpers'; - -import setupMirage from '../helpers/setup-mirage'; +import setupMsw from 'crates-io/tests/helpers/setup-msw'; module('Component | CrateRow', function (hooks) { setupRenderingTest(hooks); - setupMirage(hooks); + setupMsw(hooks); test('shows crate name and highest stable version', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.2.3', yanked: true }); - this.server.create('version', { crate, num: '2.0.0-beta.1' }); - this.server.create('version', { crate, num: '1.1.2' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0' }); + this.db.version.create({ crate, num: '1.2.3', yanked: true }); + this.db.version.create({ crate, num: '2.0.0-beta.1' }); + this.db.version.create({ crate, num: '1.1.2' }); let store = this.owner.lookup('service:store'); this.crate = await store.findRecord('crate', crate.name); @@ -28,10 +27,10 @@ module('Component | CrateRow', function (hooks) { }); test('shows crate name and highest version, if there is no stable version available', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0-beta.1' }); - this.server.create('version', { crate, num: '1.0.0-beta.3' }); - this.server.create('version', { crate, num: '1.0.0-beta.2' }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0-beta.1' }); + this.db.version.create({ crate, num: '1.0.0-beta.3' }); + this.db.version.create({ crate, num: '1.0.0-beta.2' }); let store = this.owner.lookup('service:store'); this.crate = await store.findRecord('crate', crate.name); @@ -43,9 +42,9 @@ module('Component | CrateRow', function (hooks) { }); test('shows crate name and no version if all versions are yanked', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '1.0.0', yanked: true }); - this.server.create('version', { crate, num: '1.2.3', yanked: true }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '1.0.0', yanked: true }); + this.db.version.create({ crate, num: '1.2.3', yanked: true }); let store = this.owner.lookup('service:store'); this.crate = await store.findRecord('crate', crate.name); From 2372611a287570bfa7d48eefd6b06bc5c0896e5e Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:58:54 +0100 Subject: [PATCH 131/189] tests/components/owners-list: Migrate from `mirage` to `@crates-io/msw` --- tests/components/owners-list-test.js | 49 ++++++++++++++-------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/tests/components/owners-list-test.js b/tests/components/owners-list-test.js index 18a0c9afe65..5e44df4cd74 100644 --- a/tests/components/owners-list-test.js +++ b/tests/components/owners-list-test.js @@ -4,19 +4,18 @@ import { module, test } from 'qunit'; import { hbs } from 'ember-cli-htmlbars'; import { setupRenderingTest } from 'crates-io/tests/helpers'; - -import setupMirage from '../helpers/setup-mirage'; +import setupMsw from 'crates-io/tests/helpers/setup-msw'; module('Component | OwnersList', function (hooks) { setupRenderingTest(hooks); - setupMirage(hooks); + setupMsw(hooks); test('single user', async function (assert) { - let crate = this.server.create('crate'); - this.server.create('version', { crate }); + let crate = this.db.crate.create(); + this.db.version.create({ crate }); - let user = this.server.create('user'); - this.server.create('crate-ownership', { crate, user }); + let user = this.db.user.create(); + this.db.crateOwnership.create({ crate, user }); let store = this.owner.lookup('service:store'); this.crate = await store.findRecord('crate', crate.name); @@ -35,11 +34,11 @@ module('Component | OwnersList', function (hooks) { }); test('user without `name`', async function (assert) { - let crate = this.server.create('crate'); - this.server.create('version', { crate }); + let crate = this.db.crate.create(); + this.db.version.create({ crate }); - let user = this.server.create('user', { name: null, login: 'anonymous' }); - this.server.create('crate-ownership', { crate, user }); + let user = this.db.user.create({ name: null, login: 'anonymous' }); + this.db.crateOwnership.create({ crate, user }); let store = this.owner.lookup('service:store'); this.crate = await store.findRecord('crate', crate.name); @@ -58,12 +57,12 @@ module('Component | OwnersList', function (hooks) { }); test('five users', async function (assert) { - let crate = this.server.create('crate'); - this.server.create('version', { crate }); + let crate = this.db.crate.create(); + this.db.version.create({ crate }); for (let i = 0; i < 5; i++) { - let user = this.server.create('user'); - this.server.create('crate-ownership', { crate, user }); + let user = this.db.user.create(); + this.db.crateOwnership.create({ crate, user }); } let store = this.owner.lookup('service:store'); @@ -80,12 +79,12 @@ module('Component | OwnersList', function (hooks) { }); test('six users', async function (assert) { - let crate = this.server.create('crate'); - this.server.create('version', { crate }); + let crate = this.db.crate.create(); + this.db.version.create({ crate }); for (let i = 0; i < 6; i++) { - let user = this.server.create('user'); - this.server.create('crate-ownership', { crate, user }); + let user = this.db.user.create(); + this.db.crateOwnership.create({ crate, user }); } let store = this.owner.lookup('service:store'); @@ -102,16 +101,16 @@ module('Component | OwnersList', function (hooks) { }); test('teams mixed with users', async function (assert) { - let crate = this.server.create('crate'); - this.server.create('version', { crate }); + let crate = this.db.crate.create(); + this.db.version.create({ crate }); for (let i = 0; i < 3; i++) { - let user = this.server.create('user'); - this.server.create('crate-ownership', { crate, user }); + let user = this.db.user.create(); + this.db.crateOwnership.create({ crate, user }); } for (let i = 0; i < 2; i++) { - let team = this.server.create('team', { org: 'crates-io' }); - this.server.create('crate-ownership', { crate, team }); + let team = this.db.team.create({ org: 'crates-io' }); + this.db.crateOwnership.create({ crate, team }); } let store = this.owner.lookup('service:store'); From 4b420e4aa7251dc32913b8807a54300293383ada Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 16:59:59 +0100 Subject: [PATCH 132/189] tests/components/privileged-action: Migrate from `mirage` to `@crates-io/msw` --- tests/components/privileged-action-test.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/components/privileged-action-test.js b/tests/components/privileged-action-test.js index 38d2d7d49fd..dc13865dacf 100644 --- a/tests/components/privileged-action-test.js +++ b/tests/components/privileged-action-test.js @@ -4,12 +4,11 @@ import { module, test } from 'qunit'; import { hbs } from 'ember-cli-htmlbars'; import { setupRenderingTest } from 'crates-io/tests/helpers'; - -import setupMirage from '../helpers/setup-mirage'; +import setupMsw from 'crates-io/tests/helpers/setup-msw'; module('Component | PrivilegedAction', hooks => { setupRenderingTest(hooks); - setupMirage(hooks); + setupMsw(hooks); hooks.beforeEach(function () { // Adds a utility function that renders a PrivilegedAction with all the @@ -34,7 +33,7 @@ module('Component | PrivilegedAction', hooks => { }); test('unprivileged block is shown to a logged in user without access', async function (assert) { - const user = this.server.create('user'); + const user = this.db.user.create(); this.authenticateAs(user); await this.renderComponent(false); @@ -44,7 +43,7 @@ module('Component | PrivilegedAction', hooks => { }); test('privileged block is shown to a logged in user with access', async function (assert) { - const user = this.server.create('user'); + const user = this.db.user.create(); this.authenticateAs(user); await this.renderComponent(true); @@ -54,7 +53,7 @@ module('Component | PrivilegedAction', hooks => { }); test('placeholder block is shown to a logged in admin without sudo', async function (assert) { - const user = this.server.create('user', { isAdmin: true }); + const user = this.db.user.create({ isAdmin: true }); this.authenticateAs(user); const session = this.owner.lookup('service:session'); @@ -69,7 +68,7 @@ module('Component | PrivilegedAction', hooks => { }); test('privileged block is shown to a logged in admin without sudo with access', async function (assert) { - const user = this.server.create('user', { isAdmin: true }); + const user = this.db.user.create({ isAdmin: true }); this.authenticateAs(user); const session = this.owner.lookup('service:session'); @@ -84,7 +83,7 @@ module('Component | PrivilegedAction', hooks => { }); test('privileged block is shown to a logged in admin with sudo', async function (assert) { - const user = this.server.create('user', { isAdmin: true }); + const user = this.db.user.create({ isAdmin: true }); this.authenticateAs(user); const session = this.owner.lookup('service:session'); @@ -100,7 +99,7 @@ module('Component | PrivilegedAction', hooks => { }); test('automatic placeholder block', async function (assert) { - const user = this.server.create('user', { isAdmin: true }); + const user = this.db.user.create({ isAdmin: true }); this.authenticateAs(user); const session = this.owner.lookup('service:session'); From 7bafdc51004fa7bfadb33ed259dadb4c22ee3545 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 17:05:31 +0100 Subject: [PATCH 133/189] tests/components/version-list-row: Migrate from `mirage` to `@crates-io/msw` --- tests/components/version-list-row-test.js | 33 +++++++++++------------ 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/tests/components/version-list-row-test.js b/tests/components/version-list-row-test.js index e651767cc1a..384a9a43f3c 100644 --- a/tests/components/version-list-row-test.js +++ b/tests/components/version-list-row-test.js @@ -4,24 +4,23 @@ import { module, test } from 'qunit'; import { hbs } from 'ember-cli-htmlbars'; import { setupRenderingTest } from 'crates-io/tests/helpers'; - -import setupMirage from '../helpers/setup-mirage'; +import setupMsw from 'crates-io/tests/helpers/setup-msw'; module('Component | VersionList::Row', function (hooks) { setupRenderingTest(hooks); - setupMirage(hooks); + setupMsw(hooks); test('handle non-standard semver strings', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate, num: '0.4.0-alpha.01', created_at: Date.now(), updated_at: Date.now() }); - this.server.create('version', { crate, num: '0.3.0-alpha.01', created_at: Date.now(), updated_at: Date.now() }); + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '0.4.0-alpha.01', created_at: Date.now(), updated_at: Date.now() }); + this.db.version.create({ crate, num: '0.3.0-alpha.01', created_at: Date.now(), updated_at: Date.now() }); let store = this.owner.lookup('service:store'); let crateRecord = await store.findRecord('crate', crate.name); let versions = (await crateRecord.loadVersionsTask.perform()).slice(); await crateRecord.loadOwnerUserTask.perform(); - this.firstVersion = versions[0]; - this.secondVersion = versions[1]; + this.firstVersion = versions.find(it => it.num === '0.4.0-alpha.01'); + this.secondVersion = versions.find(it => it.num === '0.3.0-alpha.01'); await render(hbs``); assert.dom('[data-test-release-track] svg').exists(); @@ -33,9 +32,9 @@ module('Component | VersionList::Row', function (hooks) { }); test('handle node-semver parsing errors', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); + let crate = this.db.crate.create({ name: 'foo' }); let version = '18446744073709551615.18446744073709551615.18446744073709551615'; - this.server.create('version', { crate, num: version }); + this.db.version.create({ crate, num: version }); let store = this.owner.lookup('service:store'); let crateRecord = await store.findRecord('crate', crate.name); @@ -48,22 +47,22 @@ module('Component | VersionList::Row', function (hooks) { }); test('pluralize "feature" only when appropriate', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '0.1.0', features: {}, created_at: Date.now(), updated_at: Date.now(), }); - this.server.create('version', { + this.db.version.create({ crate, num: '0.2.0', features: { one: [] }, created_at: Date.now(), updated_at: Date.now(), }); - this.server.create('version', { + this.db.version.create({ crate, num: '0.3.0', features: { one: [], two: [] }, @@ -75,9 +74,9 @@ module('Component | VersionList::Row', function (hooks) { let crateRecord = await store.findRecord('crate', crate.name); let versions = (await crateRecord.loadVersionsTask.perform()).slice(); await crateRecord.loadOwnerUserTask.perform(); - this.firstVersion = versions[0]; - this.secondVersion = versions[1]; - this.thirdVersion = versions[2]; + this.firstVersion = versions.find(it => it.num === '0.1.0'); + this.secondVersion = versions.find(it => it.num === '0.2.0'); + this.thirdVersion = versions.find(it => it.num === '0.3.0'); await render(hbs``); assert.dom('[data-test-feature-list]').doesNotExist(); From d3b28976b8a809bd32da10a2c4215bf5dbac17b1 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 17:09:31 +0100 Subject: [PATCH 134/189] tests/models/crate: Migrate from `mirage` to `@crates-io/msw` --- tests/models/crate-test.js | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/tests/models/crate-test.js b/tests/models/crate-test.js index d722c12d9cd..bb2e0a8dfde 100644 --- a/tests/models/crate-test.js +++ b/tests/models/crate-test.js @@ -2,11 +2,12 @@ import { module, test } from 'qunit'; import AdapterError from '@ember-data/adapter/error'; -import { setupMirage, setupTest } from 'crates-io/tests/helpers'; +import { setupTest } from 'crates-io/tests/helpers'; +import setupMsw from 'crates-io/tests/helpers/setup-msw'; module('Model | Crate', function (hooks) { setupTest(hooks); - setupMirage(hooks); + setupMsw(hooks); hooks.beforeEach(function () { this.store = this.owner.lookup('service:store'); @@ -14,26 +15,26 @@ module('Model | Crate', function (hooks) { module('inviteOwner()', function () { test('happy path', async function (assert) { - let user = this.server.create('user'); + let user = this.db.user.create(); this.authenticateAs(user); - let crate = this.server.create('crate'); - this.server.create('version', { crate }); + let crate = this.db.crate.create(); + this.db.version.create({ crate }); - let user2 = this.server.create('user'); + let user2 = this.db.user.create(); let crateRecord = await this.store.findRecord('crate', crate.name); let result = await crateRecord.inviteOwner(user2.login); - assert.deepEqual(result, { ok: true, msg: 'user user-2 has been invited to be an owner of crate crate-0' }); + assert.deepEqual(result, { ok: true, msg: 'user user-2 has been invited to be an owner of crate crate-1' }); }); test('error handling', async function (assert) { - let user = this.server.create('user'); + let user = this.db.user.create(); this.authenticateAs(user); - let crate = this.server.create('crate'); - this.server.create('version', { crate }); + let crate = this.db.crate.create(); + this.db.version.create({ crate }); let crateRecord = await this.store.findRecord('crate', crate.name); @@ -46,13 +47,14 @@ module('Model | Crate', function (hooks) { module('removeOwner()', function () { test('happy path', async function (assert) { - let user = this.server.create('user'); + let user = this.db.user.create(); this.authenticateAs(user); - let crate = this.server.create('crate'); - this.server.create('version', { crate }); + let crate = this.db.crate.create(); + this.db.version.create({ crate }); - let user2 = this.server.create('user'); + let user2 = this.db.user.create(); + this.db.crateOwnership.create({ crate, user: user2 }); let crateRecord = await this.store.findRecord('crate', crate.name); @@ -61,11 +63,11 @@ module('Model | Crate', function (hooks) { }); test('error handling', async function (assert) { - let user = this.server.create('user'); + let user = this.db.user.create(); this.authenticateAs(user); - let crate = this.server.create('crate'); - this.server.create('version', { crate }); + let crate = this.db.crate.create(); + this.db.version.create({ crate }); let crateRecord = await this.store.findRecord('crate', crate.name); From 9fb9eb26dcb888ed0940101221d24da291dc6e71 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 17:11:58 +0100 Subject: [PATCH 135/189] tests/models/user: Migrate from `mirage` to `@crates-io/msw` --- tests/models/user-test.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/models/user-test.js b/tests/models/user-test.js index 22d25b054bc..be4bd853ee7 100644 --- a/tests/models/user-test.js +++ b/tests/models/user-test.js @@ -1,12 +1,13 @@ import { module, test } from 'qunit'; -import { setupTest } from 'crates-io/tests/helpers'; +import { http, HttpResponse } from 'msw'; -import setupMirage from '../helpers/setup-mirage'; +import { setupTest } from 'crates-io/tests/helpers'; +import setupMsw from 'crates-io/tests/helpers/setup-msw'; module('Model | User', function (hooks) { setupTest(hooks); - setupMirage(hooks); + setupMsw(hooks); hooks.beforeEach(function () { this.store = this.owner.lookup('service:store'); @@ -14,7 +15,7 @@ module('Model | User', function (hooks) { module('changeEmail()', function () { test('happy path', async function (assert) { - let user = this.server.create('user', { email: 'old@email.com' }); + let user = this.db.user.create({ email: 'old@email.com' }); this.authenticateAs(user); @@ -30,11 +31,12 @@ module('Model | User', function (hooks) { }); test('error handling', async function (assert) { - let user = this.server.create('user', { email: 'old@email.com' }); + let user = this.db.user.create({ email: 'old@email.com' }); this.authenticateAs(user); - this.server.put('/api/v1/users/:user_id', {}, 500); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.put('/api/v1/users/:user_id', () => error)); let { currentUser } = await this.owner.lookup('service:session').loadUserTask.perform(); @@ -55,7 +57,7 @@ module('Model | User', function (hooks) { test('happy path', async function (assert) { assert.expect(0); - let user = this.server.create('user', { emailVerificationToken: 'secret123' }); + let user = this.db.user.create({ emailVerificationToken: 'secret123' }); this.authenticateAs(user); let { currentUser } = await this.owner.lookup('service:session').loadUserTask.perform(); @@ -64,10 +66,11 @@ module('Model | User', function (hooks) { }); test('error handling', async function (assert) { - let user = this.server.create('user', { emailVerificationToken: 'secret123' }); + let user = this.db.user.create({ emailVerificationToken: 'secret123' }); this.authenticateAs(user); - this.server.put('/api/v1/users/:user_id/resend', {}, 500); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.put('/api/v1/users/:user_id/resend', () => error)); let { currentUser } = await this.owner.lookup('service:session').loadUserTask.perform(); From a87766be1d803d9f277bf170d3eeed3b19aa1dd6 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 17:24:30 +0100 Subject: [PATCH 136/189] tests/models/version: Migrate from `mirage` to `@crates-io/msw` --- tests/models/version-test.js | 69 ++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/tests/models/version-test.js b/tests/models/version-test.js index fb92b98ff6f..6e799d5f18f 100644 --- a/tests/models/version-test.js +++ b/tests/models/version-test.js @@ -1,20 +1,21 @@ import { module, test } from 'qunit'; -import { setupMirage, setupTest } from 'crates-io/tests/helpers'; +import { setupTest } from 'crates-io/tests/helpers'; +import setupMsw from 'crates-io/tests/helpers/setup-msw'; module('Model | Version', function (hooks) { setupTest(hooks); - setupMirage(hooks); + setupMsw(hooks); hooks.beforeEach(function () { this.store = this.owner.lookup('service:store'); }); test('isNew', async function (assert) { - let { server, store } = this; + let { db, store } = this; - let crate = server.create('crate'); - server.create('version', { crate, created_at: '2010-06-16T21:30:45Z' }); + let crate = db.crate.create(); + db.version.create({ crate, created_at: '2010-06-16T21:30:45Z' }); let crateRecord = await store.findRecord('crate', crate.name); let versions = (await crateRecord.versions).slice(); @@ -69,10 +70,10 @@ module('Model | Version', function (hooks) { module('semver', function () { async function prepare(context, { num }) { - let { server, store } = context; + let { db, store } = context; - let crate = server.create('crate'); - server.create('version', { crate, num }); + let crate = db.crate.create(); + db.version.create({ crate, num }); let crateRecord = await store.findRecord('crate', crate.name); let versions = (await crateRecord.versions).slice(); @@ -161,9 +162,9 @@ module('Model | Version', function (hooks) { '0.1.1', ]; - let crate = this.server.create('crate'); - for (let num of nums) { - this.server.create('version', { crate, num }); + let crate = this.db.crate.create(); + for (let num of nums.toReversed()) { + this.db.version.create({ crate, num }); } let crateRecord = await this.store.findRecord('crate', crate.name); @@ -192,10 +193,10 @@ module('Model | Version', function (hooks) { }); test('ignores yanked versions', async function (assert) { - let crate = this.server.create('crate'); - this.server.create('version', { crate, num: '0.4.0' }); - this.server.create('version', { crate, num: '0.4.1' }); - this.server.create('version', { crate, num: '0.4.2', yanked: true }); + let crate = this.db.crate.create(); + this.db.version.create({ crate, num: '0.4.0' }); + this.db.version.create({ crate, num: '0.4.1' }); + this.db.version.create({ crate, num: '0.4.2', yanked: true }); let crateRecord = await this.store.findRecord('crate', crate.name); let versions = (await crateRecord.loadVersionsTask.perform()).slice(); @@ -203,17 +204,17 @@ module('Model | Version', function (hooks) { assert.deepEqual( versions.map(it => ({ num: it.num, isHighestOfReleaseTrack: it.isHighestOfReleaseTrack })), [ - { num: '0.4.0', isHighestOfReleaseTrack: false }, - { num: '0.4.1', isHighestOfReleaseTrack: true }, { num: '0.4.2', isHighestOfReleaseTrack: false }, + { num: '0.4.1', isHighestOfReleaseTrack: true }, + { num: '0.4.0', isHighestOfReleaseTrack: false }, ], ); }); test('handles newly released versions correctly', async function (assert) { - let crate = this.server.create('crate'); - this.server.create('version', { crate, num: '0.4.0' }); - this.server.create('version', { crate, num: '0.4.1' }); + let crate = this.db.crate.create(); + this.db.version.create({ crate, num: '0.4.0' }); + this.db.version.create({ crate, num: '0.4.1' }); let crateRecord = await this.store.findRecord('crate', crate.name); let versions = (await crateRecord.loadVersionsTask.perform()).slice(); @@ -221,23 +222,23 @@ module('Model | Version', function (hooks) { assert.deepEqual( versions.map(it => ({ num: it.num, isHighestOfReleaseTrack: it.isHighestOfReleaseTrack })), [ - { num: '0.4.0', isHighestOfReleaseTrack: false }, { num: '0.4.1', isHighestOfReleaseTrack: true }, + { num: '0.4.0', isHighestOfReleaseTrack: false }, ], ); - this.server.create('version', { crate, num: '0.4.2' }); - this.server.create('version', { crate, num: '0.4.3', yanked: true }); + this.db.version.create({ crate, num: '0.4.2' }); + this.db.version.create({ crate, num: '0.4.3', yanked: true }); crateRecord = await this.store.findRecord('crate', crate.name, { reload: true }); versions = (await crateRecord.loadVersionsTask.perform({ reload: true })).slice(); assert.deepEqual( versions.map(it => ({ num: it.num, isHighestOfReleaseTrack: it.isHighestOfReleaseTrack })), [ - { num: '0.4.0', isHighestOfReleaseTrack: false }, - { num: '0.4.1', isHighestOfReleaseTrack: false }, - { num: '0.4.2', isHighestOfReleaseTrack: true }, { num: '0.4.3', isHighestOfReleaseTrack: false }, + { num: '0.4.2', isHighestOfReleaseTrack: true }, + { num: '0.4.1', isHighestOfReleaseTrack: false }, + { num: '0.4.0', isHighestOfReleaseTrack: false }, ], ); }); @@ -245,10 +246,10 @@ module('Model | Version', function (hooks) { module('featuresList', function () { async function prepare(context, { features }) { - let { server, store } = context; + let { db, store } = context; - let crate = server.create('crate'); - server.create('version', { crate, features }); + let crate = db.crate.create(); + db.version.create({ crate, features }); let crateRecord = await store.findRecord('crate', crate.name); let versions = (await crateRecord.versions).slice(); @@ -260,8 +261,8 @@ module('Model | Version', function (hooks) { assert.deepEqual(version.featureList, []); }); - test('`features: null` results in empty list', async function (assert) { - let version = await prepare(this, { features: null }); + test('`features: undefined` results in empty list', async function (assert) { + let version = await prepare(this, { features: undefined }); assert.deepEqual(version.featureList, []); }); @@ -325,10 +326,10 @@ module('Model | Version', function (hooks) { }); test('`published_by` relationship is assigned correctly', async function (assert) { - let user = this.server.create('user', { name: 'JD' }); + let user = this.db.user.create({ name: 'JD' }); - let crate = this.server.create('crate'); - this.server.create('version', { crate, publishedBy: user }); + let crate = this.db.crate.create(); + this.db.version.create({ crate, publishedBy: user }); let crateRecord = await this.store.findRecord('crate', crate.name); assert.ok(crateRecord); From 9d1e84e5a5bba8b7316630f83fcf4c8b451aaa13 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 17:24:38 +0100 Subject: [PATCH 137/189] tests/services/playground: Migrate from `mirage` to `@crates-io/msw` --- tests/services/playground-test.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/services/playground-test.js b/tests/services/playground-test.js index bba9e018bad..1eece84ae82 100644 --- a/tests/services/playground-test.js +++ b/tests/services/playground-test.js @@ -1,10 +1,13 @@ import { module, test } from 'qunit'; -import { setupMirage, setupTest } from 'crates-io/tests/helpers'; +import { http, HttpResponse } from 'msw'; + +import { setupTest } from 'crates-io/tests/helpers'; +import setupMsw from 'crates-io/tests/helpers/setup-msw'; module('Service | Playground', function (hooks) { setupTest(hooks); - setupMirage(hooks); + setupMsw(hooks); hooks.beforeEach(function () { this.playground = this.owner.lookup('service:playground'); @@ -21,14 +24,16 @@ module('Service | Playground', function (hooks) { { name: 'ansi_term', version: '0.11.0', id: 'ansi_term_0_11_0' }, ]; - this.server.get('https://play.rust-lang.org/meta/crates', { crates }, 200); + let response = HttpResponse.json({ crates }); + this.worker.use(http.get('https://play.rust-lang.org/meta/crates', () => response)); await this.playground.loadCratesTask.perform(); assert.deepEqual(this.playground.crates, crates); }); test('loadCratesTask fails on HTTP error', async function (assert) { - this.server.get('https://play.rust-lang.org/meta/crates', {}, 500); + let error = HttpResponse.json({}, { status: 500 }); + this.worker.use(http.get('https://play.rust-lang.org/meta/crates', () => error)); await assert.rejects(this.playground.loadCratesTask.perform()); assert.notOk(this.playground.crates); From 7e62faea6909cccb592566bd496ad05dd357f625 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 24 Jan 2025 17:28:45 +0100 Subject: [PATCH 138/189] tests/utils/ajax: Migrate from `mirage` to `@crates-io/msw` --- tests/utils/ajax-test.js | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/tests/utils/ajax-test.js b/tests/utils/ajax-test.js index 89d9447b74c..5c2efebd7de 100644 --- a/tests/utils/ajax-test.js +++ b/tests/utils/ajax-test.js @@ -1,32 +1,35 @@ import { module, test } from 'qunit'; +import { http, HttpResponse } from 'msw'; + import { setupTest } from 'crates-io/tests/helpers'; +import setupMsw from 'crates-io/tests/helpers/setup-msw'; import ajax, { AjaxError, HttpError } from 'crates-io/utils/ajax'; -import setupMirage from '../helpers/setup-mirage'; - module('ajax()', function (hooks) { setupTest(hooks); - setupMirage(hooks); + setupMsw(hooks); setupFetchRestore(hooks); - test('fetches a JSON document from the server', async function (assert) { - this.server.get('/foo', { foo: 42 }); + test('fetches a JSON document from the worker', async function (assert) { + this.worker.use(http.get('/foo', () => HttpResponse.json({ foo: 42 }))); let response = await ajax('/foo'); assert.deepEqual(response, { foo: 42 }); }); test('passes additional options to `fetch()`', async function (assert) { - this.server.get('/foo', { foo: 42 }); - this.server.put('/foo', { foo: 'bar' }); + this.worker.use( + http.get('/foo', () => HttpResponse.json({ foo: 42 })), + http.put('/foo', () => HttpResponse.json({ foo: 'bar' })), + ); let response = await ajax('/foo', { method: 'PUT' }); assert.deepEqual(response, { foo: 'bar' }); }); test('throws an `HttpError` for 5xx responses', async function (assert) { - this.server.get('/foo', { foo: 42 }, 500); + this.worker.use(http.get('/foo', () => HttpResponse.json({ foo: 42 }, { status: 500 }))); await assert.rejects(ajax('/foo'), function (error) { let expectedMessage = 'GET /foo failed\n\ncaused by: HttpError: GET /foo failed with: 500 Internal Server Error'; @@ -50,13 +53,13 @@ module('ajax()', function (hooks) { assert.strictEqual(cause.method, 'GET'); assert.strictEqual(cause.url, '/foo'); assert.ok(cause.response); - assert.strictEqual(cause.response.url, '/foo'); + assert.ok(cause.response.url.endsWith('/foo')); return true; }); }); test('throws an `HttpError` for 4xx responses', async function (assert) { - this.server.get('/foo', { foo: 42 }, 404); + this.worker.use(http.get('/foo', () => HttpResponse.json({ foo: 42 }, { status: 404 }))); await assert.rejects(ajax('/foo'), function (error) { let expectedMessage = 'GET /foo failed\n\ncaused by: HttpError: GET /foo failed with: 404 Not Found'; @@ -80,13 +83,13 @@ module('ajax()', function (hooks) { assert.strictEqual(cause.method, 'GET'); assert.strictEqual(cause.url, '/foo'); assert.ok(cause.response); - assert.strictEqual(cause.response.url, '/foo'); + assert.ok(cause.response.url.endsWith('/foo')); return true; }); }); test('throws an error for invalid JSON responses', async function (assert) { - this.server.get('/foo', () => '{ foo: 42'); + this.worker.use(http.get('/foo', () => HttpResponse.text('{ foo: 42'))); await assert.rejects(ajax('/foo'), function (error) { let expectedMessage = 'GET /foo failed\n\ncaused by: SyntaxError'; @@ -150,7 +153,7 @@ module('ajax()', function (hooks) { module('json()', function () { test('resolves with the JSON payload', async function (assert) { - this.server.get('/foo', { foo: 42 }, 500); + this.worker.use(http.get('/foo', () => HttpResponse.json({ foo: 42 }, { status: 500 }))); let error; await assert.rejects(ajax('/foo'), function (_error) { @@ -163,7 +166,7 @@ module('ajax()', function (hooks) { }); test('resolves with `undefined` if there is no JSON payload', async function (assert) { - this.server.get('/foo', () => '{ foo: 42', 500); + this.worker.use(http.get('/foo', () => HttpResponse.text('{ foo: 42', { status: 500 }))); let error; await assert.rejects(ajax('/foo'), function (_error) { From 3685a95d152448d2e7c831047b5f7e1b5d7a2109 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 28 Jan 2025 14:06:59 +0100 Subject: [PATCH 139/189] CI: Run `@crates-io/msw` tests in dedicated job --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e59649c7c37..cd519e8aa05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -233,14 +233,36 @@ jobs: - run: pnpm install - - run: pnpm --filter "@crates-io/msw" test - - if: github.repository == 'rust-lang/crates.io' run: pnpm percy exec --parallel -- pnpm test-coverage - if: github.repository != 'rust-lang/crates.io' run: pnpm test-coverage + msw-test: + name: Frontend / Test (@crates-io/msw) + runs-on: ubuntu-24.04 + needs: [changed-files] + if: needs.changed-files.outputs.non-rust == 'true' + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 + with: + version: ${{ env.PNPM_VERSION }} + + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + cache: pnpm + node-version-file: package.json + + - run: pnpm install + + - run: pnpm --filter "@crates-io/msw" test + e2e-test: name: Frontend / Test (playwright) runs-on: ubuntu-24.04 From a6e4c9a92ccc5a4442fdcc8c56302de7290a4bf3 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 12:02:41 +0100 Subject: [PATCH 140/189] mirage: Start `ember-cli-mirage` server only if `window.startMirage` is set This will allow us to selectively enable Mirage only for some tests in the playwright test suite --- app/initializers/ember-cli-mirage.js | 36 ++++++++++++++++++++++++++++ e2e/fixtures/mirage.ts | 1 + 2 files changed, 37 insertions(+) create mode 100644 app/initializers/ember-cli-mirage.js diff --git a/app/initializers/ember-cli-mirage.js b/app/initializers/ember-cli-mirage.js new file mode 100644 index 00000000000..1c88e4e1597 --- /dev/null +++ b/app/initializers/ember-cli-mirage.js @@ -0,0 +1,36 @@ +import { importSync, isDevelopingApp, isTesting, macroCondition } from '@embroider/macros'; + +export default { + name: 'ember-cli-mirage', + initialize(application) { + // `macroCondition(isDevelopingApp() || isTesting())` should work as well, + // but it failed to build correctly on CI, so we duplicate it into two + // conditions. Since we will be dropping `ember-cli-mirage` soon anyway, + // this is good enough for now. + if (macroCondition(isDevelopingApp())) { + let startMirage = importSync('ember-cli-mirage/start-mirage').default; + let ENV = importSync('../config/environment').default; + let makeServer = importSync('../mirage/config').default; + + application.register('mirage:make-server', makeServer, { + instantiate: false, + }); + + if (window.startMirage) { + startMirage(application.__container__, { makeServer, env: ENV }); + } + } else if (macroCondition(isTesting())) { + let startMirage = importSync('ember-cli-mirage/start-mirage').default; + let ENV = importSync('../config/environment').default; + let makeServer = importSync('../mirage/config').default; + + application.register('mirage:make-server', makeServer, { + instantiate: false, + }); + + if (window.startMirage) { + startMirage(application.__container__, { makeServer, env: ENV }); + } + } + }, +}; diff --git a/e2e/fixtures/mirage.ts b/e2e/fixtures/mirage.ts index 77dc0b45e4d..2b422bea7e9 100644 --- a/e2e/fixtures/mirage.ts +++ b/e2e/fixtures/mirage.ts @@ -49,6 +49,7 @@ export class MiragePage { } async setup() { + await this.page.addInitScript('window.startMirage = true'); await this.addHelpers(); } } From d366c395113e21cf3016a15f88fe071c1799bf0e Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 12:06:17 +0100 Subject: [PATCH 141/189] playwright: Disable `mirage` fixture from being applied automatically --- e2e/helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/helper.ts b/e2e/helper.ts index 69ac5c04ec8..b09d9e3997c 100644 --- a/e2e/helper.ts +++ b/e2e/helper.ts @@ -37,7 +37,7 @@ export const test = base.extend({ await mirage.setup(); await use(mirage); }, - { auto: true, scope: 'test' }, + { scope: 'test' }, ], ember: [ async ({ page, emberOptions }, use) => { From febc17b887644e5c485eb077df07a1be576bfbbf Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 31 Jan 2025 13:54:44 +0100 Subject: [PATCH 142/189] playwright: Set default `locale` to `en-US` --- playwright.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/playwright.config.ts b/playwright.config.ts index 07416a80bbd..1bad65e0128 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -33,6 +33,8 @@ export default defineConfig({ /* Set a custom test id that is also compatible with `ember-test-selectors` */ testIdAttribute: 'data-test-id', + + locale: 'en-US', }, /* Configure projects for major browsers */ From 55e0873f12ef5e2207e7b5f2f96e12508e8cbe41 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 12:23:41 +0100 Subject: [PATCH 143/189] playwright: Implement basic `defer()` fn --- e2e/deferred.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 e2e/deferred.ts diff --git a/e2e/deferred.ts b/e2e/deferred.ts new file mode 100644 index 00000000000..cf91b8f923e --- /dev/null +++ b/e2e/deferred.ts @@ -0,0 +1,8 @@ +export function defer(): { resolve: (any?) => T; reject: (reason: any) => void; promise: Promise } { + let resolve, reject; + let promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }) as Promise; + return { resolve, reject, promise }; +} From d3dd2352a2cdbb3f5c21b9b78b452c0f58ca0b8e Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 12:24:43 +0100 Subject: [PATCH 144/189] playwright: Implement `msw` fixture This can be used as an alternative to the `mirage` fixture and uses `@crates-io/msw` instead. --- e2e/helper.ts | 25 +++++++++++++++++++++++++ package.json | 1 + pnpm-lock.yaml | 23 +++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/e2e/helper.ts b/e2e/helper.ts index b09d9e3997c..13fb0d9c497 100644 --- a/e2e/helper.ts +++ b/e2e/helper.ts @@ -1,4 +1,9 @@ import { test as base } from '@playwright/test'; +import type { MockServiceWorker } from 'playwright-msw'; +import { http, HttpResponse } from 'msw'; +import { createWorker } from 'playwright-msw'; +import { db, handlers } from '@crates-io/msw'; + import { FakeTimers, FakeTimersOptions } from './fixtures/fake-timers'; import { MiragePage } from './fixtures/mirage'; import { PercyPage } from './fixtures/percy'; @@ -13,6 +18,11 @@ export type AppOptions = { export interface AppFixtures { clock: FakeTimers; mirage: MiragePage; + msw: { + worker: MockServiceWorker; + db: typeof db; + authenticateAs: (user: any) => Promise; + }; ember: EmberPage; percy: PercyPage; a11y: A11yPage; @@ -39,6 +49,21 @@ export const test = base.extend({ }, { scope: 'test' }, ], + // MockServiceWorker integration via `playwright-msw`. + // + // We are explicitly not using the `createWorkerFixture()`function, because + // uses `auto: true`, and we want to be explicit about our usage of the fixture. + msw: async ({ page }, use) => { + const worker = await createWorker(page, handlers); + const authenticateAs = async function (user) { + db.mswSession.create({ user }); + await page.addInitScript("globalThis.localStorage.setItem('isLoggedIn', '1')"); + }; + + await use({ worker, db, authenticateAs }); + db.reset(); + worker.resetCookieStore(); + }, ember: [ async ({ page, emberOptions }, use) => { let ember = new EmberPage(page); diff --git a/package.json b/package.json index 34ecbe8d5ba..aeb50c87c39 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,7 @@ "msw": "2.7.0", "normalize.css": "8.0.1", "nyc": "17.1.0", + "playwright-msw": "3.0.1", "postcss-preset-env": "10.1.3", "prettier": "3.4.2", "qunit": "2.24.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e7d20dd7b4..68b43af7074 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -286,6 +286,9 @@ importers: nyc: specifier: 17.1.0 version: 17.1.0 + playwright-msw: + specifier: 3.0.1 + version: 3.0.1(@playwright/test@1.50.0)(msw@2.7.0(@types/node@22.10.10)(typescript@5.7.3)) postcss-preset-env: specifier: 10.1.3 version: 10.1.3(postcss@8.5.1) @@ -1779,6 +1782,10 @@ packages: resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==} engines: {node: '>=4'} + '@mswjs/cookies@1.1.1': + resolution: {integrity: sha512-W68qOHEjx1iD+4VjQudlx26CPIoxmIAtK4ZCexU0/UJBG6jYhcuyzKJx+Iw8uhBIGd9eba64XgWVgo20it1qwA==} + engines: {node: '>=18'} + '@mswjs/data@0.16.2': resolution: {integrity: sha512-/C0d/PBcJyQJokUhcjO4HiZPc67hzllKlRtD1XELygl2t991/ATAAQJVcStn4YtVALsNodruzOHT0JIvgr0hnA==} @@ -7317,6 +7324,13 @@ packages: engines: {node: '>=18'} hasBin: true + playwright-msw@3.0.1: + resolution: {integrity: sha512-w2bVjt7kPIThOQF9OS/1vDDs0HsQfV9inxMVSUv74x/zhCcrgzVN47xpPk84okf3OcCRHHBJKq8sNeBfCDyhMg==} + engines: {node: '>=18'} + peerDependencies: + '@playwright/test': '>=1.20.0' + msw: ^2.0.0 + playwright@1.50.0: resolution: {integrity: sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w==} engines: {node: '>=18'} @@ -11212,6 +11226,8 @@ snapshots: call-me-maybe: 1.0.2 glob-to-regexp: 0.3.0 + '@mswjs/cookies@1.1.1': {} + '@mswjs/data@0.16.2(@types/node@22.10.10)(typescript@5.7.3)': dependencies: '@types/lodash': 4.17.14 @@ -18584,6 +18600,13 @@ snapshots: playwright-core@1.50.0: {} + playwright-msw@3.0.1(@playwright/test@1.50.0)(msw@2.7.0(@types/node@22.10.10)(typescript@5.7.3)): + dependencies: + '@mswjs/cookies': 1.1.1 + '@playwright/test': 1.50.0 + msw: 2.7.0(@types/node@22.10.10)(typescript@5.7.3) + strict-event-emitter: 0.5.1 + playwright@1.50.0: dependencies: playwright-core: 1.50.0 From fe40d9cf0875bce653f548a77327ed835df9f081 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 13:01:44 +0100 Subject: [PATCH 145/189] playwright: Use `@sinonjs/fake-timers` on the test runner side too --- e2e/helper.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/e2e/helper.ts b/e2e/helper.ts index 13fb0d9c497..ae9f54e70e4 100644 --- a/e2e/helper.ts +++ b/e2e/helper.ts @@ -1,9 +1,9 @@ import { test as base } from '@playwright/test'; import type { MockServiceWorker } from 'playwright-msw'; -import { http, HttpResponse } from 'msw'; import { createWorker } from 'playwright-msw'; import { db, handlers } from '@crates-io/msw'; +import * as pwFakeTimers from '@sinonjs/fake-timers'; import { FakeTimers, FakeTimersOptions } from './fixtures/fake-timers'; import { MiragePage } from './fixtures/mirage'; import { PercyPage } from './fixtures/percy'; @@ -33,11 +33,23 @@ export const test = base.extend({ emberOptions: [{ setTesting: true, mockSentry: true }, { option: true }], clock: [ async ({ page, clockOptions }, use) => { + let now = clockOptions.now; + if (typeof now === 'string') { + now = Date.parse(now); + } + + let pwClock = pwFakeTimers.install({ + ...clockOptions, + now, + toFake: ['Date'], + }); + let clock = new FakeTimers(page); if (clockOptions != null) { await clock.setup(clockOptions); } await use(clock); + pwClock?.uninstall(); }, { auto: true, scope: 'test' }, ], From ab27271cb05021a311232506dbc6b38ef8651208 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 12:25:35 +0100 Subject: [PATCH 146/189] e2e/acceptance/front-page: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/front-page.spec.ts | 39 +++++++++++++------------------ 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/e2e/acceptance/front-page.spec.ts b/e2e/acceptance/front-page.spec.ts index de0189a9c04..64b30387a39 100644 --- a/e2e/acceptance/front-page.spec.ts +++ b/e2e/acceptance/front-page.spec.ts @@ -1,11 +1,12 @@ -import { test, expect } from '@/e2e/helper'; +import { defer } from '@/e2e/deferred'; +import { expect, test } from '@/e2e/helper'; +import { loadFixtures } from '@crates-io/msw/fixtures'; +import { http, HttpResponse } from 'msw'; test.describe('Acceptance | front page', { tag: '@acceptance' }, () => { test.use({ locale: 'en' }); - test('visiting /', async ({ page, mirage, percy, a11y }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('visiting /', async ({ page, msw, percy, a11y }) => { + loadFixtures(msw.db); await page.goto('/'); @@ -19,10 +20,10 @@ test.describe('Acceptance | front page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-total-downloads] [data-test-value]')).toHaveText('143,345'); await expect(page.locator('[data-test-total-crates] [data-test-value]')).toHaveText('23'); - await expect(page.locator('[data-test-new-crates] [data-test-crate-link="0"]')).toHaveText('Inflector v1.0.0'); + await expect(page.locator('[data-test-new-crates] [data-test-crate-link="0"]')).toHaveText('serde v1.0.0'); await expect(page.locator('[data-test-new-crates] [data-test-crate-link="0"]')).toHaveAttribute( 'href', - '/crates/Inflector', + '/crates/serde', ); await expect(page.locator('[data-test-most-downloaded] [data-test-crate-link="0"]')).toHaveText('serde'); @@ -41,22 +42,18 @@ test.describe('Acceptance | front page', { tag: '@acceptance' }, () => { await a11y.audit(); }); - test('error handling', async ({ page, mirage }) => { - await mirage.addHook(server => { - // Snapshot the routes so we can restore it later - globalThis._routes = server._config.routes; - server.get('/api/v1/summary', {}, 500); - }); + test('error handling', async ({ page, msw }) => { + await msw.worker.use(http.get('/api/v1/summary', () => HttpResponse.json({}, { status: 500 }))); await page.goto('/'); await expect(page.locator('[data-test-lists]')).toHaveCount(0); await expect(page.locator('[data-test-error-message]')).toBeVisible(); await expect(page.locator('[data-test-try-again-button]')).toBeEnabled(); - await page.evaluate(() => { - globalThis.deferred = require('rsvp').defer(); - server.get('/api/v1/summary', () => globalThis.deferred.promise); - }); + await msw.worker.resetHandlers(); + + let deferred = defer(); + msw.worker.use(http.get('/api/v1/summary', () => deferred.promise)); const button = page.locator('[data-test-try-again-button]'); await button.click(); @@ -65,12 +62,8 @@ test.describe('Acceptance | front page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-error-message]')).toBeVisible(); await expect(page.locator('[data-test-try-again-button]')).toBeDisabled(); - await page.evaluate(async () => { - // Restore the routes - globalThis._routes.call(server); - const data = await globalThis.fetch('/api/v1/summary').then(r => r.json()); - return globalThis.deferred.resolve(data); - }); + deferred.resolve(); + await expect(page.locator('[data-test-lists]')).toBeVisible(); await expect(page.locator('[data-test-error-message]')).toHaveCount(0); await expect(page.locator('[data-test-try-again-button]')).toHaveCount(0); From 7fe47e4791db996500cea5d819cf7b0033a527b5 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 12:39:25 +0100 Subject: [PATCH 147/189] e2e/acceptance/settings/add-owner: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/settings/add-owner.spec.ts | 40 ++++++++++------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/e2e/acceptance/settings/add-owner.spec.ts b/e2e/acceptance/settings/add-owner.spec.ts index 8fd6f7cc074..43f53cbdf0f 100644 --- a/e2e/acceptance/settings/add-owner.spec.ts +++ b/e2e/acceptance/settings/add-owner.spec.ts @@ -1,22 +1,20 @@ import { expect, test } from '@/e2e/helper'; test.describe('Acceptance | Settings | Add Owner', { tag: '@acceptance' }, () => { - test.beforeEach(async ({ mirage }) => { - await mirage.addHook(server => { - let user1 = server.create('user', { name: 'blabaere' }); - let user2 = server.create('user', { name: 'thehydroimpulse' }); - let team1 = server.create('team', { org: 'org', name: 'blabaere' }); - let team2 = server.create('team', { org: 'org', name: 'thehydroimpulse' }); + test.beforeEach(async ({ msw }) => { + let user1 = msw.db.user.create({ name: 'blabaere' }); + let user2 = msw.db.user.create({ name: 'thehydroimpulse' }); + let team1 = msw.db.team.create({ org: 'org', name: 'blabaere' }); + let team2 = msw.db.team.create({ org: 'org', name: 'thehydroimpulse' }); - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '1.0.0' }); - server.create('crate-ownership', { crate, user: user1 }); - server.create('crate-ownership', { crate, user: user2 }); - server.create('crate-ownership', { crate, team: team1 }); - server.create('crate-ownership', { crate, team: team2 }); + let crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate, num: '1.0.0' }); + msw.db.crateOwnership.create({ crate, user: user1 }); + msw.db.crateOwnership.create({ crate, user: user2 }); + msw.db.crateOwnership.create({ crate, team: team1 }); + msw.db.crateOwnership.create({ crate, team: team2 }); - authenticateAs(user1); - }); + await msw.authenticateAs(user1); }); test('attempting to add owner without username', async ({ page }) => { @@ -37,10 +35,8 @@ test.describe('Acceptance | Settings | Add Owner', { tag: '@acceptance' }, () => await expect(page.locator('[data-test-owners] [data-test-owner-user]')).toHaveCount(2); }); - test('add a new owner', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.create('user', { name: 'iain8' }); - }); + test('add a new owner', async ({ page, msw }) => { + msw.db.user.create({ name: 'iain8' }); await page.goto('/crates/nanomsg/settings'); await page.fill('input[name="username"]', 'iain8'); @@ -53,11 +49,9 @@ test.describe('Acceptance | Settings | Add Owner', { tag: '@acceptance' }, () => await expect(page.locator('[data-test-owners] [data-test-owner-user]')).toHaveCount(2); }); - test('add a team owner', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.create('user', { name: 'iain8' }); - server.create('team', { org: 'rust-lang', name: 'crates-io' }); - }); + test('add a team owner', async ({ page, msw }) => { + msw.db.user.create({ name: 'iain8' }); + msw.db.team.create({ org: 'rust-lang', name: 'crates-io' }); await page.goto('/crates/nanomsg/settings'); await page.fill('input[name="username"]', 'github:rust-lang:crates-io'); From 3748f6fd848c9855b946fe928d32060ce50849ea Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 12:45:51 +0100 Subject: [PATCH 148/189] e2e/acceptance/settings/remove-owner: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/settings/remove-owner.spec.ts | 74 ++++++++------------ 1 file changed, 28 insertions(+), 46 deletions(-) diff --git a/e2e/acceptance/settings/remove-owner.spec.ts b/e2e/acceptance/settings/remove-owner.spec.ts index fcd8c7a68b5..f767ed711c4 100644 --- a/e2e/acceptance/settings/remove-owner.spec.ts +++ b/e2e/acceptance/settings/remove-owner.spec.ts @@ -1,29 +1,23 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Acceptance | Settings | Remove Owner', { tag: '@acceptance' }, () => { - test.beforeEach(async ({ page, mirage }) => { - await page.addInitScript(() => { - globalThis.crate = { name: 'nanomsg' }; - }); - await mirage.addHook(server => { - let user1 = server.create('user', { name: 'blabaere' }); - let user2 = server.create('user', { name: 'thehydroimpulse' }); - let team1 = server.create('team', { org: 'org', name: 'blabaere' }); - let team2 = server.create('team', { org: 'org', name: 'thehydroimpulse' }); - - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '1.0.0' }); - server.create('crate-ownership', { crate, user: user1 }); - server.create('crate-ownership', { crate, user: user2 }); - server.create('crate-ownership', { crate, team: team1 }); - server.create('crate-ownership', { crate, team: team2 }); - - authenticateAs(user1); - - globalThis.crate = crate; - globalThis.user2 = user2; - globalThis.team1 = team1; - }); + let user1, user2, team1, team2, crate; + + test.beforeEach(async ({ msw }) => { + user1 = msw.db.user.create({ name: 'blabaere' }); + user2 = msw.db.user.create({ name: 'thehydroimpulse' }); + team1 = msw.db.team.create({ org: 'org', name: 'blabaere' }); + team2 = msw.db.team.create({ org: 'org', name: 'thehydroimpulse' }); + + crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate, num: '1.0.0' }); + msw.db.crateOwnership.create({ crate, user: user1 }); + msw.db.crateOwnership.create({ crate, user: user2 }); + msw.db.crateOwnership.create({ crate, team: team1 }); + msw.db.crateOwnership.create({ crate, team: team2 }); + + await msw.authenticateAs(user1); }); test('remove a crate owner when owner is a user', async ({ page }) => { @@ -36,19 +30,13 @@ test.describe('Acceptance | Settings | Remove Owner', { tag: '@acceptance' }, () await expect(page.locator('[data-test-owner-user]')).toHaveCount(1); }); - test('remove a user crate owner (error behavior)', async ({ page, mirage }) => { - await mirage.addHook(server => { - // we are intentionally returning a 200 response here, because is what - // the real backend also returns due to legacy reasons - server.delete('/api/v1/crates/nanomsg/owners', { errors: [{ detail: 'nope' }] }); - }); - - await page.goto('about:blank'); - let crate = await page.evaluate<{ name: string }>('crate'); + test('remove a user crate owner (error behavior)', async ({ page, msw }) => { + // we are intentionally returning a 200 response here, because is what + // the real backend also returns due to legacy reasons + let error = HttpResponse.json({ errors: [{ detail: 'nope' }] }); + await msw.worker.use(http.delete('/api/v1/crates/nanomsg/owners', () => error)); await page.goto(`/crates/${crate.name}/settings`); - - const user2 = await page.evaluate(() => JSON.parse(JSON.stringify(user2))); await page.click(`[data-test-owner-user="${user2.login}"] [data-test-remove-owner-button]`); await expect(page.locator('[data-test-notification-message="error"]')).toHaveText( @@ -67,19 +55,13 @@ test.describe('Acceptance | Settings | Remove Owner', { tag: '@acceptance' }, () await expect(page.locator('[data-test-owner-team]')).toHaveCount(1); }); - test('remove a team crate owner (error behavior)', async ({ page, mirage }) => { - await mirage.addHook(server => { - // we are intentionally returning a 200 response here, because is what - // the real backend also returns due to legacy reasons - server.delete('/api/v1/crates/nanomsg/owners', { errors: [{ detail: 'nope' }] }); - }); - - await page.goto('about:blank'); - let crate = await page.evaluate<{ name: string }>('crate'); + test('remove a team crate owner (error behavior)', async ({ page, msw }) => { + // we are intentionally returning a 200 response here, because is what + // the real backend also returns due to legacy reasons + let error = HttpResponse.json({ errors: [{ detail: 'nope' }] }); + await msw.worker.use(http.delete('/api/v1/crates/nanomsg/owners', () => error)); await page.goto(`/crates/${crate.name}/settings`); - - let team1 = await page.evaluate(() => JSON.parse(JSON.stringify(team1))); await page.click(`[data-test-owner-team="${team1.login}"] [data-test-remove-owner-button]`); await expect(page.locator('[data-test-notification-message="error"]')).toHaveText( From 24369f866382b28176d75be057fceb78f4eb0cee Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 12:47:42 +0100 Subject: [PATCH 149/189] e2e/acceptance/settings/settings: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/settings/settings.spec.ts | 28 +++++++++++------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/e2e/acceptance/settings/settings.spec.ts b/e2e/acceptance/settings/settings.spec.ts index 2f2ce0165e1..9adf2c31ea1 100644 --- a/e2e/acceptance/settings/settings.spec.ts +++ b/e2e/acceptance/settings/settings.spec.ts @@ -1,22 +1,20 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; test.describe('Acceptance | Settings', { tag: '@acceptance' }, () => { - test.beforeEach(async ({ mirage }) => { - await mirage.addHook(server => { - let user1 = server.create('user', { name: 'blabaere' }); - let user2 = server.create('user', { name: 'thehydroimpulse' }); - let team1 = server.create('team', { org: 'org', name: 'blabaere' }); - let team2 = server.create('team', { org: 'org', name: 'thehydroimpulse' }); + test.beforeEach(async ({ msw }) => { + let user1 = msw.db.user.create({ name: 'blabaere' }); + let user2 = msw.db.user.create({ name: 'thehydroimpulse' }); + let team1 = msw.db.team.create({ org: 'org', name: 'blabaere' }); + let team2 = msw.db.team.create({ org: 'org', name: 'thehydroimpulse' }); - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '1.0.0' }); - server.create('crate-ownership', { crate, user: user1 }); - server.create('crate-ownership', { crate, user: user2 }); - server.create('crate-ownership', { crate, team: team1 }); - server.create('crate-ownership', { crate, team: team2 }); + let crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate, num: '1.0.0' }); + msw.db.crateOwnership.create({ crate, user: user1 }); + msw.db.crateOwnership.create({ crate, user: user2 }); + msw.db.crateOwnership.create({ crate, team: team1 }); + msw.db.crateOwnership.create({ crate, team: team2 }); - authenticateAs(user1); - }); + await msw.authenticateAs(user1); }); test('listing crate owners', async ({ page, percy, a11y }) => { From aa884d740761ee1adad14bfdd0bf46c7536a2312 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 13:01:53 +0100 Subject: [PATCH 150/189] e2e/acceptance/api-token: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/api-token.spec.ts | 86 +++++++++++++++----------------- 1 file changed, 40 insertions(+), 46 deletions(-) diff --git a/e2e/acceptance/api-token.spec.ts b/e2e/acceptance/api-token.spec.ts index 8824693a39f..d707d28e093 100644 --- a/e2e/acceptance/api-token.spec.ts +++ b/e2e/acceptance/api-token.spec.ts @@ -1,38 +1,37 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Acceptance | api-tokens', { tag: '@acceptance' }, () => { - test.beforeEach(async ({ mirage }) => { - await mirage.addHook(server => { - let user = server.create('user', { - login: 'johnnydee', - name: 'John Doe', - email: 'john@doe.com', - avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', - }); - server.create('api-token', { - user, - name: 'BAR', - createdAt: '2017-11-19T17:59:22', - lastUsedAt: null, - expiredAt: '2017-12-19T17:59:22', - }); - - server.create('api-token', { - user, - name: 'recently expired', - createdAt: '2017-08-01T12:34:56', - lastUsedAt: '2017-11-02T01:45:14', - expiredAt: '2017-11-19T17:59:22', - }); - server.create('api-token', { - user, - name: 'foo', - createdAt: '2017-08-01T12:34:56', - lastUsedAt: '2017-11-02T01:45:14', - }); - - globalThis.authenticateAs(user); + test.beforeEach(async ({ msw }) => { + let user = msw.db.user.create({ + login: 'johnnydee', + name: 'John Doe', + email: 'john@doe.com', + avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', }); + msw.db.apiToken.create({ + user, + name: 'BAR', + createdAt: '2017-11-19T17:59:22', + lastUsedAt: null, + expiredAt: '2017-12-19T17:59:22', + }); + + msw.db.apiToken.create({ + user, + name: 'recently expired', + createdAt: '2017-08-01T12:34:56', + lastUsedAt: '2017-11-02T01:45:14', + expiredAt: '2017-11-19T17:59:22', + }); + msw.db.apiToken.create({ + user, + name: 'foo', + createdAt: '2017-08-01T12:34:56', + lastUsedAt: '2017-11-02T01:45:14', + }); + + await msw.authenticateAs(user); }); test('/me is showing the list of active API tokens', async ({ page }) => { @@ -72,16 +71,13 @@ test.describe('Acceptance | api-tokens', { tag: '@acceptance' }, () => { await expect(row3.locator('[data-test-token]')).toHaveCount(0); }); - test('API tokens can be revoked', async ({ page }) => { + test('API tokens can be revoked', async ({ page, msw }) => { await page.goto('/settings/tokens'); await expect(page).toHaveURL('/settings/tokens'); await expect(page.locator('[data-test-api-token]')).toHaveCount(3); await page.click('[data-test-api-token="1"] [data-test-revoke-token-button]'); - expect( - await page.evaluate(() => server.schema['apiTokens'].all().length), - 'API token has been deleted from the backend database', - ).toBe(2); + expect(msw.db.apiToken.findMany({}).length, 'API token has been deleted from the backend database').toBe(2); await expect(page.locator('[data-test-api-token]')).toHaveCount(2); await expect(page.locator('[data-test-api-token="2"]')).toBeVisible(); @@ -97,12 +93,10 @@ test.describe('Acceptance | api-tokens', { tag: '@acceptance' }, () => { await expect(page).toHaveURL('/settings/tokens/new?from=1'); }); - test('failed API tokens revocation shows an error', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.delete('/api/v1/me/tokens/:id', {}, 500); - }); + test('failed API tokens revocation shows an error', async ({ page, msw }) => { + await msw.worker.use(http.delete('/api/v1/me/tokens/:id', () => HttpResponse.json({}, { status: 500 }))); - await mirage.page.goto('/settings/tokens'); + await page.goto('/settings/tokens'); await expect(page).toHaveURL('/settings/tokens'); await expect(page.locator('[data-test-api-token]')).toHaveCount(3); @@ -115,7 +109,7 @@ test.describe('Acceptance | api-tokens', { tag: '@acceptance' }, () => { ); }); - test('new API tokens can be created', async ({ page, percy }) => { + test('new API tokens can be created', async ({ page, percy, msw }) => { await page.goto('/settings/tokens'); await expect(page).toHaveURL('/settings/tokens'); await expect(page.locator('[data-test-api-token]')).toHaveCount(3); @@ -129,7 +123,7 @@ test.describe('Acceptance | api-tokens', { tag: '@acceptance' }, () => { await page.click('[data-test-generate]'); - let token = await page.evaluate(() => server.schema['apiTokens'].findBy({ name: 'the new token' })?.token); + let token = msw.db.apiToken.findFirst({ where: { name: { equals: 'the new token' } } })?.token; expect(token, 'API token has been created in the backend database').toBeTruthy(); await expect(page.locator('[data-test-api-token="4"] [data-test-name]')).toHaveText('the new token'); @@ -140,14 +134,14 @@ test.describe('Acceptance | api-tokens', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-token]')).toHaveText(token); }); - test('API tokens are only visible in plaintext until the page is left', async ({ page }) => { + test('API tokens are only visible in plaintext until the page is left', async ({ page, msw }) => { await page.goto('/settings/tokens'); await page.click('[data-test-new-token-button]'); await page.fill('[data-test-name]', 'the new token'); await page.click('[data-test-scope="publish-update"]'); await page.click('[data-test-generate]'); - let token = await page.evaluate(() => server.schema['apiTokens'].findBy({ name: 'the new token' })?.token); + let token = msw.db.apiToken.findFirst({ where: { name: { equals: 'the new token' } } })?.token; await expect(page.locator('[data-test-token]')).toHaveText(token); // leave the API tokens page From 1eeb54e05a4e9a404807432bbfb13078d8e23791 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 13:49:07 +0100 Subject: [PATCH 151/189] e2e/acceptance/categories: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/categories.spec.ts | 38 ++++++++++++------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/e2e/acceptance/categories.spec.ts b/e2e/acceptance/categories.spec.ts index 036ce16ed43..be2537e31df 100644 --- a/e2e/acceptance/categories.spec.ts +++ b/e2e/acceptance/categories.spec.ts @@ -1,15 +1,13 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; test.describe('Acceptance | categories', { tag: '@acceptance' }, () => { - test('listing categories', async ({ page, mirage, percy, a11y }) => { - await mirage.addHook(server => { - server.create('category', { category: 'API bindings' }); - server.create('category', { category: 'Algorithms' }); - server.createList('crate', 1, { categoryIds: ['algorithms'] }); - server.create('category', { category: 'Asynchronous' }); - server.createList('crate', 15, { categoryIds: ['asynchronous'] }); - server.create('category', { category: 'Everything', crates_cnt: 1234 }); - }); + test('listing categories', async ({ page, msw, percy, a11y }) => { + msw.db.category.create({ category: 'API bindings' }); + let algos = msw.db.category.create({ category: 'Algorithms' }); + msw.db.crate.create({ categories: [algos] }); + let async = msw.db.category.create({ category: 'Asynchronous' }); + Array.from({ length: 15 }).forEach(() => msw.db.crate.create({ categories: [async] })); + msw.db.category.create({ category: 'Everything', crates_cnt: 1234 }); await page.goto('/categories'); @@ -22,10 +20,8 @@ test.describe('Acceptance | categories', { tag: '@acceptance' }, () => { await a11y.audit(); }); - test('category/:category_id index default sort is recent-downloads', async ({ page, mirage, percy, a11y }) => { - await mirage.addHook(server => { - server.create('category', { category: 'Algorithms' }); - }); + test('category/:category_id index default sort is recent-downloads', async ({ page, msw, percy, a11y }) => { + msw.db.category.create({ category: 'Algorithms' }); await page.goto('/categories/algorithms'); await expect(page.locator('[data-test-category-sort] [data-test-current-order]')).toHaveText('Recent Downloads'); @@ -34,11 +30,9 @@ test.describe('Acceptance | categories', { tag: '@acceptance' }, () => { await a11y.audit(); }); - test('listing category slugs', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.create('category', { category: 'Algorithms', description: 'Crates for algorithms' }); - server.create('category', { category: 'Asynchronous', description: 'Async crates' }); - }); + test('listing category slugs', async ({ page, msw }) => { + msw.db.category.create({ category: 'Algorithms', description: 'Crates for algorithms' }); + msw.db.category.create({ category: 'Asynchronous', description: 'Async crates' }); await page.goto('/category_slugs'); await expect(page.locator('[data-test-category-slug="algorithms"]')).toHaveText('algorithms'); @@ -50,10 +44,8 @@ test.describe('Acceptance | categories', { tag: '@acceptance' }, () => { test.describe('Acceptance | categories (locale: de)', { tag: '@acceptance' }, () => { test.use({ locale: 'de' }); - test('listing categories', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.create('category', { category: 'Everything', crates_cnt: 1234 }); - }); + test('listing categories', async ({ page, msw }) => { + msw.db.category.create({ category: 'Everything', crates_cnt: 1234 }); await page.goto('categories'); await expect(page.locator('[data-test-category="everything"] [data-test-crate-count]')).toHaveText('1.234 crates'); From 917557a660a844d39fd7f82dffb1c6725130c893 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 14:01:07 +0100 Subject: [PATCH 152/189] e2e/acceptance/crate: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/crate.spec.ts | 154 ++++++++++++++--------------------- 1 file changed, 59 insertions(+), 95 deletions(-) diff --git a/e2e/acceptance/crate.spec.ts b/e2e/acceptance/crate.spec.ts index 505545ae678..99b412d020b 100644 --- a/e2e/acceptance/crate.spec.ts +++ b/e2e/acceptance/crate.spec.ts @@ -1,11 +1,11 @@ import { expect, test } from '@/e2e/helper'; +import { loadFixtures } from '@crates-io/msw/fixtures'; +import { http, HttpResponse } from 'msw'; test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { - test('visiting a crate page from the front page', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'nanomsg', newest_version: '0.6.1' }); - server.create('version', { crate, num: '0.6.1' }); - }); + test('visiting a crate page from the front page', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'nanomsg', newest_version: '0.6.1' }); + msw.db.version.create({ crate, num: '0.6.1' }); await page.goto('/'); await page.click('[data-test-just-updated] [data-test-crate-link="0"]'); @@ -17,12 +17,10 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-heading] [data-test-crate-version]')).toHaveText('v0.6.1'); }); - test('visiting /crates/nanomsg', async ({ page, mirage, ember, percy, a11y }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '0.6.0' }); - server.create('version', { crate, num: '0.6.1', rust_version: '1.69' }); - }); + test('visiting /crates/nanomsg', async ({ page, msw, ember, percy, a11y }) => { + let crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate, num: '0.6.0' }); + msw.db.version.create({ crate, num: '0.6.1', rust_version: '1.69' }); await page.goto('/crates/nanomsg'); @@ -40,12 +38,10 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await a11y.audit(); }); - test('visiting /crates/nanomsg/', async ({ page, mirage, ember }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '0.6.0' }); - server.create('version', { crate, num: '0.6.1' }); - }); + test('visiting /crates/nanomsg/', async ({ page, msw, ember }) => { + let crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate, num: '0.6.0' }); + msw.db.version.create({ crate, num: '0.6.1' }); await page.goto('/crates/nanomsg/'); @@ -60,12 +56,10 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-crate-stats-label]')).toHaveText('Stats Overview'); }); - test('visiting /crates/nanomsg/0.6.0', async ({ page, mirage, ember, percy, a11y }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '0.6.0' }); - server.create('version', { crate, num: '0.6.1' }); - }); + test('visiting /crates/nanomsg/0.6.0', async ({ page, msw, ember, percy, a11y }) => { + let crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate, num: '0.6.0' }); + msw.db.version.create({ crate, num: '0.6.1' }); await page.goto('/crates/nanomsg/0.6.0'); @@ -92,10 +86,8 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-try-again]')).toHaveCount(0); }); - test('other crate loading error shows an error message', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.get('/api/v1/crates/:crate_name', {}, 500); - }); + test('other crate loading error shows an error message', async ({ page, msw }) => { + msw.worker.use(http.get('/api/v1/crates/:crate_name', () => HttpResponse.json({}, { status: 500 }))); await page.goto('/crates/nanomsg'); await expect(page).toHaveURL('/crates/nanomsg'); @@ -105,12 +97,10 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-try-again]')).toBeVisible(); }); - test('unknown versions fall back to latest version and show an error message', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '0.6.0' }); - server.create('version', { crate, num: '0.6.1' }); - }); + test('unknown versions fall back to latest version and show an error message', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate, num: '0.6.0' }); + msw.db.version.create({ crate, num: '0.6.1' }); await page.goto('/crates/nanomsg/0.7.0'); @@ -121,14 +111,12 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-try-again]')).toHaveCount(0); }); - test('other versions loading error shows an error message', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '0.6.0' }); - server.create('version', { crate, num: '0.6.1' }); + test('other versions loading error shows an error message', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate, num: '0.6.0' }); + msw.db.version.create({ crate, num: '0.6.1' }); - server.get('/api/v1/crates/:crate_name/versions', {}, 500); - }); + await msw.worker.use(http.get('/api/v1/crates/:crate_name/versions', () => HttpResponse.json({}, { status: 500 }))); await page.goto('/'); await page.click('[data-test-just-updated] [data-test-crate-link="0"]'); @@ -139,11 +127,9 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-try-again]')).toBeVisible(); }); - test('works for non-canonical names', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo-bar' }); - server.create('version', { crate }); - }); + test('works for non-canonical names', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo-bar' }); + msw.db.version.create({ crate }); await page.goto('/crates/foo_bar'); @@ -153,10 +139,8 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-heading] [data-test-crate-name]')).toHaveText('foo-bar'); }); - test('navigating to the all versions page', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('navigating to the all versions page', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/crates/nanomsg'); await page.click('[data-test-versions-tab] a'); @@ -166,10 +150,8 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { ); }); - test('navigating to the reverse dependencies page', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('navigating to the reverse dependencies page', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/crates/nanomsg'); await page.click('[data-test-rev-deps-tab] a'); @@ -178,10 +160,8 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page.locator('a[href="/crates/unicorn-rpc"]')).toHaveText('unicorn-rpc'); }); - test('navigating to a user page', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('navigating to a user page', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/crates/nanomsg'); await page.click('[data-test-owners] [data-test-owner-link="blabaere"]'); @@ -190,10 +170,8 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-heading] [data-test-username]')).toHaveText('blabaere'); }); - test('navigating to a team page', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('navigating to a team page', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/crates/nanomsg'); await page.click('[data-test-owners] [data-test-owner-link="github:org:thehydroimpulse"]'); @@ -202,10 +180,8 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-heading] [data-test-team-name]')).toHaveText('thehydroimpulseteam'); }); - test('crates having user-owners', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('crates having user-owners', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/crates/nanomsg'); @@ -216,10 +192,8 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-owners] li')).toHaveCount(4); }); - test('crates having team-owners', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('crates having team-owners', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/crates/nanomsg'); @@ -227,10 +201,8 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-owners] li')).toHaveCount(4); }); - test('crates license is supplied by version', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('crates license is supplied by version', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/crates/nanomsg'); await expect(page.locator('[data-test-license]')).toHaveText('Apache-2.0'); @@ -239,13 +211,11 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-license]')).toHaveText('MIT OR Apache-2.0'); }); - test.skip('crates can be yanked by owner', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); + test.skip('crates can be yanked by owner', async ({ page, msw }) => { + loadFixtures(msw.db); - let user = server.schema['users'].findBy({ login: 'thehydroimpulse' }); - authenticateAs(user); - }); + let user = msw.db.user.findFirst({ where: { login: { equals: 'thehydroimpulse' } } }); + await msw.authenticateAs(user); await page.goto('/crates/nanomsg/0.5.0'); const yankButton = page.locator('[data-test-version-yank-button="0.5.0"]'); @@ -261,36 +231,30 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(yankButton).toBeVisible(); }); - test('navigating to the owners page when not logged in', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('navigating to the owners page when not logged in', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/crates/nanomsg'); await expect(page.locator('[data-test-settings-tab]')).toHaveCount(0); }); - test('navigating to the owners page when not an owner', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); + test('navigating to the owners page when not an owner', async ({ page, msw }) => { + loadFixtures(msw.db); - let user = server.schema['users'].findBy({ login: 'iain8' }); - authenticateAs(user); - }); + let user = msw.db.user.findFirst({ where: { login: { equals: 'iain8' } } }); + await msw.authenticateAs(user); await page.goto('/crates/nanomsg'); await expect(page.locator('[data-test-settings-tab]')).toHaveCount(0); }); - test('navigating to the settings page', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); + test('navigating to the settings page', async ({ page, msw }) => { + loadFixtures(msw.db); - let user = server.schema['users'].findBy({ login: 'thehydroimpulse' }); - authenticateAs(user); - }); + let user = msw.db.user.findFirst({ where: { login: { equals: 'thehydroimpulse' } } }); + await msw.authenticateAs(user); await page.goto('/crates/nanomsg'); await page.click('[data-test-settings-tab] a'); From cf8cec92261f9c8ff3be1e99e3b8c3a3864cbb46 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 14:05:08 +0100 Subject: [PATCH 153/189] e2e/acceptance/crate-deletion: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/crate-deletion.spec.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/e2e/acceptance/crate-deletion.spec.ts b/e2e/acceptance/crate-deletion.spec.ts index ba349ce25fe..0fa4de33711 100644 --- a/e2e/acceptance/crate-deletion.spec.ts +++ b/e2e/acceptance/crate-deletion.spec.ts @@ -1,15 +1,13 @@ import { expect, test } from '@/e2e/helper'; test.describe('Acceptance | crate deletion', { tag: '@acceptance' }, () => { - test('happy path', async ({ page, mirage }) => { - await mirage.addHook(server => { - let user = server.create('user'); - authenticateAs(user); + test('happy path', async ({ page, msw }) => { + let user = msw.db.user.create(); + await msw.authenticateAs(user); - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate }); - server.create('crate-ownership', { crate, user }); - }); + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate }); + msw.db.crateOwnership.create({ crate, user }); await page.goto('/crates/foo'); await expect(page).toHaveURL('/crates/foo'); @@ -34,7 +32,7 @@ test.describe('Acceptance | crate deletion', { tag: '@acceptance' }, () => { let message = 'Crate foo has been successfully deleted.'; await expect(page.locator('[data-test-notification-message="success"]')).toHaveText(message); - let crate = await page.evaluate(() => server.schema.crates.findBy({ name: 'foo' })); + crate = msw.db.crate.findFirst({ where: { name: { equals: 'foo' } } }); expect(crate).toBeNull(); }); }); From 4c1a947b8eb9e94e15bbe518140ccc213beff95c Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 14:11:06 +0100 Subject: [PATCH 154/189] e2e/acceptance/crate-dependencies: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/crate-dependencies.spec.ts | 76 ++++++++++------------- 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/e2e/acceptance/crate-dependencies.spec.ts b/e2e/acceptance/crate-dependencies.spec.ts index eb020ffa03f..ac792096dd0 100644 --- a/e2e/acceptance/crate-dependencies.spec.ts +++ b/e2e/acceptance/crate-dependencies.spec.ts @@ -1,10 +1,10 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; +import { loadFixtures } from '@crates-io/msw/fixtures'; +import { http, HttpResponse } from 'msw'; test.describe('Acceptance | crate dependencies page', { tag: '@acceptance' }, () => { - test('shows the lists of dependencies', async ({ page, mirage, percy, a11y }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('shows the lists of dependencies', async ({ page, msw, percy, a11y }) => { + loadFixtures(msw.db); await page.goto('/crates/nanomsg/dependencies'); await expect(page).toHaveURL('/crates/nanomsg/0.6.1/dependencies'); @@ -18,11 +18,9 @@ test.describe('Acceptance | crate dependencies page', { tag: '@acceptance' }, () await a11y.audit(); }); - test('empty list case', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '0.6.1' }); - }); + test('empty list case', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate, num: '0.6.1' }); await page.goto('/crates/nanomsg/dependencies'); @@ -41,10 +39,8 @@ test.describe('Acceptance | crate dependencies page', { tag: '@acceptance' }, () await expect(page.locator('[data-test-try-again]')).toHaveCount(0); }); - test('shows an error page if crate fails to load', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.get('/api/v1/crates/:crate_name', {}, 500); - }); + test('shows an error page if crate fails to load', async ({ page, msw }) => { + await msw.worker.use(http.get('/api/v1/crates/:crate_name', () => HttpResponse.json({}, { status: 500 }))); await page.goto('/crates/foo/1.0.0/dependencies'); await expect(page).toHaveURL('/crates/foo/1.0.0/dependencies'); @@ -54,11 +50,9 @@ test.describe('Acceptance | crate dependencies page', { tag: '@acceptance' }, () await expect(page.locator('[data-test-try-again]')).toBeVisible(); }); - test('shows an error page if version is not found', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '2.0.0' }); - }); + test('shows an error page if version is not found', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '2.0.0' }); await page.goto('/crates/foo/1.0.0/dependencies'); await expect(page).toHaveURL('/crates/foo/1.0.0/dependencies'); @@ -68,12 +62,10 @@ test.describe('Acceptance | crate dependencies page', { tag: '@acceptance' }, () await expect(page.locator('[data-test-try-again]')).toHaveCount(0); }); - test('shows an error page if versions fail to load', async ({ page, mirage, ember }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '2.0.0' }); - server.get('/api/v1/crates/:crate_name/versions', {}, 500); - }); + test('shows an error page if versions fail to load', async ({ page, msw, ember }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '2.0.0' }); + await msw.worker.use(http.get('/api/v1/crates/:crate_name/versions', () => HttpResponse.json({}, { status: 500 }))); await page.goto('/crates/foo/1.0.0/dependencies'); @@ -84,13 +76,12 @@ test.describe('Acceptance | crate dependencies page', { tag: '@acceptance' }, () await expect(page.locator('[data-test-try-again]')).toBeVisible(); }); - test('shows error message if loading of dependencies fails', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.0.0' }); + test('shows error message if loading of dependencies fails', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.0.0' }); - server.get('/api/v1/crates/:crate_name/:version_num/dependencies', {}, 500); - }); + let error = HttpResponse.json({}, { status: 500 }); + await msw.worker.use(http.get('/api/v1/crates/:crate_name/:version_num/dependencies', () => error)); await page.goto('/crates/foo/1.0.0/dependencies'); await expect(page).toHaveURL('/crates/foo/1.0.0/dependencies'); @@ -100,21 +91,20 @@ test.describe('Acceptance | crate dependencies page', { tag: '@acceptance' }, () await expect(page.locator('[data-test-try-again]')).toBeVisible(); }); - test('hides description if loading of dependency details fails', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'nanomsg' }); - let version = server.create('version', { crate, num: '0.6.1' }); + test('hides description if loading of dependency details fails', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'nanomsg' }); + let version = msw.db.version.create({ crate, num: '0.6.1' }); - let foo = server.create('crate', { name: 'foo', description: 'This is the foo crate' }); - server.create('version', { crate: foo, num: '1.0.0' }); - server.create('dependency', { crate: foo, version, req: '^1.0.0', kind: 'normal' }); + let foo = msw.db.crate.create({ name: 'foo', description: 'This is the foo crate' }); + msw.db.version.create({ crate: foo, num: '1.0.0' }); + msw.db.dependency.create({ crate: foo, version, req: '^1.0.0', kind: 'normal' }); - let bar = server.create('crate', { name: 'bar', description: 'This is the bar crate' }); - server.create('version', { crate: bar, num: '2.3.4' }); - server.create('dependency', { crate: bar, version, req: '^2.0.0', kind: 'normal' }); + let bar = msw.db.crate.create({ name: 'bar', description: 'This is the bar crate' }); + msw.db.version.create({ crate: bar, num: '2.3.4' }); + msw.db.dependency.create({ crate: bar, version, req: '^2.0.0', kind: 'normal' }); - server.get('/api/v1/crates', {}, 500); - }); + let error = HttpResponse.json({}, { status: 500 }); + await msw.worker.use(http.get('/api/v1/crates', () => error)); await page.goto('/crates/nanomsg/dependencies'); await expect(page).toHaveURL('/crates/nanomsg/0.6.1/dependencies'); From 60e4b5ec06a4bbf2ae5b549bcd020bafd80acf85 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 14:29:31 +0100 Subject: [PATCH 155/189] e2e/acceptance/crate-following: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/crate-following.spec.ts | 95 ++++++++++++-------------- 1 file changed, 44 insertions(+), 51 deletions(-) diff --git a/e2e/acceptance/crate-following.spec.ts b/e2e/acceptance/crate-following.spec.ts index 8c6326a02f2..f6b02e43dd7 100644 --- a/e2e/acceptance/crate-following.spec.ts +++ b/e2e/acceptance/crate-following.spec.ts @@ -1,35 +1,32 @@ -import { test, expect } from '@/e2e/helper'; +import { defer } from '@/e2e/deferred'; +import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Acceptance | Crate following', { tag: '@acceptance' }, () => { - test.beforeEach(async ({ mirage }) => { - let hook = String(server => { - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '0.6.0' }); - - let loggedIn = !globalThis.skipLogin; - if (loggedIn) { - let followedCrates = !!globalThis.following ? [crate] : []; - let user = server.create('user', { followedCrates }); - globalThis.authenticateAs(user); - } - }); - await mirage.addHook(hook); - }); + async function prepare(msw, { skipLogin = false, following = false } = {}) { + let crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate, num: '0.6.0' }); + + let loggedIn = !skipLogin; + if (loggedIn) { + let followedCrates = following ? [crate] : []; + let user = msw.db.user.create({ followedCrates }); + await msw.authenticateAs(user); + } + } + + test("unauthenticated users don't see the follow button", async ({ page, msw }) => { + await prepare(msw, { skipLogin: true }); - test("unauthenticated users don't see the follow button", async ({ page }) => { - await page.addInitScript(() => { - globalThis.skipLogin = true; - }); await page.goto('/crates/nanomsg'); await expect(page.locator('[data-test-follow-button]')).toHaveCount(0); }); - test('authenticated users see a loading spinner and can follow/unfollow crates', async ({ page, mirage }) => { - await mirage.addHook(server => { - globalThis.defer = require('rsvp').defer; - globalThis.followingDeferred = globalThis.defer(); - server.get('/api/v1/crates/:crate_id/following', globalThis.followingDeferred.promise); - }); + test('authenticated users see a loading spinner and can follow/unfollow crates', async ({ page, msw }) => { + await prepare(msw); + + let followingDeferred = defer(); + await msw.worker.use(http.get('/api/v1/crates/:crate_id/following', () => followingDeferred.promise)); await page.goto('/crates/nanomsg'); @@ -39,44 +36,41 @@ test.describe('Acceptance | Crate following', { tag: '@acceptance' }, () => { await expect(followButton).toBeDisabled(); await expect(spinner).toBeVisible(); - await page.evaluate(() => globalThis.followingDeferred.resolve({ following: false })); + followingDeferred.resolve(HttpResponse.json({ following: false })); await expect(followButton).toHaveText('Follow'); await expect(followButton).toBeEnabled(); await expect(spinner).toHaveCount(0); - await page.evaluate(() => { - globalThis.followDeferred = globalThis.defer(); - server.put('/api/v1/crates/:crate_id/follow', globalThis.followDeferred.promise); - }); + let followDeferred = defer(); + await msw.worker.use(http.put('/api/v1/crates/:crate_id/follow', () => followDeferred.promise)); await followButton.click(); await expect(followButton).toHaveText('Loading…'); await expect(followButton).toBeDisabled(); await expect(spinner).toBeVisible(); - await page.evaluate(() => globalThis.followDeferred.resolve({ ok: true })); + followDeferred.resolve(HttpResponse.json({ ok: true })); await expect(followButton).toHaveText('Unfollow'); await expect(followButton).toBeEnabled(); await expect(spinner).toHaveCount(0); - await page.evaluate(() => { - globalThis.unfollowDeferred = globalThis.defer(); - server.delete('/api/v1/crates/:crate_id/follow', globalThis.unfollowDeferred.promise); - }); + let unfollowDeferred = defer(); + await msw.worker.use(http.delete('/api/v1/crates/:crate_id/follow', () => unfollowDeferred.promise)); await followButton.click(); await expect(followButton).toHaveText('Loading…'); await expect(followButton).toBeDisabled(); await expect(spinner).toBeVisible(); - await page.evaluate(() => globalThis.unfollowDeferred.resolve({ ok: true })); + unfollowDeferred.resolve(HttpResponse.json({ ok: true })); await expect(followButton).toHaveText('Follow'); await expect(followButton).toBeEnabled(); await expect(spinner).toHaveCount(0); }); - test('error handling when loading following state fails', async ({ mirage, page }) => { - await mirage.addHook(server => { - server.get('/api/v1/crates/:crate_id/following', {}, 500); - }); + test('error handling when loading following state fails', async ({ msw, page }) => { + await prepare(msw); + + let error = HttpResponse.json({}, { status: 500 }); + await msw.worker.use(http.get('/api/v1/crates/:crate_id/following', () => error)); await page.goto('/crates/nanomsg'); const followButton = page.locator('[data-test-follow-button]'); @@ -87,10 +81,11 @@ test.describe('Acceptance | Crate following', { tag: '@acceptance' }, () => { ); }); - test('error handling when follow fails', async ({ mirage, page }) => { - await mirage.addHook(server => { - server.put('/api/v1/crates/:crate_id/follow', {}, 500); - }); + test('error handling when follow fails', async ({ msw, page }) => { + await prepare(msw); + + let error = HttpResponse.json({}, { status: 500 }); + await msw.worker.use(http.put('/api/v1/crates/:crate_id/follow', () => error)); await page.goto('/crates/nanomsg'); await page.locator('[data-test-follow-button]').click(); @@ -99,13 +94,11 @@ test.describe('Acceptance | Crate following', { tag: '@acceptance' }, () => { ); }); - test('error handling when unfollow fails', async ({ mirage, page }) => { - await page.addInitScript(() => { - globalThis.following = true; - }); - await mirage.addHook(server => { - server.del('/api/v1/crates/:crate_id/follow', {}, 500); - }); + test('error handling when unfollow fails', async ({ msw, page }) => { + await prepare(msw, { following: true }); + + let error = HttpResponse.json({}, { status: 500 }); + await msw.worker.use(http.delete('/api/v1/crates/:crate_id/follow', () => error)); await page.goto('/crates/nanomsg'); await page.locator('[data-test-follow-button]').click(); From d7baf9f08ed650fcab9fa045606670dd5aed02cb Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 14:33:39 +0100 Subject: [PATCH 156/189] e2e/acceptance/crate-navtabs: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/crate-navtabs.spec.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/e2e/acceptance/crate-navtabs.spec.ts b/e2e/acceptance/crate-navtabs.spec.ts index cc49c8f09d4..2fb880b23f7 100644 --- a/e2e/acceptance/crate-navtabs.spec.ts +++ b/e2e/acceptance/crate-navtabs.spec.ts @@ -2,11 +2,9 @@ import { test, expect } from '@/e2e/helper'; import { Locator } from '@playwright/test'; test.describe('Acceptance | crate navigation tabs', { tag: '@acceptance' }, () => { - test('basic navigation between tabs works as expected', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '0.6.1' }); - }); + test('basic navigation between tabs works as expected', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate, num: '0.6.1' }); const tabReadme = page.locator('[data-test-readme-tab] a'); const tabVersions = page.locator('[data-test-versions-tab] a'); From de1e94a7d7f817d95688b960291b98f66d3109e6 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 14:35:02 +0100 Subject: [PATCH 157/189] e2e/acceptance/crates: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/crates.spec.ts | 59 ++++++++++++++--------------------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/e2e/acceptance/crates.spec.ts b/e2e/acceptance/crates.spec.ts index e12f52507b7..986bb9135aa 100644 --- a/e2e/acceptance/crates.spec.ts +++ b/e2e/acceptance/crates.spec.ts @@ -1,13 +1,12 @@ import { expect, test } from '@/e2e/helper'; +import { loadFixtures } from '@crates-io/msw/fixtures'; test.describe('Acceptance | crates page', { tag: '@acceptance' }, () => { // should match the default set in the crates controller const per_page = 50; - test('visiting the crates page from the front page', async ({ page, mirage, percy, a11y }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('visiting the crates page from the front page', async ({ page, msw, percy, a11y }) => { + loadFixtures(msw.db); await page.goto('/'); await page.click('[data-test-all-crates-link]'); @@ -19,10 +18,8 @@ test.describe('Acceptance | crates page', { tag: '@acceptance' }, () => { await a11y.audit(); }); - test('visiting the crates page directly', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('visiting the crates page directly', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/crates'); await page.click('[data-test-all-crates-link]'); @@ -31,14 +28,12 @@ test.describe('Acceptance | crates page', { tag: '@acceptance' }, () => { await expect(page).toHaveTitle('Crates - crates.io: Rust Package Registry'); }); - test('listing crates', async ({ page, mirage }) => { - await mirage.addHook(server => { - const per_page = 50; - for (let i = 1; i <= per_page; i++) { - let crate = server.create('crate'); - server.create('version', { crate }); - } - }); + test('listing crates', async ({ page, msw }) => { + const per_page = 50; + for (let i = 1; i <= per_page; i++) { + let crate = msw.db.crate.create(); + msw.db.version.create({ crate }); + } await page.goto('/crates'); @@ -46,14 +41,12 @@ test.describe('Acceptance | crates page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-crates-nav] [data-test-total-rows]')).toHaveText(`${per_page}`); }); - test('navigating to next page of crates', async ({ page, mirage }) => { - await mirage.addHook(server => { - const per_page = 50; - for (let i = 1; i <= per_page + 2; i++) { - let crate = server.create('crate'); - server.create('version', { crate }); - } - }); + test('navigating to next page of crates', async ({ page, msw }) => { + const per_page = 50; + for (let i = 1; i <= per_page + 2; i++) { + let crate = msw.db.crate.create(); + msw.db.version.create({ crate }); + } const page_start = per_page + 1; const total = per_page + 2; @@ -65,29 +58,23 @@ test.describe('Acceptance | crates page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-crates-nav] [data-test-total-rows]')).toHaveText(`${total}`); }); - test('crates default sort is alphabetical', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('crates default sort is alphabetical', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/crates'); await expect(page.locator('[data-test-crates-sort] [data-test-current-order]')).toHaveText('Recent Downloads'); }); - test('downloads appears for each crate on crate list', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('downloads appears for each crate on crate list', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/crates'); await expect(page.locator('[data-test-crate-row="0"] [data-test-downloads]')).toHaveText('All-Time: 21,573'); }); - test('recent downloads appears for each crate on crate list', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('recent downloads appears for each crate on crate list', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/crates'); await expect(page.locator('[data-test-crate-row="0"] [data-test-recent-downloads]')).toHaveText('Recent: 2,000'); From f8498a9b601f25dc9e2f54702118585991e25319 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 14:39:32 +0100 Subject: [PATCH 158/189] e2e/acceptance/dashboard: Migrate from `mirage` to `@crates-io/msw` --- app/templates/dashboard.hbs | 2 +- e2e/acceptance/dashboard.spec.ts | 79 +++++++++++++++++--------------- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/app/templates/dashboard.hbs b/app/templates/dashboard.hbs index 9a9dc67f5b2..7bce9c57399 100644 --- a/app/templates/dashboard.hbs +++ b/app/templates/dashboard.hbs @@ -45,7 +45,7 @@
-
    +
      {{#each this.myFeed as |version|}}
    • diff --git a/e2e/acceptance/dashboard.spec.ts b/e2e/acceptance/dashboard.spec.ts index b562f4d5d23..1baefd56928 100644 --- a/e2e/acceptance/dashboard.spec.ts +++ b/e2e/acceptance/dashboard.spec.ts @@ -1,4 +1,5 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Acceptance | Dashboard', { tag: '@acceptance' }, () => { test('shows "page requires authentication" error when not logged in', async ({ page }) => { @@ -8,47 +9,51 @@ test.describe('Acceptance | Dashboard', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-login]')).toBeVisible(); }); - test('shows the dashboard when logged in', async ({ page, mirage, percy }) => { - await mirage.addHook(server => { - let user = server.create('user', { - login: 'johnnydee', - name: 'John Doe', - email: 'john@doe.com', - avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', - }); - - authenticateAs(user); - - { - let crate = server.create('crate', { name: 'rand' }); - server.create('version', { crate, num: '0.5.0' }); - server.create('version', { crate, num: '0.6.0' }); - server.create('version', { crate, num: '0.7.0' }); - server.create('version', { crate, num: '0.7.1' }); - server.create('version', { crate, num: '0.7.2' }); - server.create('version', { crate, num: '0.7.3' }); - server.create('version', { crate, num: '0.8.0' }); - server.create('version', { crate, num: '0.8.1' }); - server.create('version', { crate, num: '0.9.0' }); - server.create('version', { crate, num: '1.0.0' }); - server.create('version', { crate, num: '1.1.0' }); - user.followedCrates.add(crate); - } - - { - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('crate-ownership', { crate, user }); - server.create('version', { crate, num: '0.1.0' }); - user.followedCrates.add(crate); - } + test('shows the dashboard when logged in', async ({ page, msw, percy }) => { + let user = msw.db.user.create({ + login: 'johnnydee', + name: 'John Doe', + email: 'john@doe.com', + avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', + }); - user.save(); + await msw.authenticateAs(user); + + { + let crate = msw.db.crate.create({ name: 'rand' }); + msw.db.version.create({ crate, num: '0.5.0' }); + msw.db.version.create({ crate, num: '0.6.0' }); + msw.db.version.create({ crate, num: '0.7.0' }); + msw.db.version.create({ crate, num: '0.7.1' }); + msw.db.version.create({ crate, num: '0.7.2' }); + msw.db.version.create({ crate, num: '0.7.3' }); + msw.db.version.create({ crate, num: '0.8.0' }); + msw.db.version.create({ crate, num: '0.8.1' }); + msw.db.version.create({ crate, num: '0.9.0' }); + msw.db.version.create({ crate, num: '1.0.0' }); + msw.db.version.create({ crate, num: '1.1.0' }); + user = msw.db.user.update({ + where: { id: { equals: user.id } }, + data: { followedCrates: [...user.followedCrates, crate] }, + }); + } + + { + let crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.crateOwnership.create({ crate, user }); + msw.db.version.create({ crate, num: '0.1.0' }); + user = msw.db.user.update({ + where: { id: { equals: user.id } }, + data: { followedCrates: [...user.followedCrates, crate] }, + }); + } - server.get(`/api/v1/users/${user.id}/stats`, { total_downloads: 3892 }); - }); + let response = HttpResponse.json({ total_downloads: 3892 }); + await msw.worker.use(http.get(`/api/v1/users/${user.id}/stats`, () => response)); await page.goto('/dashboard'); await expect(page).toHaveURL('/dashboard'); + await expect(page.locator('[data-test-feed-list]')).toBeVisible(); await percy.snapshot(); }); }); From 0d0a0682f67bd174182041914cda1eb3d4d17f6b Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 15:15:09 +0100 Subject: [PATCH 159/189] e2e/acceptance/email-change: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/email-change.spec.ts | 98 ++++++++++++----------------- 1 file changed, 40 insertions(+), 58 deletions(-) diff --git a/e2e/acceptance/email-change.spec.ts b/e2e/acceptance/email-change.spec.ts index 272e95b5562..b152bf2bf12 100644 --- a/e2e/acceptance/email-change.spec.ts +++ b/e2e/acceptance/email-change.spec.ts @@ -1,12 +1,10 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Acceptance | Email Change', { tag: '@acceptance' }, () => { - test('happy path', async ({ page, mirage }) => { - await mirage.addHook(server => { - let user = server.create('user', { email: 'old@email.com' }); - globalThis.user = user; - authenticateAs(user); - }); + test('happy path', async ({ page, msw }) => { + let user = msw.db.user.create({ email: 'old@email.com' }); + await msw.authenticateAs(user); await page.goto('/settings/profile'); await expect(page).toHaveURL('/settings/profile'); @@ -39,19 +37,15 @@ test.describe('Acceptance | Email Change', { tag: '@acceptance' }, () => { await expect(emailInput.locator('[data-test-verification-sent]')).toBeVisible(); await expect(emailInput.locator('[data-test-resend-button]')).toBeEnabled(); - await page.evaluate(() => globalThis.user.reload()); - await page.waitForFunction(expect => globalThis.user.email === expect, 'new@email.com'); - await page.waitForFunction(expect => globalThis.user.emailVerified === expect, false); - await page.waitForFunction(() => !!globalThis.user.emailVerificationToken); + user = msw.db.user.findFirst({ where: { id: { equals: user.id } } }); + await expect(user.email).toBe('new@email.com'); + await expect(user.emailVerified).toBe(false); + await expect(user.emailVerificationToken).toBeDefined(); }); - test('happy path with `email: null`', async ({ page, mirage }) => { - await mirage.addHook(server => { - let user = server.create('user', { email: undefined }); - - authenticateAs(user); - globalThis.user = user; - }); + test('happy path with `email: null`', async ({ page, msw }) => { + let user = msw.db.user.create({ email: undefined }); + await msw.authenticateAs(user); await page.goto('/settings/profile'); await expect(page).toHaveURL('/settings/profile'); @@ -80,19 +74,15 @@ test.describe('Acceptance | Email Change', { tag: '@acceptance' }, () => { await expect(emailInput.locator('[data-test-verification-sent]')).toBeVisible(); await expect(emailInput.locator('[data-test-resend-button]')).toBeEnabled(); - await page.evaluate(() => globalThis.user.reload()); - await page.waitForFunction(expect => globalThis.user.email === expect, 'new@email.com'); - await page.waitForFunction(expect => globalThis.user.emailVerified === expect, false); - await page.waitForFunction(() => !!globalThis.user.emailVerificationToken); + user = msw.db.user.findFirst({ where: { id: { equals: user.id } } }); + await expect(user.email).toBe('new@email.com'); + await expect(user.emailVerified).toBe(false); + await expect(user.emailVerificationToken).toBeDefined(); }); - test('cancel button', async ({ page, mirage }) => { - await mirage.addHook(server => { - let user = server.create('user', { email: 'old@email.com' }); - - authenticateAs(user); - globalThis.user = user; - }); + test('cancel button', async ({ page, msw }) => { + let user = msw.db.user.create({ email: 'old@email.com' }); + await msw.authenticateAs(user); await page.goto('/settings/profile'); const emailInput = page.locator('[data-test-email-input]'); @@ -106,21 +96,18 @@ test.describe('Acceptance | Email Change', { tag: '@acceptance' }, () => { await expect(emailInput.locator('[data-test-not-verified]')).toHaveCount(0); await expect(emailInput.locator('[data-test-verification-sent]')).toHaveCount(0); - await page.evaluate(() => globalThis.user.reload()); - await page.waitForFunction(expect => globalThis.user.email === expect, 'old@email.com'); - await page.waitForFunction(expect => globalThis.user.emailVerified === expect, true); - await page.waitForFunction(() => !globalThis.user.emailVerificationToken); + user = msw.db.user.findFirst({ where: { id: { equals: user.id } } }); + await expect(user.email).toBe('old@email.com'); + await expect(user.emailVerified).toBe(true); + await expect(user.emailVerificationToken).toBe(null); }); - test('server error', async ({ page, mirage }) => { - await mirage.addHook(server => { - let user = server.create('user', { email: 'old@email.com' }); + test('server error', async ({ page, msw }) => { + let user = msw.db.user.create({ email: 'old@email.com' }); + await msw.authenticateAs(user); - authenticateAs(user); - globalThis.user = user; - - server.put('/api/v1/users/:user_id', {}, 500); - }); + let error = HttpResponse.json({}, { status: 500 }); + await msw.worker.use(http.put('/api/v1/users/:user_id', () => error)); await page.goto('/settings/profile'); const emailInput = page.locator('[data-test-email-input]'); @@ -134,19 +121,16 @@ test.describe('Acceptance | Email Change', { tag: '@acceptance' }, () => { 'Error in saving email: An unknown error occurred while saving this email.', ); - await page.evaluate(() => globalThis.user.reload()); - await page.waitForFunction(expect => globalThis.user.email === expect, 'old@email.com'); - await page.waitForFunction(expect => globalThis.user.emailVerified === expect, true); - await page.waitForFunction(() => !globalThis.user.emailVerificationToken); + user = msw.db.user.findFirst({ where: { id: { equals: user.id } } }); + await expect(user.email).toBe('old@email.com'); + await expect(user.emailVerified).toBe(true); + await expect(user.emailVerificationToken).toBe(null); }); test.describe('Resend button', function () { - test('happy path', async ({ page, mirage }) => { - await mirage.addHook(server => { - let user = server.create('user', { email: 'john@doe.com', emailVerificationToken: 'secret123' }); - - authenticateAs(user); - }); + test('happy path', async ({ page, msw }) => { + let user = msw.db.user.create({ email: 'john@doe.com', emailVerificationToken: 'secret123' }); + await msw.authenticateAs(user); await page.goto('/settings/profile'); await expect(page).toHaveURL('/settings/profile'); @@ -165,14 +149,12 @@ test.describe('Acceptance | Email Change', { tag: '@acceptance' }, () => { await expect(button).toHaveText('Sent!'); }); - test('server error', async ({ page, mirage }) => { - await mirage.addHook(server => { - let user = server.create('user', { email: 'john@doe.com', emailVerificationToken: 'secret123' }); - - authenticateAs(user); + test('server error', async ({ page, msw }) => { + let user = msw.db.user.create({ email: 'john@doe.com', emailVerificationToken: 'secret123' }); + await msw.authenticateAs(user); - server.put('/api/v1/users/:user_id/resend', {}, 500); - }); + let error = HttpResponse.json({}, { status: 500 }); + await msw.worker.use(http.put('/api/v1/users/:user_id/resend', () => error)); await page.goto('/settings/profile'); await expect(page).toHaveURL('/settings/profile'); From 8a43670c3a99a34298042d09847f490024fa8120 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 15:15:15 +0100 Subject: [PATCH 160/189] e2e/acceptance/email-confirmation: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/email-confirmation.spec.ts | 30 +++++++++-------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/e2e/acceptance/email-confirmation.spec.ts b/e2e/acceptance/email-confirmation.spec.ts index 086e60e5fea..034a5d144dc 100644 --- a/e2e/acceptance/email-confirmation.spec.ts +++ b/e2e/acceptance/email-confirmation.spec.ts @@ -1,31 +1,25 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; test.describe('Acceptance | Email Confirmation', { tag: '@acceptance' }, () => { - test('unauthenticated happy path', async ({ page, mirage }) => { - await mirage.addHook(server => { - let user = server.create('user', { emailVerificationToken: 'badc0ffee' }); - globalThis.user = user; - }); + test('unauthenticated happy path', async ({ page, msw }) => { + let user = msw.db.user.create({ emailVerificationToken: 'badc0ffee' }); await page.goto('/confirm/badc0ffee'); - await page.waitForFunction(expect => globalThis.user.emailVerified === expect, false); + await expect(user.emailVerified).toBe(false); await expect(page).toHaveURL('/'); await expect(page.locator('[data-test-notification-message="success"]')).toBeVisible(); - await page.evaluate(() => globalThis.user.reload()); - await page.waitForFunction(expect => globalThis.user.emailVerified === expect, true); + user = msw.db.user.findFirst({ where: { id: { equals: user.id } } }); + await expect(user.emailVerified).toBe(true); }); - test('authenticated happy path', async ({ page, mirage, ember }) => { - await mirage.addHook(server => { - let user = server.create('user', { emailVerificationToken: 'badc0ffee' }); + test('authenticated happy path', async ({ page, msw, ember }) => { + let user = msw.db.user.create({ emailVerificationToken: 'badc0ffee' }); - authenticateAs(user); - globalThis.user = user; - }); + await msw.authenticateAs(user); await page.goto('/confirm/badc0ffee'); - await page.waitForFunction(expect => globalThis.user.emailVerified === expect, false); + await expect(user.emailVerified).toBe(false); await expect(page).toHaveURL('/'); await expect(page.locator('[data-test-notification-message="success"]')).toBeVisible(); @@ -35,8 +29,8 @@ test.describe('Acceptance | Email Confirmation', { tag: '@acceptance' }, () => { }); expect(emailVerified).toBe(true); - await page.evaluate(() => globalThis.user.reload()); - await page.waitForFunction(expect => globalThis.user.emailVerified === expect, true); + user = msw.db.user.findFirst({ where: { id: { equals: user.id } } }); + await expect(user.emailVerified).toBe(true); }); test('error case', async ({ page }) => { From 55648e1d0d418cae3920e0abbe447c92acda5fd3 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 15:29:45 +0100 Subject: [PATCH 161/189] e2e/acceptance/invites: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/invites.spec.ts | 225 ++++++++++++++++++--------------- 1 file changed, 123 insertions(+), 102 deletions(-) diff --git a/e2e/acceptance/invites.spec.ts b/e2e/acceptance/invites.spec.ts index c42875d865c..55f3d1888a8 100644 --- a/e2e/acceptance/invites.spec.ts +++ b/e2e/acceptance/invites.spec.ts @@ -1,4 +1,5 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Acceptance | /me/pending-invites', { tag: '@acceptance' }, () => { test('shows "page requires authentication" error when not logged in', async ({ page }) => { @@ -10,38 +11,38 @@ test.describe('Acceptance | /me/pending-invites', { tag: '@acceptance' }, () => }); test.describe('Acceptance | /me/pending-invites', { tag: '@acceptance' }, () => { - test.beforeEach(async ({ mirage }) => { - await mirage.addHook(server => { - let inviter = server.create('user', { name: 'janed' }); - let inviter2 = server.create('user', { name: 'wycats' }); - - let user = server.create('user'); - - let nanomsg = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate: nanomsg }); - server.create('crate-owner-invitation', { - crate: nanomsg, - createdAt: '2016-12-24T12:34:56Z', - invitee: user, - inviter, - }); - - let ember = server.create('crate', { name: 'ember-rs' }); - server.create('version', { crate: ember }); - server.create('crate-owner-invitation', { - crate: ember, - createdAt: '2020-12-31T12:34:56Z', - invitee: user, - inviter: inviter2, - }); - - authenticateAs(user); - - Object.assign(globalThis, { nanomsg, user }); + async function prepare(msw) { + let inviter = msw.db.user.create({ name: 'janed' }); + let inviter2 = msw.db.user.create({ name: 'wycats' }); + + let user = msw.db.user.create(); + + let nanomsg = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate: nanomsg }); + msw.db.crateOwnerInvitation.create({ + crate: nanomsg, + createdAt: '2016-12-24T12:34:56Z', + invitee: user, + inviter, }); - }); - test('list all pending crate owner invites', async ({ page }) => { + let ember = msw.db.crate.create({ name: 'ember-rs' }); + msw.db.version.create({ crate: ember }); + msw.db.crateOwnerInvitation.create({ + crate: ember, + createdAt: '2020-12-31T12:34:56Z', + invitee: user, + inviter: inviter2, + }); + + await msw.authenticateAs(user); + + return { nanomsg, user }; + } + + test('list all pending crate owner invites', async ({ page, msw }) => { + await prepare(msw); + await page.goto('/me/pending-invites'); await expect(page).toHaveURL('/me/pending-invites'); await expect(page.locator('[data-test-invite]')).toHaveCount(2); @@ -67,10 +68,9 @@ test.describe('Acceptance | /me/pending-invites', { tag: '@acceptance' }, () => await expect(page.locator('[data-test-declined-message]')).toHaveCount(0); }); - test('shows empty list message', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.schema['crateOwnerInvitations'].all().destroy(); - }); + test('shows empty list message', async ({ page, msw }) => { + await prepare(msw); + msw.db.crateOwnerInvitation.deleteMany({}); await page.goto('/me/pending-invites'); await expect(page).toHaveURL('/me/pending-invites'); @@ -78,52 +78,64 @@ test.describe('Acceptance | /me/pending-invites', { tag: '@acceptance' }, () => await expect(page.locator('[data-test-empty-state]')).toBeVisible(); }); - test('invites can be declined', async ({ page }) => { - await page.goto('/me/pending-invites'); - await expect(page).toHaveURL('/me/pending-invites'); + test('invites can be declined', async ({ page, msw }) => { + let { nanomsg, user } = await prepare(msw); - await page.waitForFunction(expect => { - const { crateOwnerInvitations }: any = server.schema; - const { nanomsg, user }: any = globalThis; - return crateOwnerInvitations.where({ crateId: nanomsg.id, inviteeId: user.id }).length === expect; - }, 1); + let invites = msw.db.crateOwnerInvitation.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + invitee: { id: { equals: user.id } }, + }, + }); + expect(invites.length).toBe(1); - await page.waitForFunction(expect => { - const { crateOwnerships }: any = server.schema; - return crateOwnerships.where({ crateId: globalThis.nanomsg.id, userId: globalThis.user.id }).length === expect; - }, 0); + let owners = msw.db.crateOwnership.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + user: { id: { equals: user.id } }, + }, + }); + expect(owners.length).toBe(0); - const nanomsg = page.locator('[data-test-invite="nanomsg"]'); - await nanomsg.locator('[data-test-decline-button]').click(); - await expect(nanomsg.and(page.locator('[data-test-declined-message]'))).toHaveText( + await page.goto('/me/pending-invites'); + await expect(page).toHaveURL('/me/pending-invites'); + + const nanomsgL = page.locator('[data-test-invite="nanomsg"]'); + await nanomsgL.locator('[data-test-decline-button]').click(); + await expect(nanomsgL.and(page.locator('[data-test-declined-message]'))).toHaveText( 'Declined. You have not been added as an owner of crate nanomsg.', ); - await expect(nanomsg.locator('[data-test-crate-link]')).toHaveCount(0); - await expect(nanomsg.locator('[data-test-inviter-link]')).toHaveCount(0); + await expect(nanomsgL.locator('[data-test-crate-link]')).toHaveCount(0); + await expect(nanomsgL.locator('[data-test-inviter-link]')).toHaveCount(0); await expect(page.locator('[data-test-error-message]')).toHaveCount(0); await expect(page.locator('[data-test-accepted-message]')).toHaveCount(0); - await page.waitForFunction(expect => { - const { crateOwnerInvitations }: any = server.schema; - const { nanomsg, user }: any = globalThis; - return crateOwnerInvitations.where({ crateId: nanomsg.id, inviteeId: user.id }).length === expect; - }, 0); - - await page.waitForFunction(expect => { - const { crateOwnerships }: any = server.schema; - const { nanomsg, user }: any = globalThis; - return crateOwnerships.where({ crateId: nanomsg.id, userId: user.id }).length === expect; - }, 0); + invites = msw.db.crateOwnerInvitation.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + invitee: { id: { equals: user.id } }, + }, + }); + expect(invites.length).toBe(0); + + owners = msw.db.crateOwnership.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + user: { id: { equals: user.id } }, + }, + }); + expect(owners.length).toBe(0); }); - test('error message is shown if decline request fails', async ({ page, mirage }) => { + test('error message is shown if decline request fails', async ({ page, msw }) => { + await prepare(msw); + await page.goto('/me/pending-invites'); await expect(page).toHaveURL('/me/pending-invites'); - await page.evaluate(() => { - server.put('/api/v1/me/crate_owner_invitations/:crate_id', {}, 500); - }); + let error = HttpResponse.json({}, { status: 500 }); + await msw.worker.use(http.put('/api/v1/me/crate_owner_invitations/:crate_id', () => error)); await page.click('[data-test-invite="nanomsg"] [data-test-decline-button]'); await expect(page.locator('[data-test-notification-message="error"]')).toContainText('Error in declining invite'); @@ -131,21 +143,27 @@ test.describe('Acceptance | /me/pending-invites', { tag: '@acceptance' }, () => await expect(page.locator('[data-test-declined-message]')).toHaveCount(0); }); - test('invites can be accepted', async ({ page, percy }) => { - await page.goto('/me/pending-invites'); - await expect(page).toHaveURL('/me/pending-invites'); + test('invites can be accepted', async ({ page, percy, msw }) => { + let { nanomsg, user } = await prepare(msw); - await page.waitForFunction(expect => { - const { crateOwnerInvitations }: any = server.schema; - const { nanomsg, user }: any = globalThis; - return crateOwnerInvitations.where({ crateId: nanomsg.id, inviteeId: user.id }).length === expect; - }, 1); + let invites = msw.db.crateOwnerInvitation.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + invitee: { id: { equals: user.id } }, + }, + }); + expect(invites.length).toBe(1); - await page.waitForFunction(expect => { - const { crateOwnerships }: any = server.schema; - const { nanomsg, user }: any = globalThis; - return crateOwnerships.where({ crateId: nanomsg.id, userId: user.id }).length === expect; - }, 0); + let owners = msw.db.crateOwnership.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + user: { id: { equals: user.id } }, + }, + }); + expect(owners.length).toBe(0); + + await page.goto('/me/pending-invites'); + await expect(page).toHaveURL('/me/pending-invites'); await page.click('[data-test-invite="nanomsg"] [data-test-accept-button]'); await expect(page.locator('[data-test-error-message]')).toHaveCount(0); @@ -158,26 +176,31 @@ test.describe('Acceptance | /me/pending-invites', { tag: '@acceptance' }, () => await percy.snapshot(); - await page.waitForFunction(expect => { - const { crateOwnerInvitations }: any = server.schema; - const { nanomsg, user }: any = globalThis; - return crateOwnerInvitations.where({ crateId: nanomsg.id, inviteeId: user.id }).length === expect; - }, 0); - - await page.waitForFunction(expect => { - const { crateOwnerships }: any = server.schema; - const { nanomsg, user }: any = globalThis; - return crateOwnerships.where({ crateId: nanomsg.id, userId: user.id }).length === expect; - }, 1); + invites = msw.db.crateOwnerInvitation.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + invitee: { id: { equals: user.id } }, + }, + }); + expect(invites.length).toBe(0); + + owners = msw.db.crateOwnership.findMany({ + where: { + crate: { id: { equals: nanomsg.id } }, + user: { id: { equals: user.id } }, + }, + }); + expect(owners.length).toBe(1); }); - test('error message is shown if accept request fails', async ({ page }) => { + test('error message is shown if accept request fails', async ({ page, msw }) => { + await prepare(msw); + await page.goto('/me/pending-invites'); await expect(page).toHaveURL('/me/pending-invites'); - page.evaluate(() => { - server.put('/api/v1/me/crate_owner_invitations/:crate_id', {}, 500); - }); + let error = HttpResponse.json({}, { status: 500 }); + await msw.worker.use(http.put('/api/v1/me/crate_owner_invitations/:crate_id', () => error)); await page.click('[data-test-invite="nanomsg"] [data-test-accept-button]'); await expect(page.locator('[data-test-notification-message="error"]')).toHaveText('Error in accepting invite'); @@ -185,15 +208,13 @@ test.describe('Acceptance | /me/pending-invites', { tag: '@acceptance' }, () => await expect(page.locator('[data-test-declined-message]')).toHaveCount(0); }); - test('specific error message is shown if accept request fails', async ({ page, mirage }) => { + test('specific error message is shown if accept request fails', async ({ page, msw }) => { + await prepare(msw); + let errorMessage = 'The invitation to become an owner of the demo_crate crate expired. Please reach out to an owner of the crate to request a new invitation.'; - await page.exposeBinding('_errorMessage', () => errorMessage); - await mirage.addHook(async server => { - let errorMessage = await globalThis._errorMessage(); - let payload = { errors: [{ detail: errorMessage }] }; - server.put('/api/v1/me/crate_owner_invitations/:crate_id', payload, 410); - }); + let error = HttpResponse.json({ errors: [{ detail: errorMessage }] }, { status: 410 }); + await msw.worker.use(http.put('/api/v1/me/crate_owner_invitations/:crate_id', () => error)); await page.goto('/me/pending-invites'); await expect(page).toHaveURL('/me/pending-invites'); From 7f9a3883523dc43fdd97be35aacffc0d08abeb0d Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 15:31:21 +0100 Subject: [PATCH 162/189] e2e/acceptance/keyword: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/keyword.spec.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/e2e/acceptance/keyword.spec.ts b/e2e/acceptance/keyword.spec.ts index 3160621e2f4..357b9d77f6c 100644 --- a/e2e/acceptance/keyword.spec.ts +++ b/e2e/acceptance/keyword.spec.ts @@ -1,10 +1,8 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; test.describe('Acceptance | keywords', { tag: '@acceptance' }, () => { - test('keyword/:keyword_id index default sort is recent-downloads', async ({ page, mirage, percy, a11y }) => { - await mirage.addHook(server => { - server.create('keyword', { keyword: 'network' }); - }); + test('keyword/:keyword_id index default sort is recent-downloads', async ({ page, msw, percy, a11y }) => { + msw.db.keyword.create({ keyword: 'network' }); await page.goto('/keywords/network'); From 340e3bea849a3f3b510181acee4177fe59267bdf Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 15:37:20 +0100 Subject: [PATCH 163/189] e2e/acceptance/login: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/login.spec.ts | 71 ++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/e2e/acceptance/login.spec.ts b/e2e/acceptance/login.spec.ts index a33af5f5c1f..aaeb9704747 100644 --- a/e2e/acceptance/login.spec.ts +++ b/e2e/acceptance/login.spec.ts @@ -1,7 +1,8 @@ import { test, expect } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Acceptance | Login', { tag: '@acceptance' }, () => { - test('successful login', async ({ page, mirage }) => { + test('successful login', async ({ page, msw }) => { // mock `window.open()` await page.addInitScript(() => { globalThis.open = (url, target, features) => { @@ -10,28 +11,32 @@ test.describe('Acceptance | Login', { tag: '@acceptance' }, () => { }; }); - await mirage.config({ trackRequests: true }); - await mirage.addHook(server => { - server.get('/api/private/session/begin', { url: 'url-to-github-including-state-secret' }); - - server.get('/api/private/session/authorize', () => { - let user = server.create('user'); - server.create('mirage-session', { user }); - return { ok: true }; - }); - - server.get('/api/v1/me', () => ({ - user: { - id: 42, - login: 'johnnydee', - name: 'John Doe', - email: 'john@doe.name', - avatar: 'https://avatars2.githubusercontent.com/u/12345?v=4', - url: 'https://github.com/johnnydee', - }, - owned_crates: [], - })); - }); + msw.worker.use( + http.get('/api/private/session/begin', () => HttpResponse.json({ url: 'url-to-github-including-state-secret' })), + http.get('/api/private/session/authorize', ({ request }) => { + let url = new URL(request.url); + expect([...url.searchParams.keys()]).toEqual(['code', 'state']); + expect(url.searchParams.get('code')).toBe('901dd10e07c7e9fa1cd5'); + expect(url.searchParams.get('state')).toBe('fYcUY3FMdUUz00FC7vLT7A'); + + let user = msw.db.user.create(); + msw.db.mswSession.create({ user }); + return HttpResponse.json({ ok: true }); + }), + http.get('/api/v1/me', () => + HttpResponse.json({ + user: { + id: 42, + login: 'johnnydee', + name: 'John Doe', + email: 'john@doe.name', + avatar: 'https://avatars2.githubusercontent.com/u/12345?v=4', + url: 'https://github.com/johnnydee', + }, + owned_crates: [], + }), + ), + ); await page.goto('/'); await expect(page).toHaveURL('/'); @@ -52,16 +57,10 @@ test.describe('Acceptance | Login', { tag: '@acceptance' }, () => { window.postMessage(message, window.location.origin); }, message); - const queryParams = await page.evaluate( - () => - server.pretender.handledRequests.find(req => req.url.startsWith('/api/private/session/authorize')).queryParams, - ); - expect(queryParams).toEqual(message); - await expect(page.locator('[data-test-user-menu] [data-test-toggle]')).toHaveText('John Doe'); }); - test('failed login', async ({ page, mirage }) => { + test('failed login', async ({ page, msw }) => { // mock `window.open()` await page.addInitScript(() => { globalThis.open = (url, target, features) => { @@ -70,12 +69,12 @@ test.describe('Acceptance | Login', { tag: '@acceptance' }, () => { }; }); - await mirage.addHook(server => { - server.get('/api/private/session/begin', { url: 'url-to-github-including-state-secret' }); - - let payload = { errors: [{ detail: 'Forbidden' }] }; - server.get('/api/private/session/authorize', payload, 403); - }); + msw.worker.use( + http.get('/api/private/session/begin', () => HttpResponse.json({ url: 'url-to-github-including-state-secret' })), + http.get('/api/private/session/authorize', () => + HttpResponse.json({ errors: [{ detail: 'Forbidden' }] }, { status: 403 }), + ), + ); await page.goto('/'); await expect(page).toHaveURL('/'); From 402072296e1f75bac41fd86512defce9a457c337 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 15:42:22 +0100 Subject: [PATCH 164/189] e2e/acceptance/logout: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/logout.spec.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/e2e/acceptance/logout.spec.ts b/e2e/acceptance/logout.spec.ts index 1dd424d6fb3..f4f281b9784 100644 --- a/e2e/acceptance/logout.spec.ts +++ b/e2e/acceptance/logout.spec.ts @@ -1,11 +1,9 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; test.describe('Acceptance | Logout', { tag: '@acceptance' }, () => { - test('successful logout', async ({ page, mirage }) => { - await mirage.addHook(server => { - let user = server.create('user', { name: 'John Doe' }); - authenticateAs(user); - }); + test('successful logout', async ({ page, msw }) => { + let user = msw.db.user.create({ name: 'John Doe' }); + await msw.authenticateAs(user); await page.goto('/crates'); await expect(page).toHaveURL('/crates'); From d26562ccf37390279b0f548a09cac3c99bfecfce Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 15:42:31 +0100 Subject: [PATCH 165/189] e2e/acceptance/publish-notifications: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/publish-notifications.spec.ts | 44 +++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/e2e/acceptance/publish-notifications.spec.ts b/e2e/acceptance/publish-notifications.spec.ts index 9feaac3e8e7..2d801fff89c 100644 --- a/e2e/acceptance/publish-notifications.spec.ts +++ b/e2e/acceptance/publish-notifications.spec.ts @@ -1,12 +1,11 @@ +import { defer } from '@/e2e/deferred'; import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Acceptance | publish notifications', { tag: '@acceptance' }, () => { - test('unsubscribe and resubscribe', async ({ page, mirage }) => { - await mirage.addHook(server => { - let user = server.create('user'); - globalThis.user = user; - authenticateAs(user); - }); + test('unsubscribe and resubscribe', async ({ page, msw }) => { + let user = msw.db.user.create(); + await msw.authenticateAs(user); await page.goto('/settings/profile'); await expect(page).toHaveURL('/settings/profile'); @@ -16,24 +15,23 @@ test.describe('Acceptance | publish notifications', { tag: '@acceptance' }, () = await expect(page.locator('[data-test-notifications] input[type=checkbox]')).not.toBeChecked(); await page.click('[data-test-notifications] button'); - await page.waitForFunction(() => globalThis.user.reload().publishNotifications === false); + user = msw.db.user.findFirst({ where: { id: { equals: user.id } } }); + expect(user.publishNotifications).toBe(false); await page.click('[data-test-notifications] input[type=checkbox]'); await expect(page.locator('[data-test-notifications] input[type=checkbox]')).toBeChecked(); await page.click('[data-test-notifications] button'); - await page.waitForFunction(() => globalThis.user.reload().publishNotifications === true); + user = msw.db.user.findFirst({ where: { id: { equals: user.id } } }); + expect(user.publishNotifications).toBe(true); }); - test('loading state', async ({ page, mirage }) => { - await mirage.addHook(server => { - let user = server.create('user'); - authenticateAs(user); - globalThis.user = user; + test('loading state', async ({ page, msw }) => { + let user = msw.db.user.create(); + await msw.authenticateAs(user); - globalThis.deferred = require('rsvp').defer(); - server.put('/api/v1/users/:user_id', globalThis.deferred.promise); - }); + let deferred = defer(); + msw.worker.use(http.put('/api/v1/users/:user_id', () => deferred.promise)); await page.goto('/settings/profile'); await expect(page).toHaveURL('/settings/profile'); @@ -44,20 +42,16 @@ test.describe('Acceptance | publish notifications', { tag: '@acceptance' }, () = await expect(page.locator('[data-test-notifications] input[type=checkbox]')).toBeDisabled(); await expect(page.locator('[data-test-notifications] button')).toBeDisabled(); - await page.evaluate(async () => globalThis.deferred.resolve()); + deferred.resolve(); await expect(page.locator('[data-test-notifications] [data-test-spinner]')).not.toBeVisible(); await expect(page.locator('[data-test-notification-message="error"]')).not.toBeVisible(); }); - test('error state', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.logging = true; - let user = server.create('user'); - authenticateAs(user); - globalThis.user = user; + test('error state', async ({ page, msw }) => { + let user = msw.db.user.create(); + await msw.authenticateAs(user); - server.put('/api/v1/users/:user_id', '', 500); - }); + msw.worker.use(http.put('/api/v1/users/:user_id', () => HttpResponse.text('', { status: 500 }))); await page.goto('/settings/profile'); await expect(page).toHaveURL('/settings/profile'); From 331eb63667bfe201d51a5a24289143d43e926e80 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 15:58:20 +0100 Subject: [PATCH 166/189] e2e/acceptance/read-only: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/read-only-mode.spec.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/e2e/acceptance/read-only-mode.spec.ts b/e2e/acceptance/read-only-mode.spec.ts index 9dd89787956..66954cbd3cf 100644 --- a/e2e/acceptance/read-only-mode.spec.ts +++ b/e2e/acceptance/read-only-mode.spec.ts @@ -1,4 +1,5 @@ import { test, expect, AppFixtures } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Acceptance | Read-only Mode', { tag: '@acceptance' }, () => { test.beforeEach(async ({ context }) => { @@ -12,29 +13,26 @@ test.describe('Acceptance | Read-only Mode', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-notification-message="info"]')).toHaveCount(0); }); - test('notification is shown for read-only mode', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.get('/api/v1/site_metadata', { read_only: true }); - }); + test('notification is shown for read-only mode', async ({ page, msw }) => { + let error = HttpResponse.json({}, { status: 500 }); + await msw.worker.use(http.put('/api/v1/me/crate_owner_invitations/:crate_id', () => error)); + + await msw.worker.use(http.get('/api/v1/site_metadata', () => HttpResponse.json({ read_only: true }))); await page.goto('/'); await expect(page.locator('[data-test-notification-message="info"]')).toContainText('read-only mode'); }); - test('server errors are handled gracefully', async ({ page, mirage, ember }) => { - await mirage.addHook(server => { - server.get('/api/v1/site_metadata', {}, 500); - }); + test('server errors are handled gracefully', async ({ page, msw, ember }) => { + await msw.worker.use(http.get('/api/v1/site_metadata', () => HttpResponse.json({}, { status: 500 }))); await page.goto('/'); await expect(page.locator('[data-test-notification-message="info"]')).toHaveCount(0); await checkSentryEventsNumber(ember, 0); }); - test('client errors are reported on sentry', async ({ page, mirage, ember }) => { - await mirage.addHook(server => { - server.get('/api/v1/site_metadata', {}, 404); - }); + test('client errors are reported on sentry', async ({ page, msw, ember }) => { + await msw.worker.use(http.get('/api/v1/site_metadata', () => HttpResponse.json({}, { status: 404 }))); await page.goto('/'); await expect(page.locator('[data-test-notification-message="info"]')).toHaveCount(0); From fa7bc921589edf5a567e0f1caa8dd959595285ba Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 15:58:29 +0100 Subject: [PATCH 167/189] e2e/acceptance/readme-rendering: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/readme-rendering.spec.ts | 43 ++++++++----------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/e2e/acceptance/readme-rendering.spec.ts b/e2e/acceptance/readme-rendering.spec.ts index 8b2093ed08f..40be046ed0f 100644 --- a/e2e/acceptance/readme-rendering.spec.ts +++ b/e2e/acceptance/readme-rendering.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from '@/e2e/helper'; -import { Response } from 'miragejs'; +import { http, HttpResponse } from 'msw'; const README_HTML = `

      Serde is a framework for serializing and deserializing Rust data structures efficiently and generically.

      @@ -85,14 +85,9 @@ graph TD; `; test.describe('Acceptance | README rendering', { tag: '@acceptance' }, () => { - test('it works', async ({ page, mirage, percy }) => { - await page.addInitScript(readmeHTML => { - globalThis.readmeHTML = readmeHTML; - }, README_HTML); - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'serde' }); - server.create('version', { crate, num: '1.0.0', readme: globalThis.readmeHTML }); - }); + test('it works', async ({ page, msw, percy }) => { + let crate = msw.db.crate.create({ name: 'serde' }); + msw.db.version.create({ crate, num: '1.0.0', readme: README_HTML }); await page.goto('/crates/serde'); const readme = page.locator('[data-test-readme]'); @@ -105,38 +100,28 @@ test.describe('Acceptance | README rendering', { tag: '@acceptance' }, () => { await percy.snapshot(); }); - test('it shows a fallback if no readme is available', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'serde' }); - server.create('version', { crate, num: '1.0.0' }); - }); + test('it shows a fallback if no readme is available', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'serde' }); + msw.db.version.create({ crate, num: '1.0.0' }); await page.goto('/crates/serde'); await expect(page.locator('[data-test-no-readme]')).toBeVisible(); }); - test('it shows an error message and retry button if loading fails', async ({ page, mirage }) => { - await page.exposeBinding('resp200', () => new Response(200, { 'Content-Type': 'text/html' }, 'foo')); + test('it shows an error message and retry button if loading fails', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'serde' }); + msw.db.version.create({ crate, num: '1.0.0', readme: 'foo' }); - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'serde' }); - server.create('version', { crate, num: '1.0.0' }); - - server.logging = true; - // Simulate a server error when fetching the README - server.get('/api/v1/crates/:name/:version/readme', {}, 500); - }); + // Simulate a server error when fetching the README + msw.worker.use(http.get('/api/v1/crates/:name/:version/readme', () => HttpResponse.html('', { status: 500 }))); await page.goto('/crates/serde'); await expect(page.locator('[data-test-readme-error]')).toBeVisible(); await expect(page.locator('[data-test-retry-button]')).toBeVisible(); - await page.evaluate(() => { - // Simulate a successful response when fetching the README - server.get('/api/v1/crates/:name/:version/readme', {}); - }); + await msw.worker.resetHandlers(); await page.click('[data-test-retry-button]'); - await expect(page.locator('[data-test-readme]')).toHaveText('{}'); + await expect(page.locator('[data-test-readme]')).toHaveText('foo'); }); }); From 234756ed8994ff89b27eaa4f967c8137b2f0becf Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 16:05:35 +0100 Subject: [PATCH 168/189] e2e/acceptance/reverse-dependencies: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/reverse-dependencies.spec.ts | 100 ++++++++------------ 1 file changed, 39 insertions(+), 61 deletions(-) diff --git a/e2e/acceptance/reverse-dependencies.spec.ts b/e2e/acceptance/reverse-dependencies.spec.ts index 206d8834383..e5cde39a127 100644 --- a/e2e/acceptance/reverse-dependencies.spec.ts +++ b/e2e/acceptance/reverse-dependencies.spec.ts @@ -1,71 +1,51 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Acceptance | /crates/:crate_id/reverse_dependencies', { tag: '@acceptance' }, () => { - test.beforeEach(async ({ page, mirage }) => { - await page.addInitScript(() => { - globalThis.foo = { name: 'foo' }; - globalThis.bar = { name: 'bar' }; - globalThis.baz = { name: 'baz' }; - }); - await mirage.addHook(server => { - console.log('[>>>] mirage'); - let foo = server.create('crate', globalThis.foo); - server.create('version', { crate: foo }); - - let bar = server.create('crate', globalThis.bar); - server.create('version', { crate: bar }); - - let baz = server.create('crate', globalThis.baz); - server.create('version', { crate: baz }); - - server.create('dependency', { crate: foo, version: bar.versions.models[0] }); - server.create('dependency', { crate: foo, version: baz.versions.models[0] }); - - globalThis.foo = foo; - globalThis.bar = bar; - globalThis.baz = baz; - }); - - // this allows us to evaluate the name before goingo to the actual page - await page.goto('about:blank'); - }); + function prepare(msw) { + let foo = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate: foo }); + + let bar = msw.db.crate.create({ name: 'bar' }); + let barV = msw.db.version.create({ crate: bar }); + + let baz = msw.db.crate.create({ name: 'baz' }); + let bazV = msw.db.version.create({ crate: baz }); - test('shows a list of crates depending on the selected crate', async ({ page }) => { - const foo = await page.evaluate(() => globalThis.foo); + msw.db.dependency.create({ crate: foo, version: barV }); + msw.db.dependency.create({ crate: foo, version: bazV }); + + return { foo, bar, baz }; + } + + test('shows a list of crates depending on the selected crate', async ({ page, msw }) => { + let { foo, bar, baz } = prepare(msw); await page.goto(`/crates/${foo.name}/reverse_dependencies`); await expect(page).toHaveURL(`/crates/${foo.name}/reverse_dependencies`); - const { bar, baz } = await page.evaluate(() => { - const val = item => ({ name: item.name, description: item.description }); - return { bar: val(bar), baz: val(baz) }; - }); - await expect(page.locator('[data-test-row]')).toHaveCount(2); const row0 = page.locator('[data-test-row="0"]'); - await expect(row0.locator('[data-test-crate-name]')).toHaveText(bar.name); - await expect(row0.locator('[data-test-description]')).toHaveText(bar.description); + await expect(row0.locator('[data-test-crate-name]')).toHaveText(baz.name); + await expect(row0.locator('[data-test-description]')).toHaveText(baz.description); const row1 = page.locator('[data-test-row="1"]'); - await expect(row1.locator('[data-test-crate-name]')).toHaveText(baz.name); - await expect(row1.locator('[data-test-description]')).toHaveText(baz.description); + await expect(row1.locator('[data-test-crate-name]')).toHaveText(bar.name); + await expect(row1.locator('[data-test-description]')).toHaveText(bar.description); }); - test('supports pagination', async ({ page, mirage }) => { - await mirage.addHook(server => { - let foo = globalThis.foo; + test('supports pagination', async ({ page, msw }) => { + let { foo } = prepare(msw); - for (let i = 0; i < 20; i++) { - let crate = server.create('crate'); - let version = server.create('version', { crate }); - server.create('dependency', { crate: foo, version }); - } - }); + for (let i = 0; i < 20; i++) { + let crate = msw.db.crate.create(); + let version = msw.db.version.create({ crate }); + msw.db.dependency.create({ crate: foo, version }); + } const row = page.locator('[data-test-row]'); const currentRows = page.locator('[data-test-current-rows]'); const totalRows = page.locator('[data-test-total-rows]'); - const foo = await page.evaluate(() => globalThis.foo); await page.goto(`/crates/${foo.name}/reverse_dependencies`); await expect(page).toHaveURL(`/crates/${foo.name}/reverse_dependencies`); await expect(row).toHaveCount(10); @@ -85,12 +65,11 @@ test.describe('Acceptance | /crates/:crate_id/reverse_dependencies', { tag: '@ac await expect(totalRows).toHaveText('22'); }); - test('shows a generic error if the server is broken', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.get('/api/v1/crates/:crate_id/reverse_dependencies', {}, 500); - }); + test('shows a generic error if the server is broken', async ({ page, msw }) => { + let { foo } = prepare(msw); - const foo = await page.evaluate(() => globalThis.foo); + let error = HttpResponse.json({}, { status: 500 }); + await msw.worker.use(http.get('/api/v1/crates/:crate_id/reverse_dependencies', () => error)); await page.goto(`/crates/${foo.name}/reverse_dependencies`); await expect(page).toHaveURL('/'); @@ -99,13 +78,12 @@ test.describe('Acceptance | /crates/:crate_id/reverse_dependencies', { tag: '@ac ); }); - test('shows a detailed error if available', async ({ page, mirage }) => { - await mirage.addHook(server => { - let payload = { errors: [{ detail: 'cannot request more than 100 items' }] }; - server.get('/api/v1/crates/:crate_id/reverse_dependencies', payload, 400); - }); + test('shows a detailed error if available', async ({ page, msw }) => { + let { foo } = prepare(msw); - const foo = await page.evaluate(() => globalThis.foo); + let payload = { errors: [{ detail: 'cannot request more than 100 items' }] }; + let error = HttpResponse.json(payload, { status: 400 }); + await msw.worker.use(http.get('/api/v1/crates/:crate_id/reverse_dependencies', () => error)); await page.goto(`/crates/${foo.name}/reverse_dependencies`); await expect(page).toHaveURL('/'); From eb1e267fa1b0fa2f71840aad8be3e23ad9cd0ddf Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 16:16:38 +0100 Subject: [PATCH 169/189] e2e/acceptance/search: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/search.spec.ts | 185 +++++++++++++++------------------- 1 file changed, 79 insertions(+), 106 deletions(-) diff --git a/e2e/acceptance/search.spec.ts b/e2e/acceptance/search.spec.ts index 64b0265a462..f32065be029 100644 --- a/e2e/acceptance/search.spec.ts +++ b/e2e/acceptance/search.spec.ts @@ -1,10 +1,11 @@ -import { test, expect } from '@/e2e/helper'; +import { defer } from '@/e2e/deferred'; +import { expect, test } from '@/e2e/helper'; +import { loadFixtures } from '@crates-io/msw/fixtures'; +import { http, HttpResponse } from 'msw'; test.describe('Acceptance | search', { tag: '@acceptance' }, () => { - test('searching for "rust"', async ({ page, mirage, percy, a11y }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('searching for "rust"', async ({ page, msw, percy, a11y }) => { + loadFixtures(msw.db); await page.goto('/'); await page.fill('[data-test-search-input]', 'rust'); @@ -31,10 +32,8 @@ test.describe('Acceptance | search', { tag: '@acceptance' }, () => { await a11y.audit(); }); - test('searching for "rust" from query', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('searching for "rust" from query', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/search?q=rust'); @@ -46,10 +45,8 @@ test.describe('Acceptance | search', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-search-nav]')).toHaveText('Displaying 1-7 of 7 total results'); }); - test('clearing search results', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('clearing search results', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/search?q=rust'); @@ -63,10 +60,8 @@ test.describe('Acceptance | search', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-search-input]')).toHaveValue(''); }); - test('pressing S key to focus the search bar', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('pressing S key to focus the search bar', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/'); @@ -94,10 +89,8 @@ test.describe('Acceptance | search', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-search-input]')).toBeFocused(); }); - test('check search results are by default displayed by relevance', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('check search results are by default displayed by relevance', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/'); await page.fill('[data-test-search-input]', 'rust'); @@ -106,14 +99,12 @@ test.describe('Acceptance | search', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-search-sort] [data-test-current-order]')).toHaveText('Relevance'); }); - test('error handling when searching from the frontpage', async ({ page, mirage }) => { - await mirage.addHook(server => { - globalThis._routes = server._config.routes; - let crate = server.create('crate', { name: 'rust' }); - server.create('version', { crate, num: '1.0.0' }); + test('error handling when searching from the frontpage', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'rust' }); + msw.db.version.create({ crate, num: '1.0.0' }); - server.get('/api/v1/crates', {}, 500); - }); + let error = HttpResponse.json({}, { status: 500 }); + await msw.worker.use(http.get('/api/v1/crates', () => error)); await page.goto('/'); await page.fill('[data-test-search-input]', 'rust'); @@ -122,10 +113,9 @@ test.describe('Acceptance | search', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-error-message]')).toBeVisible(); await expect(page.locator('[data-test-try-again-button]')).toBeEnabled(); - await page.evaluate(() => { - const deferred = (globalThis.deferred = require('rsvp').defer()); - server.get('/api/v1/crates', () => deferred.promise); - }); + await msw.worker.resetHandlers(); + let deferred = defer(); + await msw.worker.use(http.get('/api/v1/crates', () => deferred.promise)); await page.click('[data-test-try-again-button]'); await expect(page.locator('[data-test-page-header] [data-test-spinner]')).toBeVisible(); @@ -133,32 +123,23 @@ test.describe('Acceptance | search', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-error-message]')).toBeVisible(); await expect(page.locator('[data-test-try-again-button]')).toBeDisabled(); - await page.evaluate(async () => { - // Restore the routes - globalThis._routes.call(server); - const data = await globalThis.fetch('/api/v1/crates').then(r => r.json()); - globalThis.deferred.resolve(data); - }); + deferred.resolve(); await expect(page.locator('[data-test-error-message]')).toHaveCount(0); await expect(page.locator('[data-test-try-again-button]')).toHaveCount(0); await expect(page.locator('[data-test-crate-row]')).toHaveCount(1); }); - test('error handling when searching from the search page', async ({ page, mirage }) => { - await mirage.addHook(server => { - globalThis._routes = server._config.routes; - let crate = server.create('crate', { name: 'rust' }); - server.create('version', { crate, num: '1.0.0' }); - }); + test('error handling when searching from the search page', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'rust' }); + msw.db.version.create({ crate, num: '1.0.0' }); await page.goto('/search?q=rust'); await expect(page.locator('[data-test-crate-row]')).toHaveCount(1); await expect(page.locator('[data-test-error-message]')).toHaveCount(0); await expect(page.locator('[data-test-try-again-button]')).toHaveCount(0); - await page.evaluate(() => { - server.get('/api/v1/crates', {}, 500); - }); + let error = HttpResponse.json({}, { status: 500 }); + await msw.worker.use(http.get('/api/v1/crates', () => error)); await page.fill('[data-test-search-input]', 'ru'); await page.locator('[data-test-search-form]').getByRole('button', { name: 'Submit' }).click(); @@ -166,10 +147,9 @@ test.describe('Acceptance | search', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-error-message]')).toBeVisible(); await expect(page.locator('[data-test-try-again-button]')).toBeEnabled(); - await page.evaluate(() => { - const deferred = (globalThis.deferred = require('rsvp').defer()); - server.get('/api/v1/crates', () => deferred.promise); - }); + await msw.worker.resetHandlers(); + let deferred = defer(); + await msw.worker.use(http.get('/api/v1/crates', () => deferred.promise)); await page.click('[data-test-try-again-button]'); await expect(page.locator('[data-test-page-header] [data-test-spinner]')).toBeVisible(); @@ -177,76 +157,69 @@ test.describe('Acceptance | search', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-error-message]')).toBeVisible(); await expect(page.locator('[data-test-try-again-button]')).toBeDisabled(); - await page.evaluate(async () => { - // Restore the routes - globalThis._routes.call(server); - const data = await globalThis.fetch('/api/v1/crates').then(r => r.json()); - globalThis.deferred.resolve(data); - }); + deferred.resolve(); await expect(page.locator('[data-test-crate-row]')).toHaveCount(1); }); - test('passes query parameters to the backend', async ({ page, mirage }) => { - await mirage.config({ trackRequests: true }); - await mirage.addHook(server => { - server.get('/api/v1/crates', () => ({ crates: [], meta: { total: 0 } })); - }); + test('passes query parameters to the backend', async ({ page, msw }) => { + msw.worker.use( + http.get('/api/v1/crates', function ({ request }) { + let url = new URL(request.url); + expect(Object.fromEntries(url.searchParams.entries())).toEqual({ + all_keywords: 'fire ball', + page: '3', + per_page: '15', + q: 'rust', + sort: 'new', + }); + + return HttpResponse.json({ crates: [], meta: { total: 0 } }); + }), + ); await page.goto('/search?q=rust&page=3&per_page=15&sort=new&all_keywords=fire ball'); - const queryParams = await page.evaluate( - () => server.pretender.handledRequests.find(req => req.url.startsWith('/api/v1/crates')).queryParams, - ); - expect(queryParams).toEqual({ - all_keywords: 'fire ball', - page: '3', - per_page: '15', - q: 'rust', - sort: 'new', - }); }); - test('supports `keyword:bla` filters', async ({ page, mirage }) => { - await mirage.config({ trackRequests: true }); - await mirage.addHook(server => { - server.get('/api/v1/crates', () => ({ crates: [], meta: { total: 0 } })); - }); + test('supports `keyword:bla` filters', async ({ page, msw }) => { + msw.worker.use( + http.get('/api/v1/crates', function ({ request }) { + let url = new URL(request.url); + expect(Object.fromEntries(url.searchParams.entries())).toEqual({ + all_keywords: 'fire ball', + page: '3', + per_page: '15', + q: 'rust', + sort: 'new', + }); + + return HttpResponse.json({ crates: [], meta: { total: 0 } }); + }), + ); await page.goto('/search?q=rust keyword:fire keyword:ball&page=3&per_page=15&sort=new'); - const queryParams = await page.evaluate( - () => server.pretender.handledRequests.find(req => req.url.startsWith('/api/v1/crates')).queryParams, - ); - expect(queryParams).toEqual({ - all_keywords: 'fire ball', - page: '3', - per_page: '15', - q: 'rust', - sort: 'new', - }); }); - test('`all_keywords` query parameter takes precedence over `keyword` filters', async ({ page, mirage }) => { - await mirage.config({ trackRequests: true }); - await mirage.addHook(server => { - server.get('/api/v1/crates', () => ({ crates: [], meta: { total: 0 } })); - }); + test('`all_keywords` query parameter takes precedence over `keyword` filters', async ({ page, msw }) => { + msw.worker.use( + http.get('/api/v1/crates', function ({ request }) { + let url = new URL(request.url); + expect(Object.fromEntries(url.searchParams.entries())).toEqual({ + all_keywords: 'fire ball', + page: '3', + per_page: '15', + q: 'rust keywords:foo', + sort: 'new', + }); + + return HttpResponse.json({ crates: [], meta: { total: 0 } }); + }), + ); await page.goto('/search?q=rust keywords:foo&page=3&per_page=15&sort=new&all_keywords=fire ball'); - const queryParams = await page.evaluate( - () => server.pretender.handledRequests.find(req => req.url.startsWith('/api/v1/crates')).queryParams, - ); - expect(queryParams).toEqual({ - all_keywords: 'fire ball', - page: '3', - per_page: '15', - q: 'rust keywords:foo', - sort: 'new', - }); }); - test('visiting without query parameters works', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test('visiting without query parameters works', async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/search'); From 1f7c914136777ce6b96fcf68b195e9a2a0418c0a Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 16:34:54 +0100 Subject: [PATCH 170/189] e2e/acceptance/sudo: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/sudo.spec.ts | 75 +++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 41 deletions(-) diff --git a/e2e/acceptance/sudo.spec.ts b/e2e/acceptance/sudo.spec.ts index 5ba65a6e5fa..3970bbd60da 100644 --- a/e2e/acceptance/sudo.spec.ts +++ b/e2e/acceptance/sudo.spec.ts @@ -2,33 +2,32 @@ import { test, expect } from '@/e2e/helper'; import { format } from 'date-fns/format'; test.describe('Acceptance | sudo', { tag: '@acceptance' }, () => { - test.beforeEach(async ({ mirage }) => { - await mirage.addHook(server => { - const isAdmin = globalThis.isAdmin; - const user = server.create('user', { - login: 'johnnydee', - name: 'John Doe', - email: 'john@doe.com', - avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', - isAdmin, - }); - - const crate = server.create('crate', { - name: 'foo', - newest_version: '0.1.0', - }); - - const version = server.create('version', { - crate, - num: '0.1.0', - }); - - authenticateAs(user); + async function prepare(msw, { isAdmin = false } = {}) { + let user = msw.db.user.create({ + login: 'johnnydee', + name: 'John Doe', + email: 'john@doe.com', + avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', + isAdmin, + }); + + let crate = msw.db.crate.create({ + name: 'foo', + newest_version: '0.1.0', + }); + + let version = msw.db.version.create({ + crate, + num: '0.1.0', }); - }); - test('non-admin users do not see any controls', async ({ page }) => { - await page.addInitScript(() => (globalThis.isAdmin = false)); + await msw.authenticateAs(user); + + return { user, crate, version }; + } + + test('non-admin users do not see any controls', async ({ page, msw }) => { + await prepare(msw); await page.goto('/crates/foo/versions'); @@ -41,8 +40,8 @@ test.describe('Acceptance | sudo', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-version-yank-button="0.1.0"]')).toHaveCount(0); }); - test('admin user is not initially in sudo mode', async ({ page }) => { - await page.addInitScript(() => (globalThis.isAdmin = true)); + test('admin user is not initially in sudo mode', async ({ page, msw }) => { + await prepare(msw, { isAdmin: true }); await page.goto('/crates/foo/versions'); @@ -64,8 +63,8 @@ test.describe('Acceptance | sudo', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-version-yank-button="0.1.0"]')).toBeVisible(); }); - test('admin user can enter sudo mode', async ({ page }) => { - await page.addInitScript(() => (globalThis.isAdmin = true)); + test('admin user can enter sudo mode', async ({ page, msw }) => { + await prepare(msw, { isAdmin: true }); await page.exposeFunction('format', ((date, options) => format(date, options)) as typeof format); await page.goto('/crates/foo/versions'); @@ -99,8 +98,8 @@ test.describe('Acceptance | sudo', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-version-yank-button="0.1.0"]')).toBeVisible(); }); - test('admin can yank a crate in sudo mode', async ({ page }) => { - await page.addInitScript(() => (globalThis.isAdmin = true)); + test('admin can yank a crate in sudo mode', async ({ page, msw }) => { + let { version } = await prepare(msw, { isAdmin: true }); await page.goto('/crates/foo/versions'); @@ -113,21 +112,15 @@ test.describe('Acceptance | sudo', { tag: '@acceptance' }, () => { await yankButton.click(); // Verify backend state after yanking - const yankedVersion = await page.evaluate(() => { - const crate = server.schema['crates'].findBy({ name: 'foo' }); - return server.schema['versions'].findBy({ crateId: crate.id, num: '0.1.0', yanked: true }); - }); - expect(yankedVersion, 'The version should be yanked').toBeTruthy(); + version = msw.db.version.findFirst({ where: { id: { equals: version.id } } }); + expect(version.yanked, 'The version should be yanked').toBe(true); await expect(unyankButton).toBeVisible(); await unyankButton.click(); // Verify backend state after unyanking - const unyankedVersion = await page.evaluate(() => { - const crate = server.schema['crates'].findBy({ name: 'foo' }); - return server.schema['versions'].findBy({ crateId: crate.id, num: '0.1.0', yanked: false }); - }); - expect(unyankedVersion, 'The version should be unyanked').toBeTruthy(); + version = msw.db.version.findFirst({ where: { id: { equals: version.id } } }); + expect(version.yanked, 'The version should be unyanked').toBe(false); await expect(yankButton).toBeVisible(); }); From 63236b3a9a242346a1cd7c00a99b392e04ec8341 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 16:37:24 +0100 Subject: [PATCH 171/189] e2e/acceptance/support: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/support.spec.ts | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/e2e/acceptance/support.spec.ts b/e2e/acceptance/support.spec.ts index fcbcec746c3..95a52f9552c 100644 --- a/e2e/acceptance/support.spec.ts +++ b/e2e/acceptance/support.spec.ts @@ -31,13 +31,10 @@ test.describe('Acceptance | support page', { tag: '@acceptance' }, () => { }); test.describe('reporting a crate from support page', () => { - test.beforeEach(async ({ page, mirage }) => { - await mirage.config({ trackRequests: true }); - await mirage.addHook(server => { - globalThis._routes = server._config.routes; - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '0.6.0' }); - }); + test.beforeEach(async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate, num: '0.6.0' }); + // mock `window.open()` await page.addInitScript(() => { globalThis.open = (url, target, features) => { @@ -195,13 +192,10 @@ test detail }); test.describe('reporting a crate from crate page', () => { - test.beforeEach(async ({ page, mirage }) => { - await mirage.config({ trackRequests: true }); - await mirage.addHook(server => { - globalThis._routes = server._config.routes; - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '0.6.0' }); - }); + test.beforeEach(async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate, num: '0.6.0' }); + // mock `window.open()` await page.addInitScript(() => { globalThis.open = (url, target, features) => { From 525c2ceb08c4fa34f01ca7f107579edfd85aacf9 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 16:41:18 +0100 Subject: [PATCH 172/189] e2e/acceptance/team-page: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/team-page.spec.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/e2e/acceptance/team-page.spec.ts b/e2e/acceptance/team-page.spec.ts index b5fc3dfd374..24c4c47e6e7 100644 --- a/e2e/acceptance/team-page.spec.ts +++ b/e2e/acceptance/team-page.spec.ts @@ -1,11 +1,9 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; +import { loadFixtures } from '@crates-io/msw/fixtures'; test.describe('Acceptance | team page', { tag: '@acceptance' }, () => { - test.beforeEach(async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); - + test.beforeEach(async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/teams/github:org:thehydroimpulse'); }); From e65e19243d3ec1750ca17345f7b33ffcfcebc452 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 16:41:26 +0100 Subject: [PATCH 173/189] e2e/acceptance/token-invites: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/token-invites.spec.ts | 42 ++++++++-------------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/e2e/acceptance/token-invites.spec.ts b/e2e/acceptance/token-invites.spec.ts index feda617cdf6..a1cec381125 100644 --- a/e2e/acceptance/token-invites.spec.ts +++ b/e2e/acceptance/token-invites.spec.ts @@ -1,4 +1,5 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Acceptance | /accept-invite/:token', { tag: '@acceptance' }, () => { test('visiting to /accept-invite shows 404 page', async ({ page }) => { @@ -23,44 +24,23 @@ test.describe('Acceptance | /accept-invite/:token', { tag: '@acceptance' }, () = ); }); - test('shows error for expired token', async ({ page, mirage }) => { + test('shows error for expired token', async ({ page, msw }) => { let errorMessage = 'The invitation to become an owner of the demo_crate crate expired. Please reach out to an owner of the crate to request a new invitation.'; - await page.exposeBinding('_errorMessage', () => errorMessage); - await mirage.addHook(server => { - server.put( - '/api/v1/me/crate_owner_invitations/accept/:token', - async () => { - let errorMessage = await globalThis._errorMessage(); - let payload = { errors: [{ detail: errorMessage }] }; - return payload; - }, - 410, - ); - }); + let error = HttpResponse.json({ errors: [{ detail: errorMessage }] }, { status: 410 }); + await msw.worker.use(http.put('/api/v1/me/crate_owner_invitations/accept/:token', () => error)); await page.goto('/accept-invite/secret123'); await expect(page).toHaveURL('/accept-invite/secret123'); await expect(page.locator('[data-test-error-message]')).toHaveText(errorMessage); }); - test('shows success for known token', async ({ page, mirage, percy }) => { - await mirage.addHook(server => { - let inviter = server.create('user'); - let invitee = server.create('user'); - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate }); - let invite = server.create('crate-owner-invitation', { crate, invitee, inviter }); - - globalThis.invite = invite; - }); - - // NOTE: Because the current implementation only works with the miragejs server running in the - // browser, we need to navigate to a random page to trigger the server startup and generate a - // token. This step will not be necessary in production or once we migrate the miragejs server - // to run in nodejs. - await page.goto(`/accept-invite/123`); - const invite = await page.evaluate(() => ({ token: globalThis.invite.token })); + test('shows success for known token', async ({ page, msw, percy }) => { + let inviter = msw.db.user.create(); + let invitee = msw.db.user.create(); + let crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate }); + let invite = msw.db.crateOwnerInvitation.create({ crate, invitee, inviter }); await page.goto(`/accept-invite/${invite.token}`); await expect(page).toHaveURL(`/accept-invite/${invite.token}`); From 9702df605a51236a82309c826e58f5c622cbd735 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 16:42:13 +0100 Subject: [PATCH 174/189] e2e/acceptance/user-page: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/user-page.spec.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/e2e/acceptance/user-page.spec.ts b/e2e/acceptance/user-page.spec.ts index da4c7d31213..636dc455af6 100644 --- a/e2e/acceptance/user-page.spec.ts +++ b/e2e/acceptance/user-page.spec.ts @@ -1,10 +1,9 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; +import { loadFixtures } from '@crates-io/msw/fixtures'; test.describe('Acceptance | user page', { tag: '@acceptance' }, () => { - test.beforeEach(async ({ page, mirage }) => { - await mirage.addHook(server => { - server.loadFixtures(); - }); + test.beforeEach(async ({ page, msw }) => { + loadFixtures(msw.db); await page.goto('/users/thehydroimpulse'); }); From 58818a99fe9dd9bdc4e5d3663dcaa2d368e11ce8 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 27 Jan 2025 16:43:02 +0100 Subject: [PATCH 175/189] e2e/acceptance/versions: Migrate from `mirage` to `@crates-io/msw` --- e2e/acceptance/versions.spec.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/e2e/acceptance/versions.spec.ts b/e2e/acceptance/versions.spec.ts index f3001f47a17..db962b98ca2 100644 --- a/e2e/acceptance/versions.spec.ts +++ b/e2e/acceptance/versions.spec.ts @@ -1,14 +1,12 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; test.describe('Acceptance | crate versions page', { tag: '@acceptance' }, () => { - test('show versions sorted by date', async ({ page, mirage, percy }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'nanomsg' }); - server.create('version', { crate, num: '0.1.0', created_at: '2017-01-01' }); - server.create('version', { crate, num: '0.2.0', created_at: '2018-01-01' }); - server.create('version', { crate, num: '0.3.0', created_at: '2019-01-01', rust_version: '1.69' }); - server.create('version', { crate, num: '0.2.1', created_at: '2020-01-01' }); - }); + test('show versions sorted by date', async ({ page, msw, percy }) => { + let crate = msw.db.crate.create({ name: 'nanomsg' }); + msw.db.version.create({ crate, num: '0.1.0', created_at: '2017-01-01' }); + msw.db.version.create({ crate, num: '0.2.0', created_at: '2018-01-01' }); + msw.db.version.create({ crate, num: '0.3.0', created_at: '2019-01-01', rust_version: '1.69' }); + msw.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' }); await page.goto('/crates/nanomsg/versions'); await expect(page).toHaveURL('/crates/nanomsg/versions'); From 8ef87d30819a39fd8934ee3fde16ba9976181353 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 28 Jan 2025 13:00:09 +0100 Subject: [PATCH 176/189] e2e/bugs/2329: Migrate from `mirage` to `@crates-io/msw` --- e2e/bugs/2329.spec.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/e2e/bugs/2329.spec.ts b/e2e/bugs/2329.spec.ts index 97fafc55c75..d104e441414 100644 --- a/e2e/bugs/2329.spec.ts +++ b/e2e/bugs/2329.spec.ts @@ -1,25 +1,25 @@ import { test, expect } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Bug #2329', { tag: '@bugs' }, () => { - test.skip('is fixed', async ({ page, mirage }) => { - await mirage.addHook(server => { - let user = server.create('user'); + test.skip('is fixed', async ({ page, msw }) => { + let user = msw.db.user.create(); - let foobar = server.create('crate', { name: 'foo-bar' }); - server.create('crate-ownership', { crate: foobar, user, emailNotifications: true }); - server.create('version', { crate: foobar }); + let foobar = msw.db.crate.create({ name: 'foo-bar' }); + msw.db.crateOwnership.create({ crate: foobar, user, emailNotifications: true }); + msw.db.version.create({ crate: foobar }); - let bar = server.create('crate', { name: 'barrrrr' }); - server.create('crate-ownership', { crate: bar, user, emailNotifications: false }); - server.create('version', { crate: bar }); + let bar = msw.db.crate.create({ name: 'barrrrr' }); + msw.db.crateOwnership.create({ crate: bar, user, emailNotifications: false }); + msw.db.version.create({ crate: bar }); - server.get('/api/private/session/begin', { url: 'url-to-github-including-state-secret' }); - - server.get('/api/private/session/authorize', () => { - authenticateAs(user); - return { ok: true }; - }); - }); + msw.worker.use( + http.get('/api/private/session/begin', () => HttpResponse.json({ url: 'url-to-github-including-state-secret' })), + http.get('/api/private/session/authorize', () => { + msw.db.mswSession.create({ user }); + return HttpResponse.json({ ok: true }); + }), + ); await page.addInitScript(() => { let fakeWindow = { document: { write() {}, close() {} }, close() {} }; From 858182ab752f49f172908cff99fb099e4e9aabf9 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 28 Jan 2025 13:00:14 +0100 Subject: [PATCH 177/189] e2e/bugs/4506: Migrate from `mirage` to `@crates-io/msw` --- e2e/bugs/4506.spec.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/e2e/bugs/4506.spec.ts b/e2e/bugs/4506.spec.ts index 622f1d3bb97..a689793a327 100644 --- a/e2e/bugs/4506.spec.ts +++ b/e2e/bugs/4506.spec.ts @@ -1,16 +1,14 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; test.describe('Bug #4506', { tag: '@bugs' }, () => { - test.beforeEach(async ({ mirage }) => { - await mirage.addHook(server => { - server.create('keyword', { keyword: 'no-std' }); + test.beforeEach(async ({ msw }) => { + let noStd = msw.db.keyword.create({ keyword: 'no-std' }); - let foo = server.create('crate', { name: 'foo', keywordIds: ['no-std'] }); - server.create('version', { crate: foo }); + let foo = msw.db.crate.create({ name: 'foo', keywords: [noStd] }); + msw.db.version.create({ crate: foo }); - let bar = server.create('crate', { name: 'bar', keywordIds: ['no-std'] }); - server.create('version', { crate: bar }); - }); + let bar = msw.db.crate.create({ name: 'bar', keywords: [noStd] }); + msw.db.version.create({ crate: bar }); }); test('is fixed', async ({ page }) => { From 68be0b4a2d28263ad5d1c38f9587dfadd0aa95bf Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 28 Jan 2025 13:02:30 +0100 Subject: [PATCH 178/189] e2e/routes/category: Migrate from `mirage` to `@crates-io/msw` --- e2e/routes/category.spec.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/e2e/routes/category.spec.ts b/e2e/routes/category.spec.ts index 27b1167a15c..04a1636337b 100644 --- a/e2e/routes/category.spec.ts +++ b/e2e/routes/category.spec.ts @@ -1,4 +1,5 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Route | category', { tag: '@routes' }, () => { test("shows an error message if the category can't be found", async ({ page }) => { @@ -10,10 +11,8 @@ test.describe('Route | category', { tag: '@routes' }, () => { await expect(page.locator('[data-test-try-again]')).toHaveCount(0); }); - test('server error causes the error page to be shown', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.get('/api/v1/categories/:categoryId', {}, 500); - }); + test('server error causes the error page to be shown', async ({ page, msw }) => { + msw.worker.use(http.get('/api/v1/categories/:categoryId', () => HttpResponse.json({}, { status: 500 }))); await page.goto('/categories/foo'); await expect(page).toHaveURL('/categories/foo'); @@ -23,10 +22,8 @@ test.describe('Route | category', { tag: '@routes' }, () => { await expect(page.locator('[data-test-try-again]')).toBeVisible(); }); - test('updates the search field when the categories route is accessed', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.create('category', { category: 'foo' }); - }); + test('updates the search field when the categories route is accessed', async ({ page, msw }) => { + msw.db.category.create({ category: 'foo' }); const searchInput = page.locator('[data-test-search-input]'); await page.goto('/'); From 2423995a5bd208af1ce14e3f52936e2eb5d2a5b1 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 28 Jan 2025 13:08:38 +0100 Subject: [PATCH 179/189] e2e/routes/crate/delete: Migrate from `mirage` to `@crates-io/msw` --- e2e/routes/crate/delete.spec.ts | 70 +++++++++++++++------------------ 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/e2e/routes/crate/delete.spec.ts b/e2e/routes/crate/delete.spec.ts index 5bbb99e980d..c5ac3a0d7ec 100644 --- a/e2e/routes/crate/delete.spec.ts +++ b/e2e/routes/crate/delete.spec.ts @@ -1,23 +1,21 @@ +import { defer } from '@/e2e/deferred'; import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Route: crate.delete', { tag: '@routes' }, () => { - async function prepare({ mirage }) { - await mirage.addHook(server => { - let user = server.create('user'); + async function prepare(msw) { + let user = msw.db.user.create(); - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate }); - server.create('crate-ownership', { crate, user }); + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate }); + msw.db.crateOwnership.create({ crate, user }); - authenticateAs(user); - }); + await msw.authenticateAs(user); } - test('unauthenticated', async ({ mirage, page }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate }); - }); + test('unauthenticated', async ({ msw, page }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate }); await page.goto('/crates/foo/delete'); await expect(page).toHaveURL('/crates/foo/delete'); @@ -25,16 +23,14 @@ test.describe('Route: crate.delete', { tag: '@routes' }, () => { await expect(page.locator('[data-test-login]')).toBeVisible(); }); - test('not an owner', async ({ mirage, page }) => { - await mirage.addHook(server => { - let user1 = server.create('user'); - authenticateAs(user1); + test('not an owner', async ({ msw, page }) => { + let user1 = msw.db.user.create(); + await msw.authenticateAs(user1); - let user2 = server.create('user'); - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate }); - server.create('crate-ownership', { crate, user: user2 }); - }); + let user2 = msw.db.user.create(); + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate }); + msw.db.crateOwnership.create({ crate, user: user2 }); await page.goto('/crates/foo/delete'); await expect(page).toHaveURL('/crates/foo/delete'); @@ -42,8 +38,8 @@ test.describe('Route: crate.delete', { tag: '@routes' }, () => { await expect(page.locator('[data-test-go-back]')).toBeVisible(); }); - test('happy path', async ({ mirage, page, percy }) => { - await prepare({ mirage }); + test('happy path', async ({ msw, page, percy }) => { + await prepare(msw); await page.goto('/crates/foo/delete'); await expect(page).toHaveURL('/crates/foo/delete'); @@ -61,16 +57,15 @@ test.describe('Route: crate.delete', { tag: '@routes' }, () => { let message = 'Crate foo has been successfully deleted.'; await expect(page.locator('[data-test-notification-message="success"]')).toHaveText(message); - let crate = await page.evaluate(() => server.schema.crates.findBy({ name: 'foo' })); + let crate = msw.db.crate.findFirst({ where: { name: { equals: 'foo' } } }); expect(crate).toBeNull(); }); - test('loading state', async ({ page, mirage }) => { - await prepare({ mirage }); - await mirage.addHook(server => { - globalThis.deferred = require('rsvp').defer(); - server.delete('/api/v1/crates/foo', () => globalThis.deferred.promise); - }); + test('loading state', async ({ page, msw }) => { + await prepare(msw); + + let deferred = defer(); + msw.worker.use(http.delete('/api/v1/crates/:name', () => deferred.promise)); await page.goto('/crates/foo/delete'); await page.fill('[data-test-reason]', "I don't need this crate anymore"); @@ -80,16 +75,15 @@ test.describe('Route: crate.delete', { tag: '@routes' }, () => { await expect(page.locator('[data-test-confirmation-checkbox]')).toBeDisabled(); await expect(page.locator('[data-test-delete-button]')).toBeDisabled(); - await page.evaluate(async () => globalThis.deferred.resolve()); + deferred.resolve(); await expect(page).toHaveURL('/'); }); - test('error state', async ({ page, mirage }) => { - await prepare({ mirage }); - await mirage.addHook(server => { - let payload = { errors: [{ detail: 'only crates without reverse dependencies can be deleted after 72 hours' }] }; - server.delete('/api/v1/crates/foo', payload, 422); - }); + test('error state', async ({ page, msw }) => { + await prepare(msw); + + let payload = { errors: [{ detail: 'only crates without reverse dependencies can be deleted after 72 hours' }] }; + msw.worker.use(http.delete('/api/v1/crates/:name', () => HttpResponse.json(payload, { status: 422 }))); await page.goto('/crates/foo/delete'); await page.fill('[data-test-reason]', "I don't need this crate anymore"); From c459b0e90a957b79a6a24670385d28155d1c09ea Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 28 Jan 2025 13:14:15 +0100 Subject: [PATCH 180/189] e2e/routes/crate/range: Migrate from `mirage` to `@crates-io/msw` --- e2e/routes/crate/range.spec.ts | 106 ++++++++++++++------------------- 1 file changed, 46 insertions(+), 60 deletions(-) diff --git a/e2e/routes/crate/range.spec.ts b/e2e/routes/crate/range.spec.ts index 82e1f518c5b..01a391d7f22 100644 --- a/e2e/routes/crate/range.spec.ts +++ b/e2e/routes/crate/range.spec.ts @@ -1,14 +1,13 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Route | crate.range', { tag: '@routes' }, () => { - test('happy path', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.0.0' }); - server.create('version', { crate, num: '1.1.0' }); - server.create('version', { crate, num: '1.2.0' }); - server.create('version', { crate, num: '1.2.3' }); - }); + test('happy path', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.0.0' }); + msw.db.version.create({ crate, num: '1.1.0' }); + msw.db.version.create({ crate, num: '1.2.0' }); + msw.db.version.create({ crate, num: '1.2.3' }); await page.goto('/crates/foo/range/^1.1.0'); await expect(page).toHaveURL(`/crates/foo/1.2.3`); @@ -17,14 +16,12 @@ test.describe('Route | crate.range', { tag: '@routes' }, () => { await expect(page.locator('[data-test-notification-message]')).toHaveCount(0); }); - test('happy path with tilde range', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.0.0' }); - server.create('version', { crate, num: '1.1.0' }); - server.create('version', { crate, num: '1.1.1' }); - server.create('version', { crate, num: '1.2.0' }); - }); + test('happy path with tilde range', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.0.0' }); + msw.db.version.create({ crate, num: '1.1.0' }); + msw.db.version.create({ crate, num: '1.1.1' }); + msw.db.version.create({ crate, num: '1.2.0' }); await page.goto('/crates/foo/range/~1.1.0'); await expect(page).toHaveURL(`/crates/foo/1.1.1`); @@ -33,14 +30,12 @@ test.describe('Route | crate.range', { tag: '@routes' }, () => { await expect(page.locator('[data-test-notification-message]')).toHaveCount(0); }); - test('happy path with cargo style and', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.4.2' }); - server.create('version', { crate, num: '1.3.4' }); - server.create('version', { crate, num: '1.3.3' }); - server.create('version', { crate, num: '1.2.6' }); - }); + test('happy path with cargo style and', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.4.2' }); + msw.db.version.create({ crate, num: '1.3.4' }); + msw.db.version.create({ crate, num: '1.3.3' }); + msw.db.version.create({ crate, num: '1.2.6' }); await page.goto('/crates/foo/range/>=1.3.0, <1.4.0'); await expect(page).toHaveURL(`/crates/foo/1.3.4`); @@ -49,14 +44,12 @@ test.describe('Route | crate.range', { tag: '@routes' }, () => { await expect(page.locator('[data-test-notification-message]')).toHaveCount(0); }); - test('ignores yanked versions if possible', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.0.0' }); - server.create('version', { crate, num: '1.1.0' }); - server.create('version', { crate, num: '1.1.1' }); - server.create('version', { crate, num: '1.2.0', yanked: true }); - }); + test('ignores yanked versions if possible', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.0.0' }); + msw.db.version.create({ crate, num: '1.1.0' }); + msw.db.version.create({ crate, num: '1.1.1' }); + msw.db.version.create({ crate, num: '1.2.0', yanked: true }); await page.goto('/crates/foo/range/^1.0.0'); await expect(page).toHaveURL(`/crates/foo/1.1.1`); @@ -65,14 +58,12 @@ test.describe('Route | crate.range', { tag: '@routes' }, () => { await expect(page.locator('[data-test-notification-message]')).toHaveCount(0); }); - test('falls back to yanked version if necessary', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.0.0', yanked: true }); - server.create('version', { crate, num: '1.1.0', yanked: true }); - server.create('version', { crate, num: '1.1.1', yanked: true }); - server.create('version', { crate, num: '2.0.0' }); - }); + test('falls back to yanked version if necessary', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.0.0', yanked: true }); + msw.db.version.create({ crate, num: '1.1.0', yanked: true }); + msw.db.version.create({ crate, num: '1.1.1', yanked: true }); + msw.db.version.create({ crate, num: '2.0.0' }); await page.goto('/crates/foo/range/^1.0.0'); await expect(page).toHaveURL(`/crates/foo/1.1.1`); @@ -81,7 +72,7 @@ test.describe('Route | crate.range', { tag: '@routes' }, () => { await expect(page.locator('[data-test-notification-message]')).toHaveCount(0); }); - test('shows an error page if crate not found', async ({ page, mirage }) => { + test('shows an error page if crate not found', async ({ page }) => { await page.goto('/crates/foo/range/^3'); await expect(page).toHaveURL('/crates/foo/range/%5E3'); await expect(page.locator('[data-test-404-page]')).toBeVisible(); @@ -90,10 +81,8 @@ test.describe('Route | crate.range', { tag: '@routes' }, () => { await expect(page.locator('[data-test-try-again]')).toHaveCount(0); }); - test('shows an error page if crate fails to load', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.get('/api/v1/crates/:crate_name', {}, 500); - }); + test('shows an error page if crate fails to load', async ({ page, msw }) => { + msw.worker.use(http.get('/api/v1/crates/:crate_name', () => HttpResponse.json({}, { status: 500 }))); await page.goto('/crates/foo/range/^3'); await expect(page).toHaveURL('/crates/foo/range/%5E3'); @@ -103,14 +92,13 @@ test.describe('Route | crate.range', { tag: '@routes' }, () => { await expect(page.locator('[data-test-try-again]')).toBeVisible(); }); - test('shows an error page if no match found', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.0.0' }); - server.create('version', { crate, num: '1.1.0' }); - server.create('version', { crate, num: '1.1.1' }); - server.create('version', { crate, num: '2.0.0' }); - }); + test('shows an error page if no match found', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.0.0' }); + msw.db.version.create({ crate, num: '1.1.0' }); + msw.db.version.create({ crate, num: '1.1.1' }); + msw.db.version.create({ crate, num: '2.0.0' }); + await page.goto('/crates/foo/range/^3'); await expect(page).toHaveURL('/crates/foo/range/%5E3'); await expect(page.locator('[data-test-404-page]')).toBeVisible(); @@ -119,13 +107,11 @@ test.describe('Route | crate.range', { tag: '@routes' }, () => { await expect(page.locator('[data-test-try-again]')).toHaveCount(0); }); - test('shows an error page if versions fail to load', async ({ page, mirage, ember }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '3.2.1' }); + test('shows an error page if versions fail to load', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '3.2.1' }); - server.get('/api/v1/crates/:crate_name/versions', {}, 500); - }); + msw.worker.use(http.get('/api/v1/crates/:crate_name/versions', () => HttpResponse.json({}, { status: 500 }))); await page.goto('/crates/foo/range/^3'); await expect(page).toHaveURL('/crates/foo/range/%5E3'); From 9bc8b572f1eaf3484afc739c2b00be5675d84cd4 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 28 Jan 2025 13:18:12 +0100 Subject: [PATCH 181/189] e2e/routes/crate/version/crate-links: Migrate from `mirage` to `@crates-io/msw` --- e2e/routes/crate/version/crate-links.spec.ts | 54 +++++++++----------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/e2e/routes/crate/version/crate-links.spec.ts b/e2e/routes/crate/version/crate-links.spec.ts index dbd659ebdb8..15d74974b80 100644 --- a/e2e/routes/crate/version/crate-links.spec.ts +++ b/e2e/routes/crate/version/crate-links.spec.ts @@ -1,16 +1,14 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; test.describe('Route | crate.version | crate links', { tag: '@routes' }, () => { - test('shows all external crate links', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { - name: 'foo', - homepage: 'https://crates.io/', - documentation: 'https://doc.rust-lang.org/cargo/getting-started/', - repository: 'https://github.com/rust-lang/crates.io.git', - }); - server.create('version', { crate, num: '1.0.0' }); + test('shows all external crate links', async ({ page, msw }) => { + let crate = msw.db.crate.create({ + name: 'foo', + homepage: 'https://crates.io/', + documentation: 'https://doc.rust-lang.org/cargo/getting-started/', + repository: 'https://github.com/rust-lang/crates.io.git', }); + msw.db.version.create({ crate, num: '1.0.0' }); await page.goto('/crates/foo'); @@ -28,11 +26,9 @@ test.describe('Route | crate.version | crate links', { tag: '@routes' }, () => { await expect(repositoryLink).toHaveAttribute('href', 'https://github.com/rust-lang/crates.io.git'); }); - test('shows no external crate links if none are set', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.0.0' }); - }); + test('shows no external crate links if none are set', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.0.0' }); await page.goto('/crates/foo'); @@ -41,15 +37,13 @@ test.describe('Route | crate.version | crate links', { tag: '@routes' }, () => { await expect(page.locator('[data-test-repository-link]')).toHaveCount(0); }); - test('hide the homepage link if it is the same as the repository', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { - name: 'foo', - homepage: 'https://github.com/rust-lang/crates.io', - repository: 'https://github.com/rust-lang/crates.io', - }); - server.create('version', { crate, num: '1.0.0' }); + test('hide the homepage link if it is the same as the repository', async ({ page, msw }) => { + let crate = msw.db.crate.create({ + name: 'foo', + homepage: 'https://github.com/rust-lang/crates.io', + repository: 'https://github.com/rust-lang/crates.io', }); + msw.db.version.create({ crate, num: '1.0.0' }); await page.goto('/crates/foo'); @@ -61,15 +55,13 @@ test.describe('Route | crate.version | crate links', { tag: '@routes' }, () => { await expect(repositoryLink).toHaveAttribute('href', 'https://github.com/rust-lang/crates.io'); }); - test('hide the homepage link if it is the same as the repository plus `.git`', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { - name: 'foo', - homepage: 'https://github.com/rust-lang/crates.io/', - repository: 'https://github.com/rust-lang/crates.io.git', - }); - server.create('version', { crate, num: '1.0.0' }); + test('hide the homepage link if it is the same as the repository plus `.git`', async ({ page, msw }) => { + let crate = msw.db.crate.create({ + name: 'foo', + homepage: 'https://github.com/rust-lang/crates.io/', + repository: 'https://github.com/rust-lang/crates.io.git', }); + msw.db.version.create({ crate, num: '1.0.0' }); await page.goto('/crates/foo'); From 06542ccecc5f9b022bc52c2bf93af3800cbbe9e8 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 28 Jan 2025 13:18:21 +0100 Subject: [PATCH 182/189] e2e/routes/crate/version/docs-link: Migrate from `mirage` to `@crates-io/msw` --- e2e/routes/crate/version/docs-link.spec.ts | 89 ++++++++++------------ 1 file changed, 41 insertions(+), 48 deletions(-) diff --git a/e2e/routes/crate/version/docs-link.spec.ts b/e2e/routes/crate/version/docs-link.spec.ts index 3d2a74dccd2..e95f5af5422 100644 --- a/e2e/routes/crate/version/docs-link.spec.ts +++ b/e2e/routes/crate/version/docs-link.spec.ts @@ -1,11 +1,10 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Route | crate.version | docs link', { tag: '@routes' }, () => { - test('shows regular documentation link', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo', documentation: 'https://foo.io/docs' }); - server.create('version', { crate, num: '1.0.0' }); - }); + test('shows regular documentation link', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo', documentation: 'https://foo.io/docs' }); + msw.db.version.create({ crate, num: '1.0.0' }); await page.goto('/crates/foo'); await expect(page.locator('[data-test-docs-link] a')).toHaveAttribute('href', 'https://foo.io/docs'); @@ -13,14 +12,13 @@ test.describe('Route | crate.version | docs link', { tag: '@routes' }, () => { test('show no docs link if `documentation` is unspecified and there are no related docs.rs builds', async ({ page, - mirage, + msw, }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.0.0' }); + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.0.0' }); - server.get('https://docs.rs/crate/:crate/:version/status.json', 'not found', 404); - }); + let error = HttpResponse.text('not found', { status: 404 }); + msw.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => error)); await page.goto('/crates/foo'); await expect(page.getByRole('link', { name: 'crates.io', exact: true })).toHaveCount(1); @@ -30,17 +28,16 @@ test.describe('Route | crate.version | docs link', { tag: '@routes' }, () => { test('show docs link if `documentation` is unspecified and there are related docs.rs builds', async ({ page, - mirage, + msw, }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.0.0' }); - - server.get('https://docs.rs/crate/:crate/:version/status.json', { - doc_status: true, - version: '1.0.0', - }); + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.0.0' }); + + let response = HttpResponse.json({ + doc_status: true, + version: '1.0.0', }); + msw.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => response)); await page.goto('/crates/foo'); await expect(page.locator('[data-test-docs-link] a')).toHaveAttribute('href', 'https://docs.rs/foo/1.0.0'); @@ -48,14 +45,13 @@ test.describe('Route | crate.version | docs link', { tag: '@routes' }, () => { test('show original docs link if `documentation` points to docs.rs and there are no related docs.rs builds', async ({ page, - mirage, + msw, }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); - server.create('version', { crate, num: '1.0.0' }); + let crate = msw.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); + msw.db.version.create({ crate, num: '1.0.0' }); - server.get('https://docs.rs/crate/:crate/:version/status.json', 'not found', 404); - }); + let error = HttpResponse.text('not found', { status: 404 }); + msw.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => error)); await page.goto('/crates/foo'); await expect(page.locator('[data-test-docs-link] a')).toHaveAttribute('href', 'https://docs.rs/foo/0.6.2'); @@ -63,41 +59,38 @@ test.describe('Route | crate.version | docs link', { tag: '@routes' }, () => { test('show updated docs link if `documentation` points to docs.rs and there are related docs.rs builds', async ({ page, - mirage, + msw, }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); - server.create('version', { crate, num: '1.0.0' }); - - server.get('https://docs.rs/crate/:crate/:version/status.json', { - doc_status: true, - version: '1.0.0', - }); + let crate = msw.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); + msw.db.version.create({ crate, num: '1.0.0' }); + + let response = HttpResponse.json({ + doc_status: true, + version: '1.0.0', }); + msw.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => response)); await page.goto('/crates/foo'); await expect(page.locator('[data-test-docs-link] a')).toHaveAttribute('href', 'https://docs.rs/foo/1.0.0'); }); - test('ajax errors are ignored', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); - server.create('version', { crate, num: '1.0.0' }); + test('ajax errors are ignored', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); + msw.db.version.create({ crate, num: '1.0.0' }); - server.get('https://docs.rs/crate/:crate/:version/status.json', 'error', 500); - }); + let error = HttpResponse.text('error', { status: 500 }); + msw.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => error)); await page.goto('/crates/foo'); await expect(page.locator('[data-test-docs-link] a')).toHaveAttribute('href', 'https://docs.rs/foo/0.6.2'); }); - test('empty docs.rs responses are ignored', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); - server.create('version', { crate, num: '0.6.2' }); + test('empty docs.rs responses are ignored', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' }); + msw.db.version.create({ crate, num: '0.6.2' }); - server.get('https://docs.rs/crate/:crate/:version/status.json', {}); - }); + let response = HttpResponse.json({}); + msw.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => response)); await page.goto('/crates/foo'); await expect(page.locator('[data-test-docs-link] a')).toHaveAttribute('href', 'https://docs.rs/foo/0.6.2'); From ac446e5c0981d422fb3274617395aaed59498c2a Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 28 Jan 2025 13:18:56 +0100 Subject: [PATCH 183/189] e2e/routes/crate/version/model: Migrate from `mirage` to `@crates-io/msw` --- e2e/routes/crate/version/model.spec.ts | 80 +++++++++++--------------- 1 file changed, 34 insertions(+), 46 deletions(-) diff --git a/e2e/routes/crate/version/model.spec.ts b/e2e/routes/crate/version/model.spec.ts index 25a50093fe0..f7a921f9af4 100644 --- a/e2e/routes/crate/version/model.spec.ts +++ b/e2e/routes/crate/version/model.spec.ts @@ -1,14 +1,12 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; test.describe('Route | crate.version | model() hook', { tag: '@routes' }, () => { test.describe('with explicit version number in the URL', () => { - test('shows yanked versions', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.0.0' }); - server.create('version', { crate, num: '1.2.3', yanked: true }); - server.create('version', { crate, num: '2.0.0-beta.1' }); - }); + test('shows yanked versions', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.0.0' }); + msw.db.version.create({ crate, num: '1.2.3', yanked: true }); + msw.db.version.create({ crate, num: '2.0.0-beta.1' }); await page.goto('/crates/foo/1.2.3'); await expect(page).toHaveURL(`/crates/foo/1.2.3`); @@ -20,13 +18,11 @@ test.describe('Route | crate.version | model() hook', { tag: '@routes' }, () => await expect(page.locator('[data-test-notification-message]')).toHaveCount(0); }); - test('shows error page for unknown versions', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.0.0' }); - server.create('version', { crate, num: '1.2.3', yanked: true }); - server.create('version', { crate, num: '2.0.0-beta.1' }); - }); + test('shows error page for unknown versions', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.0.0' }); + msw.db.version.create({ crate, num: '1.2.3', yanked: true }); + msw.db.version.create({ crate, num: '2.0.0-beta.1' }); await page.goto('/crates/foo/2.0.0'); await expect(page).toHaveURL(`/crates/foo/2.0.0`); @@ -37,14 +33,12 @@ test.describe('Route | crate.version | model() hook', { tag: '@routes' }, () => }); }); test.describe('without version number in the URL', () => { - test('defaults to the highest stable version', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.0.0' }); - server.create('version', { crate, num: '1.2.3', yanked: true }); - server.create('version', { crate, num: '2.0.0-beta.1' }); - server.create('version', { crate, num: '2.0.0' }); - }); + test('defaults to the highest stable version', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.0.0' }); + msw.db.version.create({ crate, num: '1.2.3', yanked: true }); + msw.db.version.create({ crate, num: '2.0.0-beta.1' }); + msw.db.version.create({ crate, num: '2.0.0' }); await page.goto('/crates/foo'); await expect(page).toHaveURL(`/crates/foo`); @@ -56,13 +50,11 @@ test.describe('Route | crate.version | model() hook', { tag: '@routes' }, () => await expect(page.locator('[data-test-notification-message]')).toHaveCount(0); }); - test('defaults to the highest stable version, even if there are higher prereleases', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.0.0' }); - server.create('version', { crate, num: '1.2.3', yanked: true }); - server.create('version', { crate, num: '2.0.0-beta.1' }); - }); + test('defaults to the highest stable version, even if there are higher prereleases', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.0.0' }); + msw.db.version.create({ crate, num: '1.2.3', yanked: true }); + msw.db.version.create({ crate, num: '2.0.0-beta.1' }); await page.goto('/crates/foo'); await expect(page).toHaveURL(`/crates/foo`); @@ -74,15 +66,13 @@ test.describe('Route | crate.version | model() hook', { tag: '@routes' }, () => await expect(page.locator('[data-test-notification-message]')).toHaveCount(0); }); - test('defaults to the highest not-yanked version', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.0.0', yanked: true }); - server.create('version', { crate, num: '1.2.3', yanked: true }); - server.create('version', { crate, num: '2.0.0-beta.1' }); - server.create('version', { crate, num: '2.0.0-beta.2' }); - server.create('version', { crate, num: '2.0.0', yanked: true }); - }); + test('defaults to the highest not-yanked version', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.0.0', yanked: true }); + msw.db.version.create({ crate, num: '1.2.3', yanked: true }); + msw.db.version.create({ crate, num: '2.0.0-beta.1' }); + msw.db.version.create({ crate, num: '2.0.0-beta.2' }); + msw.db.version.create({ crate, num: '2.0.0', yanked: true }); await page.goto('/crates/foo'); await expect(page).toHaveURL(`/crates/foo`); @@ -94,13 +84,11 @@ test.describe('Route | crate.version | model() hook', { tag: '@routes' }, () => await expect(page.locator('[data-test-notification-message]')).toHaveCount(0); }); - test('if there are only yanked versions, it defaults to the latest version', async ({ page, mirage }) => { - await mirage.addHook(server => { - let crate = server.create('crate', { name: 'foo' }); - server.create('version', { crate, num: '1.0.0', yanked: true }); - server.create('version', { crate, num: '1.2.3', yanked: true }); - server.create('version', { crate, num: '2.0.0-beta.1', yanked: true }); - }); + test('if there are only yanked versions, it defaults to the latest version', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '1.0.0', yanked: true }); + msw.db.version.create({ crate, num: '1.2.3', yanked: true }); + msw.db.version.create({ crate, num: '2.0.0-beta.1', yanked: true }); await page.goto('/crates/foo'); await expect(page).toHaveURL(`/crates/foo`); From 4eaf3db12fa510c87572330c91bd7c73d2c11aa8 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 28 Jan 2025 13:20:10 +0100 Subject: [PATCH 184/189] e2e/routes/keyword: Migrate from `mirage` to `@crates-io/msw` --- e2e/routes/keyword.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/e2e/routes/keyword.spec.ts b/e2e/routes/keyword.spec.ts index 25b0dacfd59..2946f7acb27 100644 --- a/e2e/routes/keyword.spec.ts +++ b/e2e/routes/keyword.spec.ts @@ -1,4 +1,5 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Route | keyword', { tag: '@routes' }, () => { test('shows an empty list if the keyword does not exist on the server', async ({ page }) => { @@ -7,10 +8,9 @@ test.describe('Route | keyword', { tag: '@routes' }, () => { await expect(page.locator('[data-test-crate-row]')).toHaveCount(0); }); - test('server error causes the error page to be shown', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.get('/api/v1/crates', {}, 500); - }); + test('server error causes the error page to be shown', async ({ page, msw }) => { + let error = HttpResponse.json({}, { status: 500 }); + msw.worker.use(http.get('/api/v1/crates', () => error)); await page.goto('/keywords/foo'); await expect(page).toHaveURL('/keywords/foo'); From b71030376e9b415aadbe50558cbf6e06bce8d7db Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 28 Jan 2025 13:33:05 +0100 Subject: [PATCH 185/189] e2e/routes/settings/tokens/index: Migrate from `mirage` to `@crates-io/msw` --- e2e/routes/settings/tokens/index.spec.ts | 29 ++++++++---------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/e2e/routes/settings/tokens/index.spec.ts b/e2e/routes/settings/tokens/index.spec.ts index 267373897d9..63661616294 100644 --- a/e2e/routes/settings/tokens/index.spec.ts +++ b/e2e/routes/settings/tokens/index.spec.ts @@ -1,26 +1,17 @@ -import { test, expect } from '@/e2e/helper'; +import { expect, test } from '@/e2e/helper'; test.describe('/settings/tokens', { tag: '@routes' }, () => { - test.beforeEach(async ({ mirage }) => { - await mirage.addHook(server => { - let user = server.create('user', { - login: 'johnnydee', - name: 'John Doe', - email: 'john@doe.com', - avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', - }); - - authenticateAs(user); - - globalThis.user = user; + test('reloads all tokens from the server', async ({ page, msw }) => { + let user = msw.db.user.create({ + login: 'johnnydee', + name: 'John Doe', + email: 'john@doe.com', + avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', }); - }); - test('reloads all tokens from the server', async ({ page, mirage }) => { - await mirage.addHook(server => { - const user = globalThis.user; - server.create('api-token', { user, name: 'token-1' }); - }); + await msw.authenticateAs(user); + + msw.db.apiToken.create({ user, name: 'token-1' }); await page.goto('/settings/tokens/new'); await expect(page).toHaveURL('/settings/tokens/new'); From 1ea2da8ebdec508b109818a46e1524bccc776605 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 28 Jan 2025 13:33:10 +0100 Subject: [PATCH 186/189] e2e/routes/settings/tokens/new: Migrate from `mirage` to `@crates-io/msw` --- e2e/routes/settings/tokens/new.spec.ts | 132 ++++++++++++------------- 1 file changed, 64 insertions(+), 68 deletions(-) diff --git a/e2e/routes/settings/tokens/new.spec.ts b/e2e/routes/settings/tokens/new.spec.ts index 56253e80c70..87b1a8bfb8e 100644 --- a/e2e/routes/settings/tokens/new.spec.ts +++ b/e2e/routes/settings/tokens/new.spec.ts @@ -1,22 +1,24 @@ +import { defer } from '@/e2e/deferred'; import { expect, test } from '@/e2e/helper'; -import { Response } from 'miragejs'; +import { http, HttpResponse } from 'msw'; test.describe('/settings/tokens/new', { tag: '@routes' }, () => { - test.beforeEach(async ({ mirage }) => { - await mirage.addHook(server => { - let user = server.create('user', { - login: 'johnnydee', - name: 'John Doe', - email: 'john@doe.com', - avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', - }); - - authenticateAs(user); - globalThis.user = user; + async function prepare(msw) { + let user = msw.db.user.create({ + login: 'johnnydee', + name: 'John Doe', + email: 'john@doe.com', + avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4', }); - }); - test('can navigate to the route', async ({ page }) => { + await msw.authenticateAs(user); + + return { user }; + } + + test('can navigate to the route', async ({ page, msw }) => { + await prepare(msw); + await page.goto('/'); await expect(page).toHaveURL('/'); @@ -31,7 +33,9 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { await expect(page).toHaveURL('/settings/tokens/new'); }); - test('happy path', async ({ page }) => { + test('happy path', async ({ page, msw }) => { + await prepare(msw); + await page.goto('/settings/tokens/new'); await expect(page).toHaveURL('/settings/tokens/new'); @@ -40,10 +44,7 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { await page.click('[data-test-scope="publish-update"]'); await page.click('[data-test-generate]'); - let token = await page.evaluate(() => { - let token = server.schema['apiTokens'].findBy({ name: 'token-name' }); - return JSON.parse(JSON.stringify(token)); - }); + let token = msw.db.apiToken.findFirst({ where: { name: { equals: 'token-name' } } }); expect(token, 'API token has been created in the backend database').toBeTruthy(); expect(token.name).toBe('token-name'); expect(token.expiredAt).toBe(null); @@ -60,7 +61,9 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { await expect(page.locator('[data-test-api-token="1"] [data-test-expired-at]')).toHaveCount(0); }); - test('crate scopes', async ({ page }) => { + test('crate scopes', async ({ page, msw }) => { + await prepare(msw); + await page.goto('/settings/tokens/new'); await expect(page).toHaveURL('/settings/tokens/new'); @@ -130,10 +133,7 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { await page.click('[data-test-generate]'); - let token = await page.evaluate(() => { - let token = server.schema['apiTokens'].findBy({ name: 'token-name' }); - return JSON.parse(JSON.stringify(token)); - }); + let token = msw.db.apiToken.findFirst({ where: { name: { equals: 'token-name' } } }); expect(token, 'API token has been created in the backend database').toBeTruthy(); expect(token.name).toBe('token-name'); expect(token.crateScopes).toEqual(['serde-*', 'serde']); @@ -151,14 +151,14 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { await expect(page.locator('[data-test-api-token="1"] [data-test-expired-at]')).toHaveCount(0); }); - test('token expiry', async ({ page }) => { + test('token expiry', async ({ page, msw }) => { + await prepare(msw); + await page.goto('/settings/tokens/new'); await expect(page).toHaveURL('/settings/tokens/new'); await expect(page.locator('[data-test-name]')).toHaveValue(''); await expect(page.locator('[data-test-expiry]')).toHaveValue('90'); - let expiryDate = new Date('2018-02-18T00:00:00'); - let expectedDate = expiryDate.toLocaleDateString(undefined, { dateStyle: 'long' }); - let expectedDescription = `The token will expire on ${expectedDate}`; + let expectedDescription = `The token will expire on February 18, 2018`; await expect(page.locator('[data-test-expiry-description]')).toHaveText(expectedDescription); await page.fill('[data-test-name]', 'token-name'); @@ -166,18 +166,13 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { await expect(page.locator('[data-test-expiry-description]')).toHaveText('The token will never expire'); await page.locator('[data-test-expiry]').selectOption('30'); - expiryDate = new Date('2017-12-20T00:00:00'); - expectedDate = expiryDate.toLocaleDateString(undefined, { dateStyle: 'long' }); - expectedDescription = `The token will expire on ${expectedDate}`; + expectedDescription = `The token will expire on December 20, 2017`; await expect(page.locator('[data-test-expiry-description]')).toHaveText(expectedDescription); await page.click('[data-test-scope="publish-update"]'); await page.click('[data-test-generate]'); - let token = await page.evaluate(() => { - let token = server.schema['apiTokens'].findBy({ name: 'token-name' }); - return JSON.parse(JSON.stringify(token)); - }); + let token = msw.db.apiToken.findFirst({ where: { name: { equals: 'token-name' } } }); expect(token, 'API token has been created in the backend database').toBeTruthy(); expect(token.name).toBe('token-name'); expect(token.expiredAt.slice(0, 10)).toBe('2017-12-20'); @@ -196,7 +191,9 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { ); }); - test('token expiry with custom date', async ({ page }) => { + test('token expiry with custom date', async ({ page, msw }) => { + await prepare(msw); + await page.goto('/settings/tokens/new'); await expect(page).toHaveURL('/settings/tokens/new'); @@ -215,10 +212,7 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { await page.click('[data-test-generate]'); - let token = await page.evaluate(() => { - let token = server.schema['apiTokens'].findBy({ name: 'token-name' }); - return JSON.parse(JSON.stringify(token)); - }); + let token = msw.db.apiToken.findFirst({ where: { name: { equals: 'token-name' } } }); expect(token, 'API token has been created in the backend database').toBeTruthy(); expect(token.name).toBe('token-name'); expect(token.expiredAt.slice(0, 10)).toBe('2024-05-04'); @@ -237,12 +231,11 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { ); }); - test('loading and error state', async ({ page, mirage }) => { - await page.exposeBinding('resp500', () => new Response(500)); - await mirage.addHook(server => { - globalThis.deferred = require('rsvp').defer(); - server.put('/api/v1/me/tokens', () => globalThis.deferred.promise); - }); + test('loading and error state', async ({ page, msw }) => { + await prepare(msw); + + let deferred = defer(); + msw.worker.use(http.put('/api/v1/me/tokens', () => deferred.promise)); await page.goto('/settings/tokens/new'); await expect(page).toHaveURL('/settings/tokens/new'); @@ -254,7 +247,7 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { await expect(page.locator('[data-test-name]')).toBeDisabled(); await expect(page.locator('[data-test-generate]')).toBeDisabled(); - await page.evaluate(async () => globalThis.deferred.resolve(await globalThis.resp500)); + deferred.resolve(HttpResponse.json({}, { status: 500 })); let message = 'An error has occurred while generating your API token. Please try again later!'; await expect(page.locator('[data-test-notification-message="error"]')).toHaveText(message); @@ -262,7 +255,9 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { await expect(page.locator('[data-test-generate]')).toBeEnabled(); }); - test('cancel button navigates back to the token list', async ({ page }) => { + test('cancel button navigates back to the token list', async ({ page, msw }) => { + await prepare(msw); + await page.goto('/settings/tokens/new'); await expect(page).toHaveURL('/settings/tokens/new'); @@ -270,7 +265,9 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { await expect(page).toHaveURL('/settings/tokens'); }); - test('empty name shows an error', async ({ page }) => { + test('empty name shows an error', async ({ page, msw }) => { + await prepare(msw); + await page.goto('/settings/tokens/new'); await expect(page).toHaveURL('/settings/tokens/new'); @@ -282,7 +279,9 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { await expect(page.locator('[data-test-scopes-group] [data-test-error]')).toHaveCount(0); }); - test('no scopes selected shows an error', async ({ page }) => { + test('no scopes selected shows an error', async ({ page, msw }) => { + await prepare(msw); + await page.goto('/settings/tokens/new'); await expect(page).toHaveURL('/settings/tokens/new'); @@ -293,19 +292,17 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { await expect(page.locator('[data-test-scopes-group] [data-test-error]')).toBeVisible(); }); - test('prefill with the exist token', async ({ page, mirage }) => { - await mirage.addHook(server => { - const user = globalThis.user; - - server.create('apiToken', { - user: user, - id: '1', - name: 'foo', - token: 'test', - createdAt: '2017-08-01T12:34:56', - lastUsedAt: '2017-11-02T01:45:14', - endpointScopes: ['publish-update'], - }); + test('prefill with the exist token', async ({ page, msw }) => { + let { user } = await prepare(msw); + + msw.db.apiToken.create({ + user: user, + id: 1, + name: 'foo', + token: 'test', + createdAt: '2017-08-01T12:34:56', + lastUsedAt: '2017-11-02T01:45:14', + endpointScopes: ['publish-update'], }); await page.goto('/settings/tokens/new?from=1'); @@ -323,10 +320,7 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { ); await page.click('[data-test-generate]'); - let newToken = await page.evaluate(() => { - let newToken = server.schema['apiTokens'].findBy({ name: 'foo', crateScopes: ['serde'] }); - return JSON.parse(JSON.stringify(newToken)); - }); + let newToken = msw.db.apiToken.findFirst({ where: { name: { equals: 'foo' } } }); expect(newToken, 'New API token has been created in the backend database').toBeTruthy(); await expect(page).toHaveURL('/settings/tokens'); @@ -335,7 +329,9 @@ test.describe('/settings/tokens/new', { tag: '@routes' }, () => { await expect(page).toHaveURL('/settings/tokens/new'); }); - test('token not found', async ({ page }) => { + test('token not found', async ({ page, msw }) => { + await prepare(msw); + await page.goto('/settings/tokens/new?from=1'); await expect(page).toHaveURL('/settings/tokens/new?from=1'); await expect(page.locator('[data-test-title]')).toHaveText('Token not found'); From a309609b447a3e244e3ab09fb0a59edcd821ac7e Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 28 Jan 2025 13:37:18 +0100 Subject: [PATCH 187/189] e2e/routes/team: Migrate from `mirage` to `@crates-io/msw` --- e2e/routes/team.spec.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/e2e/routes/team.spec.ts b/e2e/routes/team.spec.ts index 5065a82807d..cbff5c0a290 100644 --- a/e2e/routes/team.spec.ts +++ b/e2e/routes/team.spec.ts @@ -1,4 +1,5 @@ import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Route | team', { tag: '@routes' }, () => { test("shows an error message if the category can't be found", async ({ page }) => { @@ -10,10 +11,8 @@ test.describe('Route | team', { tag: '@routes' }, () => { await expect(page.locator('[data-test-try-again]')).toHaveCount(0); }); - test('server error causes the error page to be shown', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.get('/api/v1/teams/:id', {}, 500); - }); + test('server error causes the error page to be shown', async ({ page, msw }) => { + msw.worker.use(http.get('/api/v1/teams/:id', () => HttpResponse.json({}, { status: 500 }))); await page.goto('/teams/foo'); await expect(page).toHaveURL('/teams/foo'); From c34c9df6717ed5e6b84b200723781695645d3156 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 28 Jan 2025 13:37:23 +0100 Subject: [PATCH 188/189] e2e/routes/user: Migrate from `mirage` to `@crates-io/msw` --- e2e/routes/user.spec.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/e2e/routes/user.spec.ts b/e2e/routes/user.spec.ts index c71207a8770..206e7cdf148 100644 --- a/e2e/routes/user.spec.ts +++ b/e2e/routes/user.spec.ts @@ -1,4 +1,5 @@ import { expect, test } from '@/e2e/helper'; +import { http, HttpResponse } from 'msw'; test.describe('Route | user', { tag: '@routes' }, () => { test("shows an error message if the category can't be found", async ({ page }) => { @@ -10,10 +11,8 @@ test.describe('Route | user', { tag: '@routes' }, () => { await expect(page.locator('[data-test-try-again]')).toHaveCount(0); }); - test('server error causes the error page to be shown', async ({ page, mirage }) => { - await mirage.addHook(server => { - server.get('/api/v1/users/:id', {}, 500); - }); + test('server error causes the error page to be shown', async ({ page, msw }) => { + msw.worker.use(http.get('/api/v1/users/:id', () => HttpResponse.json({}, { status: 500 }))); await page.goto('/users/foo'); await expect(page).toHaveURL('/users/foo'); From a328a12207d66224fb2b642bc8bb7a63047ba1c9 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 28 Jan 2025 14:32:27 +0100 Subject: [PATCH 189/189] Remove obsolete `ember-cli-mirage` setup --- .eslintrc.js | 9 - .github/workflows/ci.yml | 2 +- app/initializers/ember-cli-mirage.js | 36 -- config/environment.js | 2 - docs/ARCHITECTURE.md | 2 +- docs/CONTRIBUTING.md | 18 +- e2e/fixtures/mirage.ts | 175 ------- e2e/helper.ts | 10 - mirage/config.js | 59 --- mirage/factories/api-token.js | 21 - mirage/factories/category.js | 21 - mirage/factories/crate-owner-invitation.js | 19 - mirage/factories/crate-ownership.js | 14 - mirage/factories/crate.js | 21 - mirage/factories/dependency.js | 21 - mirage/factories/keyword.js | 9 - mirage/factories/mirage-session.js | 9 - mirage/factories/team.js | 18 - mirage/factories/user.js | 32 -- mirage/factories/version-download.js | 12 - mirage/factories/version.js | 28 -- mirage/fixtures/categories.js | 25 - mirage/fixtures/crate-ownerships.js | 18 - mirage/fixtures/crates.js | 319 ------------ mirage/fixtures/dependencies.js | 57 --- mirage/fixtures/keywords.js | 72 --- mirage/fixtures/teams.js | 16 - mirage/fixtures/users.js | 26 - mirage/fixtures/version-downloads.js | 17 - mirage/fixtures/versions.js | 412 --------------- mirage/models/api-token.js | 5 - mirage/models/category-slug.js | 3 - mirage/models/category.js | 3 - mirage/models/crate-owner-invitation.js | 7 - mirage/models/crate-ownership.js | 7 - mirage/models/crate.js | 7 - mirage/models/dependency.js | 6 - mirage/models/keyword.js | 3 - mirage/models/mirage-session.js | 13 - mirage/models/owned-crate.js | 3 - mirage/models/team.js | 3 - mirage/models/user.js | 5 - mirage/models/version-download.js | 5 - mirage/models/version.js | 6 - mirage/route-handlers/-utils.js | 49 -- mirage/route-handlers/categories.js | 30 -- mirage/route-handlers/crates.js | 468 ------------------ mirage/route-handlers/docs-rs.js | 5 - mirage/route-handlers/invites.js | 49 -- mirage/route-handlers/keywords.js | 19 - mirage/route-handlers/me.js | 183 ------- mirage/route-handlers/metadata.js | 9 - mirage/route-handlers/session.js | 12 - mirage/route-handlers/summary.js | 41 -- mirage/route-handlers/teams.js | 9 - mirage/route-handlers/users.js | 65 --- mirage/scenarios/default.js | 9 - mirage/serializers/api-token.js | 32 -- mirage/serializers/application.js | 26 - mirage/serializers/category.js | 24 - mirage/serializers/crate-owner-invitation.js | 35 -- mirage/serializers/crate.js | 110 ---- mirage/serializers/dependency.js | 22 - mirage/serializers/keyword.js | 24 - mirage/serializers/team.js | 22 - mirage/serializers/user.js | 35 -- mirage/serializers/version-download.js | 23 - mirage/serializers/version.js | 65 --- mirage/utils/session.js | 9 - package.json | 6 +- pnpm-lock.yaml | 101 +--- tests/helpers/index.js | 4 - tests/helpers/setup-mirage.js | 19 - tests/mirage/categories/get-by-id-test.js | 57 --- tests/mirage/categories/list-test.js | 92 ---- tests/mirage/category-slugs/list-test.js | 57 --- tests/mirage/confirm/put-by-id-test.js | 45 -- tests/mirage/crates/add-owner-test.js | 119 ----- tests/mirage/crates/delete-test.js | 42 -- tests/mirage/crates/downloads-test.js | 63 --- tests/mirage/crates/follow/delete-test.js | 44 -- tests/mirage/crates/follow/get-test.js | 50 -- tests/mirage/crates/follow/put-test.js | 44 -- tests/mirage/crates/get-by-id-test.js | 330 ------------ tests/mirage/crates/list-test.js | 228 --------- tests/mirage/crates/owner-team-test.js | 48 -- tests/mirage/crates/owner-user-test.js | 48 -- .../crates/reverse-dependencies-test.js | 170 ------- tests/mirage/crates/versions/authors-test.js | 53 -- .../crates/versions/dependencies-test.js | 88 ---- .../mirage/crates/versions/downloads-test.js | 66 --- .../mirage/crates/versions/get-by-num-test.js | 57 --- tests/mirage/crates/versions/list-test.js | 155 ------ tests/mirage/crates/versions/patch-test.js | 120 ----- tests/mirage/crates/versions/readme-test.js | 45 -- .../crates/versions/yank/unyank-test.js | 57 --- .../mirage/crates/versions/yank/yank-test.js | 55 -- tests/mirage/keywords/get-by-id-test.js | 48 -- tests/mirage/keywords/list-test.js | 80 --- .../me/crate-owner-invitations/list-test.js | 101 ---- tests/mirage/me/get-test.js | 63 --- tests/mirage/me/tokens/create-test.js | 115 ----- tests/mirage/me/tokens/delete-by-id-test.js | 36 -- tests/mirage/me/tokens/list-test.js | 77 --- tests/mirage/me/updates/list-test.js | 91 ---- .../crate-owner-invitations/get-test.js | 229 --------- tests/mirage/private/session/delete-test.js | 30 -- tests/mirage/summary-test.js | 175 ------- tests/mirage/teams/get-by-id-test.js | 33 -- tests/mirage/users/get-by-id-test.js | 33 -- tests/mirage/users/resend-by-id-test.js | 37 -- tests/mirage/users/update-by-id-test.js | 91 ---- 112 files changed, 14 insertions(+), 6439 deletions(-) delete mode 100644 app/initializers/ember-cli-mirage.js delete mode 100644 e2e/fixtures/mirage.ts delete mode 100644 mirage/config.js delete mode 100644 mirage/factories/api-token.js delete mode 100644 mirage/factories/category.js delete mode 100644 mirage/factories/crate-owner-invitation.js delete mode 100644 mirage/factories/crate-ownership.js delete mode 100644 mirage/factories/crate.js delete mode 100644 mirage/factories/dependency.js delete mode 100644 mirage/factories/keyword.js delete mode 100644 mirage/factories/mirage-session.js delete mode 100644 mirage/factories/team.js delete mode 100644 mirage/factories/user.js delete mode 100644 mirage/factories/version-download.js delete mode 100644 mirage/factories/version.js delete mode 100644 mirage/fixtures/categories.js delete mode 100644 mirage/fixtures/crate-ownerships.js delete mode 100644 mirage/fixtures/crates.js delete mode 100644 mirage/fixtures/dependencies.js delete mode 100644 mirage/fixtures/keywords.js delete mode 100644 mirage/fixtures/teams.js delete mode 100644 mirage/fixtures/users.js delete mode 100644 mirage/fixtures/version-downloads.js delete mode 100644 mirage/fixtures/versions.js delete mode 100644 mirage/models/api-token.js delete mode 100644 mirage/models/category-slug.js delete mode 100644 mirage/models/category.js delete mode 100644 mirage/models/crate-owner-invitation.js delete mode 100644 mirage/models/crate-ownership.js delete mode 100644 mirage/models/crate.js delete mode 100644 mirage/models/dependency.js delete mode 100644 mirage/models/keyword.js delete mode 100644 mirage/models/mirage-session.js delete mode 100644 mirage/models/owned-crate.js delete mode 100644 mirage/models/team.js delete mode 100644 mirage/models/user.js delete mode 100644 mirage/models/version-download.js delete mode 100644 mirage/models/version.js delete mode 100644 mirage/route-handlers/-utils.js delete mode 100644 mirage/route-handlers/categories.js delete mode 100644 mirage/route-handlers/crates.js delete mode 100644 mirage/route-handlers/docs-rs.js delete mode 100644 mirage/route-handlers/invites.js delete mode 100644 mirage/route-handlers/keywords.js delete mode 100644 mirage/route-handlers/me.js delete mode 100644 mirage/route-handlers/metadata.js delete mode 100644 mirage/route-handlers/session.js delete mode 100644 mirage/route-handlers/summary.js delete mode 100644 mirage/route-handlers/teams.js delete mode 100644 mirage/route-handlers/users.js delete mode 100644 mirage/scenarios/default.js delete mode 100644 mirage/serializers/api-token.js delete mode 100644 mirage/serializers/application.js delete mode 100644 mirage/serializers/category.js delete mode 100644 mirage/serializers/crate-owner-invitation.js delete mode 100644 mirage/serializers/crate.js delete mode 100644 mirage/serializers/dependency.js delete mode 100644 mirage/serializers/keyword.js delete mode 100644 mirage/serializers/team.js delete mode 100644 mirage/serializers/user.js delete mode 100644 mirage/serializers/version-download.js delete mode 100644 mirage/serializers/version.js delete mode 100644 mirage/utils/session.js delete mode 100644 tests/helpers/setup-mirage.js delete mode 100644 tests/mirage/categories/get-by-id-test.js delete mode 100644 tests/mirage/categories/list-test.js delete mode 100644 tests/mirage/category-slugs/list-test.js delete mode 100644 tests/mirage/confirm/put-by-id-test.js delete mode 100644 tests/mirage/crates/add-owner-test.js delete mode 100644 tests/mirage/crates/delete-test.js delete mode 100644 tests/mirage/crates/downloads-test.js delete mode 100644 tests/mirage/crates/follow/delete-test.js delete mode 100644 tests/mirage/crates/follow/get-test.js delete mode 100644 tests/mirage/crates/follow/put-test.js delete mode 100644 tests/mirage/crates/get-by-id-test.js delete mode 100644 tests/mirage/crates/list-test.js delete mode 100644 tests/mirage/crates/owner-team-test.js delete mode 100644 tests/mirage/crates/owner-user-test.js delete mode 100644 tests/mirage/crates/reverse-dependencies-test.js delete mode 100644 tests/mirage/crates/versions/authors-test.js delete mode 100644 tests/mirage/crates/versions/dependencies-test.js delete mode 100644 tests/mirage/crates/versions/downloads-test.js delete mode 100644 tests/mirage/crates/versions/get-by-num-test.js delete mode 100644 tests/mirage/crates/versions/list-test.js delete mode 100644 tests/mirage/crates/versions/patch-test.js delete mode 100644 tests/mirage/crates/versions/readme-test.js delete mode 100644 tests/mirage/crates/versions/yank/unyank-test.js delete mode 100644 tests/mirage/crates/versions/yank/yank-test.js delete mode 100644 tests/mirage/keywords/get-by-id-test.js delete mode 100644 tests/mirage/keywords/list-test.js delete mode 100644 tests/mirage/me/crate-owner-invitations/list-test.js delete mode 100644 tests/mirage/me/get-test.js delete mode 100644 tests/mirage/me/tokens/create-test.js delete mode 100644 tests/mirage/me/tokens/delete-by-id-test.js delete mode 100644 tests/mirage/me/tokens/list-test.js delete mode 100644 tests/mirage/me/updates/list-test.js delete mode 100644 tests/mirage/private/crate-owner-invitations/get-test.js delete mode 100644 tests/mirage/private/session/delete-test.js delete mode 100644 tests/mirage/summary-test.js delete mode 100644 tests/mirage/teams/get-by-id-test.js delete mode 100644 tests/mirage/users/get-by-id-test.js delete mode 100644 tests/mirage/users/resend-by-id-test.js delete mode 100644 tests/mirage/users/update-by-id-test.js diff --git a/.eslintrc.js b/.eslintrc.js index a0b787261f5..21c91813c8b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -107,15 +107,6 @@ module.exports = { }, }, - // mirage files - { - files: ['mirage/**/*.js'], - rules: { - // disabled because of different `.find()` meaning - 'unicorn/no-array-callback-reference': 'off', - }, - }, - // node files { files: [ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd519e8aa05..0d01ffe560f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: files_ignore: | app/** e2e/** - mirage/** + packages/** public/** tests/** .eslintrc diff --git a/app/initializers/ember-cli-mirage.js b/app/initializers/ember-cli-mirage.js deleted file mode 100644 index 1c88e4e1597..00000000000 --- a/app/initializers/ember-cli-mirage.js +++ /dev/null @@ -1,36 +0,0 @@ -import { importSync, isDevelopingApp, isTesting, macroCondition } from '@embroider/macros'; - -export default { - name: 'ember-cli-mirage', - initialize(application) { - // `macroCondition(isDevelopingApp() || isTesting())` should work as well, - // but it failed to build correctly on CI, so we duplicate it into two - // conditions. Since we will be dropping `ember-cli-mirage` soon anyway, - // this is good enough for now. - if (macroCondition(isDevelopingApp())) { - let startMirage = importSync('ember-cli-mirage/start-mirage').default; - let ENV = importSync('../config/environment').default; - let makeServer = importSync('../mirage/config').default; - - application.register('mirage:make-server', makeServer, { - instantiate: false, - }); - - if (window.startMirage) { - startMirage(application.__container__, { makeServer, env: ENV }); - } - } else if (macroCondition(isTesting())) { - let startMirage = importSync('ember-cli-mirage/start-mirage').default; - let ENV = importSync('../config/environment').default; - let makeServer = importSync('../mirage/config').default; - - application.register('mirage:make-server', makeServer, { - instantiate: false, - }); - - if (window.startMirage) { - startMirage(application.__container__, { makeServer, env: ENV }); - } - } - }, -}; diff --git a/config/environment.js b/config/environment.js index c5c1ec61d10..0b1653e42e8 100644 --- a/config/environment.js +++ b/config/environment.js @@ -63,8 +63,6 @@ module.exports = function (environment) { if (environment === 'production') { // here you can enable a production-specific feature - delete ENV['ember-cli-mirage']; - ENV.sentry = { dsn: process.env.SENTRY_DSN_WEB, }; diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6c2430fd891..fbe938fea44 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -51,8 +51,8 @@ These files have to do with the frontend: - `.ember-cli` - Settings for the `ember` command line interface - `ember-cli-build.js` - Contains the build specification for Broccoli - `.eslintrc.js` - Defines Javascript coding style guidelines (enforced during CI???) -- `mirage/` - A mock backend used during development and testing - `node_modules/` - npm dependencies - (ignored in `.gitignore`) +- `packages/crates-io-msw` - A mock backend used for testing - `package.json` - Defines the npm package and its dependencies - `package-lock.json` - Locks dependencies to specific versions providing consistency across development and deployment diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 156bd45902b..bc23159c367 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -134,18 +134,12 @@ To build and serve the frontend assets, use the command `pnpm start`. There are variations on this command that change which backend your frontend tries to talk to: -| Command | Backend | Use case | -| ----------------------------------------- | --------------------------------------------- | ------------------------------------------------------- | -| `pnpm start:live` | | Testing UI changes with the full live site's data | -| `pnpm start:staging` | | Testing UI changes with a smaller set of realistic data | -| `pnpm start` | Static fixture test data in `mirage/fixtures` | Setting up particular situations, see note | -| `pnpm start:local` | Backend server running locally | See the Working on the backend section for setup | -| `pnpm start -- --proxy https://crates.io` | Whatever is specified in `--proxy` arg | If your use case is not covered here | - -> Note: If you want to set up a particular situation, you can edit the fixture -> data used for tests in `mirage/fixtures`. The fixture data does not currently -> contain JSON needed to support every page, so some pages might not load -> correctly. +| Command | Backend | Use case | +| ----------------------------------------- | ----------------------------------------- | ------------------------------------------------------- | +| `pnpm start:live` | | Testing UI changes with the full live site's data | +| `pnpm start:staging` | | Testing UI changes with a smaller set of realistic data | +| `pnpm start:local` | Backend server running locally | See the Working on the backend section for setup | +| `pnpm start -- --proxy https://crates.io` | Whatever is specified in `--proxy` arg | If your use case is not covered here | #### Running the frontend tests diff --git a/e2e/fixtures/mirage.ts b/e2e/fixtures/mirage.ts deleted file mode 100644 index 2b422bea7e9..00000000000 --- a/e2e/fixtures/mirage.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { Page } from '@playwright/test'; -import { Registry, Server as BaseServer, Request } from 'miragejs'; -import { HandlerOptions, RouteHandler, ServerConfig } from 'miragejs/server'; -import { CONFIG_KEY, HOOK_KEY } from '@/mirage/config'; - -const HOOK_MAPPING = { - config: CONFIG_KEY, - hook: HOOK_KEY, -} as const; -const DEFAULT_MIRAGE_CONFIG = { environment: 'test' } as const; - -export class MiragePage { - constructor(public readonly page: Page) { - this.page = page; - } - - async config(options: ServerConfig = DEFAULT_MIRAGE_CONFIG) { - await this._config({ options, force: true }); - } - - private async _config({ options, force }: { options?: ServerConfig; force: boolean }) { - await this.page.addInitScript( - ({ key, options, force }) => { - if (force || !window[Symbol.for(`${key}`)]) { - window[Symbol.for(`${key}`)] = options; - } - }, - { key: HOOK_MAPPING.config, options: { ...DEFAULT_MIRAGE_CONFIG, ...options }, force }, - ); - } - - async addHook(hook: HookFn | HookScript) { - let fn = String((k: string, h: HookFn) => { - let key = Symbol.for(`${k}`); - window[key] = (window[key] || []).concat(h); - }); - await this.page.addInitScript(`(${fn})('${HOOK_MAPPING.hook}', ${hook.toString()});`); - } - - private async addHelpers() { - await this.page.addInitScript(() => { - globalThis.authenticateAs = function (user) { - globalThis.server.create('mirage-session', { user }); - globalThis.localStorage.setItem('isLoggedIn', '1'); - }; - }); - // Use default options only if no other options are explicitly provided - await this._config({ force: false }); - } - - async setup() { - await this.page.addInitScript('window.startMirage = true'); - await this.addHelpers(); - } -} - -interface Server extends BaseServer> { - get(path: string, response: Response, status?: number): void; - get( - path: string, - handler?: RouteHandler, Response>, - options?: HandlerOptions, - ): void; - put(path: string, response: Response, status?: number): void; - put( - path: string, - handler?: RouteHandler, Response>, - options?: HandlerOptions, - ): void; - patch(path: string, response: Response, status?: number): void; - patch( - path: string, - handler?: RouteHandler, Response>, - options?: HandlerOptions, - ): void; - delete(path: string, response: Response, status?: number): void; - delete( - path: string, - handler?: RouteHandler, Response>, - options?: HandlerOptions, - ): void; - del(path: string, response: Response, status?: number): void; - del( - path: string, - handler?: RouteHandler, Response>, - options?: HandlerOptions, - ): void; - _config: ServerConfig; - pretender: PretenderSever; -} - -interface PretenderSever extends BasePretenderServer { - handledRequests: Request[]; -} - -type HookFn = (server: Server) => void; -type HookScript = Exclude[0], Function>; -type BasePretenderServer = BaseServer['pretender']; - -declare global { - var server: Server; - // TODO: Improve typing - function authenticateAs(user): void; -} - -import { default as ApiTokenModel } from '@/mirage/models/api-token'; -import { default as CategorySlugModel } from '@/mirage/models/category-slug'; -import { default as CategoryModel } from '@/mirage/models/category'; -import { default as CrateOwnerInvitationModel } from '@/mirage/models/crate-owner-invitation'; -import { default as CrateOwnershipModel } from '@/mirage/models/crate-ownership'; -import { default as CrateModel } from '@/mirage/models/crate'; -import { default as DependencyModel } from '@/mirage/models/dependency'; -import { default as KeywordModel } from '@/mirage/models/keyword'; -import { default as MirageSessionModel } from '@/mirage/models/mirage-session'; -import { default as OwnedCrateModel } from '@/mirage/models/owned-crate'; -import { default as TeamModel } from '@/mirage/models/team'; -import { default as UserModel } from '@/mirage/models/user'; -import { default as VersionDownloadModel } from '@/mirage/models/version-download'; -import { default as VersionModel } from '@/mirage/models/version'; - -import { default as ApiTokenFactory } from '@/mirage/factories/api-token'; -import { default as CategoryFactory } from '@/mirage/factories/category'; -import { default as CrateOwnerInvitationFactory } from '@/mirage/factories/crate-owner-invitation'; -import { default as CrateOwnershipFactory } from '@/mirage/factories/crate-ownership'; -import { default as CrateFactory } from '@/mirage/factories/crate'; -import { default as DependencyFactory } from '@/mirage/factories/dependency'; -import { default as KeywordFactory } from '@/mirage/factories/keyword'; -import { default as MirageSessionFactory } from '@/mirage/factories/mirage-session'; -import { default as TeamFactory } from '@/mirage/factories/team'; -import { default as UserFactory } from '@/mirage/factories/user'; -import { default as VersionDownloadFactory } from '@/mirage/factories/version-download'; -import { default as VersionFactory } from '@/mirage/factories/version'; -import { AnyResponse } from 'miragejs/-types'; - -const ModelsCamel = { - apiToken: ApiTokenModel, - categorySlug: CategorySlugModel, - category: CategoryModel, - crateOwnerInvitation: CrateOwnerInvitationModel, - crateOwnership: CrateOwnershipModel, - crate: CrateModel, - dependency: DependencyModel, - keyword: KeywordModel, - mirageSession: MirageSessionModel, - ownedCrate: OwnedCrateModel, - team: TeamModel, - user: UserModel, - versionDownload: VersionDownloadModel, - version: VersionModel, -}; - -type Models = typeof ModelsCamel & KebabKeys; - -const FactoriesCamel = { - apiToken: ApiTokenFactory, - category: CategoryFactory, - crateOwnerInvitation: CrateOwnerInvitationFactory, - crateOwnership: CrateOwnershipFactory, - crate: CrateFactory, - dependency: DependencyFactory, - keyword: KeywordFactory, - mirageSession: MirageSessionFactory, - team: TeamFactory, - user: UserFactory, - versionDownload: VersionDownloadFactory, - version: VersionFactory, -}; - -type Factories = typeof FactoriesCamel; - -// Taken from https://stackoverflow.com/a/66140779 -type Kebab = T extends `${infer F}${infer R}` - ? Kebab ? '' : '-'}${Lowercase}`> - : A; -type KebabKeys = { [K in keyof T as K extends string ? Kebab : K]: T[K] }; diff --git a/e2e/helper.ts b/e2e/helper.ts index ae9f54e70e4..b768b7f375f 100644 --- a/e2e/helper.ts +++ b/e2e/helper.ts @@ -5,7 +5,6 @@ import { db, handlers } from '@crates-io/msw'; import * as pwFakeTimers from '@sinonjs/fake-timers'; import { FakeTimers, FakeTimersOptions } from './fixtures/fake-timers'; -import { MiragePage } from './fixtures/mirage'; import { PercyPage } from './fixtures/percy'; import { A11yPage } from './fixtures/a11y'; import { EmberPage, EmberPageOptions } from './fixtures/ember'; @@ -17,7 +16,6 @@ export type AppOptions = { }; export interface AppFixtures { clock: FakeTimers; - mirage: MiragePage; msw: { worker: MockServiceWorker; db: typeof db; @@ -53,14 +51,6 @@ export const test = base.extend({ }, { auto: true, scope: 'test' }, ], - mirage: [ - async ({ page }, use) => { - let mirage = new MiragePage(page); - await mirage.setup(); - await use(mirage); - }, - { scope: 'test' }, - ], // MockServiceWorker integration via `playwright-msw`. // // We are explicitly not using the `createWorkerFixture()`function, because diff --git a/mirage/config.js b/mirage/config.js deleted file mode 100644 index 416724854b9..00000000000 --- a/mirage/config.js +++ /dev/null @@ -1,59 +0,0 @@ -import { createServer } from 'miragejs'; - -import * as Categories from './route-handlers/categories'; -import * as Crates from './route-handlers/crates'; -import * as DocsRS from './route-handlers/docs-rs'; -import * as Invites from './route-handlers/invites'; -import * as Keywords from './route-handlers/keywords'; -import * as Me from './route-handlers/me'; -import * as Metadata from './route-handlers/metadata'; -import * as Session from './route-handlers/session'; -import * as Summary from './route-handlers/summary'; -import * as Teams from './route-handlers/teams'; -import * as Users from './route-handlers/users'; - -export default function makeServer(config) { - let server = createServer({ - ...config, - routes() { - Categories.register(this); - Crates.register(this); - DocsRS.register(this); - Invites.register(this); - Keywords.register(this); - Metadata.register(this); - Me.register(this); - Session.register(this); - Summary.register(this); - Teams.register(this); - Users.register(this); - - // Used by ember-cli-code-coverage - this.passthrough('/write-coverage'); - }, - ...getHookConfig(), - }); - server = processHooks(server); - return server; -} - -export const CONFIG_KEY = 'hook:mirage:config'; -export const HOOK_KEY = 'hook:mirage:hook'; - -// Get injected config for testing with Playwright -function getHookConfig() { - return window[Symbol.for(CONFIG_KEY)]; -} - -// Process injected hooks for testing with Playwright -function processHooks(server) { - let hooks = window[Symbol.for(HOOK_KEY)]; - if (hooks && Array.isArray(hooks)) { - hooks.forEach(hook => { - if (hook && typeof hook === 'function') { - hook(server); - } - }); - } - return server; -} diff --git a/mirage/factories/api-token.js b/mirage/factories/api-token.js deleted file mode 100644 index 9f54e871e40..00000000000 --- a/mirage/factories/api-token.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Factory } from 'miragejs'; - -export default Factory.extend({ - crateScopes: null, - createdAt: '2017-11-19T17:59:22', - endpointScopes: null, - expiredAt: null, - lastUsedAt: null, - name: i => `API Token ${i + 1}`, - token: () => generateToken(), - - afterCreate(model) { - if (!model.user) { - throw new Error('Missing `user` relationship on `api-token`'); - } - }, -}); - -function generateToken() { - return Math.random().toString().slice(2); -} diff --git a/mirage/factories/category.js b/mirage/factories/category.js deleted file mode 100644 index c7bcad7dd1a..00000000000 --- a/mirage/factories/category.js +++ /dev/null @@ -1,21 +0,0 @@ -import { dasherize } from '@ember/string'; - -import { Factory } from 'miragejs'; - -export default Factory.extend({ - category: i => `Category ${i}`, - - slug() { - return dasherize(this.category); - }, - - id() { - return this.slug; - }, - - description() { - return `This is the description for the category called "${this.category}"`; - }, - - created_at: '2010-06-16T21:30:45Z', -}); diff --git a/mirage/factories/crate-owner-invitation.js b/mirage/factories/crate-owner-invitation.js deleted file mode 100644 index 75b7bdd40cc..00000000000 --- a/mirage/factories/crate-owner-invitation.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Factory } from 'miragejs'; - -export default Factory.extend({ - createdAt: '2016-12-24T12:34:56Z', - expiresAt: '2017-01-24T12:34:56Z', - token: i => `secret-token-${i}`, - - afterCreate(invite) { - if (!invite.crateId) { - throw new Error(`Missing \`crate\` relationship on \`crate-owner-invitation:${invite.id}\``); - } - if (!invite.inviteeId) { - throw new Error(`Missing \`invitee\` relationship on \`crate-owner-invitation:${invite.id}\``); - } - if (!invite.inviterId) { - throw new Error(`Missing \`inviter\` relationship on \`crate-owner-invitation:${invite.id}\``); - } - }, -}); diff --git a/mirage/factories/crate-ownership.js b/mirage/factories/crate-ownership.js deleted file mode 100644 index 187f252088e..00000000000 --- a/mirage/factories/crate-ownership.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Factory } from 'miragejs'; - -export default Factory.extend({ - emailNotifications: true, - - afterCreate(model) { - if (!model.crate) { - throw new Error('Missing `crate` relationship on `crate-ownership`'); - } - if (model.team && model.user) { - throw new Error('`team` and `user` on a `crate-ownership` are mutually exclusive'); - } - }, -}); diff --git a/mirage/factories/crate.js b/mirage/factories/crate.js deleted file mode 100644 index 82ea50d6256..00000000000 --- a/mirage/factories/crate.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Factory } from 'miragejs'; - -export default Factory.extend({ - name: i => `crate-${i}`, - - description() { - return `This is the description for the crate called "${this.name}"`; - }, - - downloads: i => (((i + 13) * 42) % 13) * 12_345, - - documentation: null, - homepage: null, - repository: null, - - created_at: '2010-06-16T21:30:45Z', - updated_at: '2017-02-24T12:34:56Z', - - badges: () => [], - _extra_downloads: () => [], -}); diff --git a/mirage/factories/dependency.js b/mirage/factories/dependency.js deleted file mode 100644 index 3649212e961..00000000000 --- a/mirage/factories/dependency.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Factory } from 'miragejs'; - -const REQS = ['^0.1.0', '^2.1.3', '0.3.7', '~5.2.12']; - -export default Factory.extend({ - default_features: i => i % 4 === 3, - features: () => [], - kind: i => (i % 3 === 0 ? 'dev' : 'normal'), - optional: i => i % 4 !== 3, - req: i => REQS[i % REQS.length], - target: null, - - afterCreate(self) { - if (!self.crateId) { - throw new Error(`Missing \`crate\` relationship on \`dependency:${self.id}\``); - } - if (!self.versionId) { - throw new Error(`Missing \`version\` relationship on \`dependency:${self.id}\``); - } - }, -}); diff --git a/mirage/factories/keyword.js b/mirage/factories/keyword.js deleted file mode 100644 index 534f3dddebb..00000000000 --- a/mirage/factories/keyword.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Factory } from 'miragejs'; - -export default Factory.extend({ - keyword: i => `keyword-${i + 1}`, - - id() { - return this.keyword; - }, -}); diff --git a/mirage/factories/mirage-session.js b/mirage/factories/mirage-session.js deleted file mode 100644 index 09e568aaac9..00000000000 --- a/mirage/factories/mirage-session.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Factory } from 'miragejs'; - -export default Factory.extend({ - afterCreate(session) { - if (!session.user) { - throw new Error('Missing `user` relationship'); - } - }, -}); diff --git a/mirage/factories/team.js b/mirage/factories/team.js deleted file mode 100644 index 65bc719470c..00000000000 --- a/mirage/factories/team.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Factory } from 'miragejs'; - -const ORGS = ['rust-lang', 'emberjs', 'rust-random', 'georust', 'actix']; - -export default Factory.extend({ - name: i => `team-${i + 1}`, - org: i => ORGS[i % ORGS.length], - - login() { - return `github:${this.org}:${this.name}`; - }, - - url() { - return `https://github.com/${this.org}`; - }, - - avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', -}); diff --git a/mirage/factories/user.js b/mirage/factories/user.js deleted file mode 100644 index 80c6c573a7b..00000000000 --- a/mirage/factories/user.js +++ /dev/null @@ -1,32 +0,0 @@ -import { dasherize } from '@ember/string'; - -import { Factory } from 'miragejs'; - -export default Factory.extend({ - name: i => `User ${i + 1}`, - - login() { - return dasherize(this.name); - }, - - email() { - return `${this.login}@crates.io`; - }, - - url() { - return `https://github.com/${this.login}`; - }, - - avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', - - emailVerified: null, - emailVerificationToken: null, - isAdmin: false, - publishNotifications: true, - - afterCreate(model) { - if (model.emailVerified === null) { - model.update({ emailVerified: model.email && !model.emailVerificationToken }); - } - }, -}); diff --git a/mirage/factories/version-download.js b/mirage/factories/version-download.js deleted file mode 100644 index 0208fc2ee08..00000000000 --- a/mirage/factories/version-download.js +++ /dev/null @@ -1,12 +0,0 @@ -import { Factory } from 'miragejs'; - -export default Factory.extend({ - date: '2019-05-21', - downloads: i => (((i * 42) % 13) + 4) * 2345, - - afterCreate(self) { - if (!self.versionId) { - throw new Error(`Missing \`version\` relationship on \`version-download:${self.date}\``); - } - }, -}); diff --git a/mirage/factories/version.js b/mirage/factories/version.js deleted file mode 100644 index b8364ec45ac..00000000000 --- a/mirage/factories/version.js +++ /dev/null @@ -1,28 +0,0 @@ -import { Factory } from 'miragejs'; - -const LICENSES = ['MIT/Apache-2.0', 'MIT', 'Apache-2.0']; - -export default Factory.extend({ - num: i => `1.0.${i}`, - - created_at: '2010-06-16T21:30:45Z', - updated_at: '2017-02-24T12:34:56Z', - - yanked: false, - yank_message: null, - license: i => LICENSES[i % LICENSES.length], - - downloads: i => (((i + 13) * 42) % 13) * 1234, - - features: () => {}, - - crate_size: i => (((i + 13) * 42) % 13) * 54_321, - readme: null, - rust_version: null, - - afterCreate(version) { - if (!version.crateId) { - throw new Error(`Missing \`crate\` relationship on \`version:${version.num}\``); - } - }, -}); diff --git a/mirage/fixtures/categories.js b/mirage/fixtures/categories.js deleted file mode 100644 index 05b37f8d55a..00000000000 --- a/mirage/fixtures/categories.js +++ /dev/null @@ -1,25 +0,0 @@ -export default [ - { - category: 'API bindings', - created_at: '2017-01-20T14:51:49Z', - description: - 'Idiomatic wrappers of specific APIs for convenient access from Rust. Includes HTTP API wrappers as well. Non-idiomatic or unsafe bindings can be found in External FFI bindings.', - id: 'api-bindings', - slug: 'api-bindings', - }, - { - category: 'Algorithms', - created_at: '2017-01-20T14:51:49Z', - description: 'Rust implementations of core algorithms such as hashing, sorting, searching, and more.', - id: 'algorithms', - slug: 'algorithms', - }, - { - category: 'Asynchronous', - created_at: '2017-01-20T14:51:49Z', - description: - 'Crates to help you deal with events independently of the main program flow, using techniques like futures, promises, waiting, or eventing.', - id: 'asynchronous', - slug: 'asynchronous', - }, -]; diff --git a/mirage/fixtures/crate-ownerships.js b/mirage/fixtures/crate-ownerships.js deleted file mode 100644 index 83a87eafd18..00000000000 --- a/mirage/fixtures/crate-ownerships.js +++ /dev/null @@ -1,18 +0,0 @@ -export default [ - { - crateId: 'nanomsg', - teamId: 1, - }, - { - crateId: 'nanomsg', - teamId: 303, - }, - { - crateId: 'nanomsg', - userId: 2, - }, - { - crateId: 'nanomsg', - userId: 303, - }, -]; diff --git a/mirage/fixtures/crates.js b/mirage/fixtures/crates.js deleted file mode 100644 index 08ff7c14306..00000000000 --- a/mirage/fixtures/crates.js +++ /dev/null @@ -1,319 +0,0 @@ -export default [ - { - badges: [], - created_at: '2014-11-23T09:01:21Z', - description: 'A Kinetic protocol library written in Rust', - documentation: 'https://icorderi.github.io/kinetic-rust/doc/kinetic/', - downloads: 225, - recent_downloads: 125, - homepage: 'https://icorderi.github.io/icorderi/kinetic-rust', - id: 'kinetic-rust', - keywordIds: [], - name: 'kinetic-rust', - repository: 'https://github.com/icorderi/kinetic-rust/', - updated_at: '2015-04-21T00:15:49Z', - versionIds: [], - }, - { - badges: [], - categoryIds: [], - created_at: '2014-12-08T02:08:06Z', - description: 'A high-level, Rust idiomatic wrapper around nanomsg.', - documentation: 'https://github.com/thehydroimpulse/nanomsg.rs', - downloads: 3888, - recent_downloads: 800, - homepage: 'https://github.com/thehydroimpulse/nanomsg.rs', - id: 'nanomsg', - keywordIds: ['network'], - name: 'nanomsg', - repository: 'https://github.com/thehydroimpulse/nanomsg.rs', - updated_at: '2016-12-28T08:40:00Z', - versionIds: [40_906, 40_905, 28_431, 21_273, 18_445, 17_384, 13_574, 9014, 8236, 7190, 4944, 940, 924], - _extra_downloads: [ - { - date: '2017-02-02', - downloads: 41, - }, - { - date: '2017-02-07', - downloads: 14, - }, - ], - teamOwnerIds: [1, 303], - userOwnerIds: [2, 303], - }, - { - created_at: '2015-02-27T11:52:13Z', - description: - 'Yo dawg, use Rust to generate Rust, right in your Rust. (See\n`external_mixin` to use scripting languages.)\n', - documentation: 'https://github.com/huonw/external_mixin#rust_mixin', - downloads: 477, - recent_downloads: 100, - exact_match: true, - homepage: 'https://github.com/huonw/external_mixin', - id: 'rust_mixin', - keywordIds: ['rust', 'plugin', 'code-generation'], - name: 'rust_mixin', - repository: 'https://github.com/huonw/external_mixin', - updated_at: '2015-02-27T11:52:13Z', - badges: [], - versionIds: [], - }, - { - created_at: '2015-02-27T11:51:58Z', - description: - 'Use your favourite interpreted language to generate your Rust, right\nin your Rust. Supports Python, Ruby and shell (`sh`) out of the box,\nwith an extensible macro to support any others. (See `rust_mixin` to\nbe able to use your all-time favourite language to generate your Rust.)\n', - documentation: 'https://github.com/huonw/external_mixin#external_mixin', - downloads: 497, - recent_downloads: 497, - homepage: 'https://github.com/huonw/external_mixin', - id: 'external_mixin', - keywordIds: ['python', 'ruby', 'shell', 'plugin', 'code-generation'], - name: 'external_mixin', - repository: 'https://github.com/huonw/external_mixin', - updated_at: '2015-02-27T11:51:58Z', - versionIds: [], - }, - { - created_at: '2015-02-27T11:51:40Z', - description: 'Backing library for `rust_mixin` and `external_mixin` to keep them\nDRY.\n', - documentation: 'https://github.com/huonw/external_mixin#external_mixin_base', - downloads: 989, - recent_downloads: 0, - homepage: 'https://github.com/huonw/external_mixin', - id: 'external_mixin_umbrella', - keywordIds: ['plugin', 'code-generation'], - name: 'external_mixin_umbrella', - repository: 'https://github.com/huonw/external_mixin', - updated_at: '2015-02-27T11:52:30Z', - versionIds: [], - }, - { - created_at: '2015-10-10T15:26:24Z', - description: - 'Adds String based inflections for Rust. Snake, kebab, camel, sentence, class, title, upper, and lower cases as well as ordinalize, deordinalize, demodulize, and foreign key are supported as both traits and pure functions acting on String types.\n', - documentation: 'http://whatisinternet.github.io/inflector/doc/inflector/', - downloads: 57, - recent_downloads: 1, - homepage: 'https://github.com/whatisinternet/inflector', - id: 'Inflector', - keywordIds: ['string', 'case', 'camel', 'snake', 'inflection'], - name: 'Inflector', - repository: 'https://github.com/whatisinternet/inflector', - updated_at: '2015-10-27T01:51:42Z', - versionIds: [], - }, - { - created_at: '2015-05-21T17:43:38Z', - description: 'Client for the ElasticSearch REST API', - documentation: 'http://benashford.github.io/rs-es/rs_es/index.html', - downloads: 321, - recent_downloads: 21, - homepage: null, - id: 'rs-es', - keywordIds: ['elasticsearch', 'elastic'], - name: 'rs-es', - repository: 'https://github.com/benashford/rs-es', - updated_at: '2015-09-09T15:34:50Z', - versionIds: [], - }, - { - created_at: '2014-11-21T05:12:08Z', - description: 'A (mostly) pure-Rust implementation of various common cryptographic algorithms.', - documentation: null, - downloads: 21_573, - recent_downloads: 2000, - homepage: 'https://github.com/DaGenix/rust-crypto/', - id: 'rust-crypto', - keywordIds: [], - name: 'rust-crypto', - repository: 'https://github.com/DaGenix/rust-crypto/', - updated_at: '2015-10-29T01:16:17Z', - versionIds: [], - }, - { - created_at: '2015-03-20T13:46:04Z', - description: 'This library provides HTSlib bindings and a high level Rust API for reading and writing BAM files.', - documentation: null, - downloads: 485, - recent_downloads: 85, - homepage: null, - id: 'rust-htslib', - keywordIds: [], - name: 'rust-htslib', - repository: 'https://github.com/rust-bio/rust-htslib.git', - updated_at: '2015-11-11T00:10:43Z', - versionIds: [], - }, - { - created_at: '2014-11-29T17:51:55Z', - description: 'Rustless is a REST-like API micro-framework for Rust.', - documentation: null, - downloads: 554, - recent_downloads: 500, - homepage: 'https://github.com/rustless/rustless', - id: 'rustless', - keywordIds: [], - name: 'rustless', - repository: 'https://crates.io/crates/rustless', - updated_at: '2015-10-31T11:49:29Z', - versionIds: [], - }, - { - created_at: '2014-12-05T20:20:39Z', - description: 'A generic serialization/deserialization framework', - documentation: 'https://serde-rs.github.io/serde/serde/serde/index.html', - downloads: 50_854, - recent_downloads: 854, - homepage: null, - id: 'serde', - keywordIds: [], - name: 'serde', - repository: 'https://github.com/serde-rs/serde', - updated_at: '2015-10-18T03:10:21Z', - versionIds: [], - }, - { - created_at: '2015-08-26T13:50:58Z', - description: 'Send cypher queries to a neo4j database', - documentation: 'http://livioribeiro.github.io/rusted_cypher/rusted_cypher/', - downloads: 156, - recent_downloads: 54, - homepage: 'https://github.com/livioribeiro/rusted-cypher', - id: 'rusted_cypher', - keywordIds: [], - name: 'rusted_cypher', - repository: 'https://github.com/livioribeiro/rusted-cypher', - updated_at: '2015-11-07T17:26:55Z', - versionIds: [], - }, - { - created_at: '2015-01-02T20:54:04Z', - description: - 'An (incomplete) port of zlib to Rust. The decompressor works, but the compressor has not yet been ported.', - documentation: null, - downloads: 223, - recent_downloads: 23, - homepage: null, - id: 'zlib', - keywordIds: [], - name: 'zlib', - repository: null, - updated_at: '2015-01-02T20:54:04Z', - versionIds: [], - }, - { - created_at: '2015-05-08T19:34:16Z', - description: - 'A light HTTP framework, with some REST-like features and the ambition of being simple, modular and non-intrusive.', - documentation: 'http://ogeon.github.io/docs/rustful/master/rustful/index.html', - downloads: 576, - recent_downloads: 76, - homepage: null, - id: 'rustful', - keywordIds: [], - name: 'rustful', - repository: 'https://github.com/Ogeon/rustful', - updated_at: '2015-09-19T21:10:27Z', - versionIds: [], - }, - { - created_at: '2014-11-24T02:34:44Z', - description: 'A native PostgreSQL driver', - documentation: 'https://sfackler.github.io/rust-postgres/doc/v0.10.1/postgres', - downloads: 13_449, - recent_downloads: 13, - homepage: null, - id: 'postgres', - keywordIds: [], - name: 'postgres', - repository: 'https://github.com/sfackler/rust-postgres', - updated_at: '2015-11-08T00:48:59Z', - versionIds: [], - }, - { - created_at: '2014-11-21T00:20:47Z', - description: 'Automatic property based testing with shrinking.', - documentation: 'http://burntsushi.net/rustdoc/quickcheck/', - downloads: 19_271, - recent_downloads: 143, - homepage: 'https://github.com/BurntSushi/quickcheck', - id: 'quickcheck', - keywordIds: [], - name: 'quickcheck', - repository: 'https://github.com/BurntSushi/quickcheck', - updated_at: '2015-09-20T21:53:38Z', - versionIds: [], - }, - { - created_at: '2014-11-21T00:21:04Z', - description: 'A macro attribute for quickcheck.', - documentation: 'http://burntsushi.net/rustdoc/quickcheck/', - downloads: 3796, - recent_downloads: 768, - homepage: 'https://github.com/BurntSushi/quickcheck', - id: 'quickcheck_macros', - keywordIds: [], - name: 'quickcheck_macros', - repository: 'https://github.com/BurntSushi/quickcheck', - updated_at: '2015-09-20T21:53:57Z', - versionIds: [], - }, - { - created_at: '2015-08-25T19:15:35Z', - description: - 'Lexical analysers generator for Rust, written in Rust (crate dedicated to rumblebars, divergences written by Nicolas Cherel)', - documentation: null, - downloads: 109, - recent_downloads: 0, - homepage: 'https://github.com/nicolas-cherel/rustlex', - id: 'unicorn-rpc', - keywordIds: [], - name: 'unicorn-rpc', - repository: 'https://github.com/nicolas-cherel/rustlex', - updated_at: '2015-08-25T19:15:35Z', - versionIds: [28_674], - }, - { - created_at: '2015-01-17T17:47:52Z', - description: 'A byte-oriented, zero-copy, parser combinators library', - documentation: 'http://rust.unhandledexpression.com/nom/', - downloads: 5169, - recent_downloads: 69, - homepage: null, - id: 'nom', - keywordIds: [], - name: 'nom', - repository: 'https://github.com/Geal/nom', - updated_at: '2015-11-22T22:00:41Z', - versionIds: [], - }, - { - id: 'libc', - name: 'libc', - downloads: 5169, - recent_downloads: 69, - updated_at: '2015-04-21T00:15:49Z', - }, - { - id: 'nanomsg-sys', - name: 'nanomsg-sys', - downloads: 5169, - recent_downloads: 69, - updated_at: '2015-04-21T00:15:49Z', - }, - { - id: 'mock-build-deps', - name: 'mock-build-deps', - downloads: 5169, - recent_downloads: 69, - updated_at: '2015-04-21T00:15:49Z', - }, - { - id: 'mock-dev-deps', - name: 'mock-dev-deps', - downloads: 5169, - recent_downloads: 69, - updated_at: '2015-04-21T00:15:49Z', - }, -]; diff --git a/mirage/fixtures/dependencies.js b/mirage/fixtures/dependencies.js deleted file mode 100644 index ea9384d747f..00000000000 --- a/mirage/fixtures/dependencies.js +++ /dev/null @@ -1,57 +0,0 @@ -export default [ - { - crateId: 'libc', - default_features: true, - features: '', - id: 146_231, - kind: 'normal', - optional: false, - req: '^0.2.18', - target: null, - versionId: 40_905, - }, - { - crateId: 'nanomsg-sys', - default_features: true, - features: '', - id: 146_232, - kind: 'normal', - optional: false, - req: '^0.6.1', - target: null, - versionId: 40_905, - }, - { - crateId: 'nanomsg', - default_features: true, - features: '', - id: 146_233, - kind: 'normal', - optional: false, - req: '^0.5.0', - target: null, - versionId: 28_674, - }, - { - crateId: 'mock-build-deps', - default_features: true, - features: '', - id: 146_234, - kind: 'build', - optional: false, - req: '^0.6.1', - target: null, - versionId: 40_905, - }, - { - crateId: 'mock-dev-deps', - default_features: true, - features: '', - id: 146_235, - kind: 'dev', - optional: true, - req: '^0.6.1', - target: null, - versionId: 40_905, - }, -]; diff --git a/mirage/fixtures/keywords.js b/mirage/fixtures/keywords.js deleted file mode 100644 index 74b081c5cc0..00000000000 --- a/mirage/fixtures/keywords.js +++ /dev/null @@ -1,72 +0,0 @@ -export default [ - { - created_at: '2014-11-23T06:47:40Z', - id: 'network', - keyword: 'network', - }, - { - created_at: '2014-11-23T06:47:40Z', - id: 'rust', - keyword: 'rust', - }, - { - created_at: '2014-11-23T06:47:40Z', - id: 'plugin', - keyword: 'plugin', - }, - { - created_at: '2014-11-23T06:47:40Z', - id: 'code-generation', - keyword: 'code-generation', - }, - { - created_at: '2014-11-23T06:47:40Z', - id: 'python', - keyword: 'python', - }, - { - created_at: '2014-11-23T06:47:40Z', - id: 'ruby', - keyword: 'ruby', - }, - { - created_at: '2014-11-23T06:47:40Z', - id: 'shell', - keyword: 'shell', - }, - { - created_at: '2014-11-23T06:47:40Z', - id: 'string', - keyword: 'string', - }, - { - created_at: '2014-11-23T06:47:40Z', - id: 'case', - keyword: 'case', - }, - { - created_at: '2014-11-23T06:47:40Z', - id: 'camel', - keyword: 'camel', - }, - { - created_at: '2014-11-23T06:47:40Z', - id: 'snake', - keyword: 'snake', - }, - { - created_at: '2014-11-23T06:47:40Z', - id: 'inflection', - keyword: 'inflection', - }, - { - created_at: '2014-11-23T06:47:40Z', - id: 'elastic', - keyword: 'elastic', - }, - { - created_at: '2014-11-23T06:47:40Z', - id: 'elasticsearch', - keyword: 'elasticsearch', - }, -]; diff --git a/mirage/fixtures/teams.js b/mirage/fixtures/teams.js deleted file mode 100644 index 0c753788449..00000000000 --- a/mirage/fixtures/teams.js +++ /dev/null @@ -1,16 +0,0 @@ -export default [ - { - avatar: 'https://avatars.githubusercontent.com/u/565790?v=3', - id: 1, - login: 'github:org:thehydroimpulse', - name: 'thehydroimpulseteam', - url: 'https://github.com/org_test', - }, - { - avatar: 'https://avatars.githubusercontent.com/u/9447137?v=3', - id: 303, - login: 'github:org:blabaere', - name: 'Team Benoît Labaere', - url: 'https://github.com/blabaere', - }, -]; diff --git a/mirage/fixtures/users.js b/mirage/fixtures/users.js deleted file mode 100644 index 00243bdce19..00000000000 --- a/mirage/fixtures/users.js +++ /dev/null @@ -1,26 +0,0 @@ -export default [ - { - avatar: 'https://avatars0.githubusercontent.com/u/9447137?v=3', - email: null, - id: 303, - login: 'blabaere', - name: 'Benoît Labaere', - url: 'https://github.com/blabaere', - }, - { - avatar: 'https://avatars.githubusercontent.com/u/565790?v=3', - email: 'dnfagnan@gmail.com', - id: 2, - login: 'thehydroimpulse', - name: 'Daniel Fagnan', - url: 'https://github.com/thehydroimpulse', - }, - { - avatar: 'https://avatars3.githubusercontent.com/u/1179195?v=3', - email: 'iain@fastmail.com', - id: 10_982, - login: 'iain8', - name: 'Iain Buchanan', - url: 'https://github.com/iain8', - }, -]; diff --git a/mirage/fixtures/version-downloads.js b/mirage/fixtures/version-downloads.js deleted file mode 100644 index 55aee7c1e6b..00000000000 --- a/mirage/fixtures/version-downloads.js +++ /dev/null @@ -1,17 +0,0 @@ -export default [ - { - date: '2017-02-10T00:00:00Z', - downloads: 2, - versionId: 40_905, - }, - { - date: '2017-02-10T00:00:00Z', - downloads: 1, - versionId: 18_445, - }, - { - date: '2017-02-11T00:00:00Z', - downloads: 1, - versionId: 40_905, - }, -]; diff --git a/mirage/fixtures/versions.js b/mirage/fixtures/versions.js deleted file mode 100644 index 87f37e27afc..00000000000 --- a/mirage/fixtures/versions.js +++ /dev/null @@ -1,412 +0,0 @@ -export default [ - { - crateId: 'nanomsg', - created_at: '2016-12-20T07:30:00Z', - downloads: 260, - features: { - bundled: ['nanomsg-sys/bundled'], - }, - id: 40_906, - num: '0.7.0-alpha.1', - updated_at: '2016-12-20T07:30:00Z', - yanked: false, - license: 'MIT', - crate_size: 912_355, - }, - { - crateId: 'nanomsg', - created_at: '2016-12-27T08:40:00Z', - downloads: 260, - features: { - bundled: ['nanomsg-sys/bundled'], - }, - id: 40_905, - num: '0.6.1', - crate_size: 8_123_545, - updated_at: '2016-12-27T08:40:00Z', - yanked: false, - license: 'Apache-2.0', - }, - { - crateId: 'nanomsg', - created_at: '2016-06-10T20:03:55Z', - downloads: 904, - features: {}, - id: 28_431, - num: '0.6.0', - updated_at: '2016-06-10T20:03:55Z', - yanked: false, - license: 'Apache-2.0', - }, - { - crateId: 'nanomsg', - created_at: '2016-01-24T22:07:58Z', - downloads: 1217, - features: {}, - id: 21_273, - num: '0.5.0', - updated_at: '2016-01-24T22:07:58Z', - yanked: false, - license: 'MIT/Apache-2.0', - }, - { - crateId: 'nanomsg', - created_at: '2015-11-23T12:10:09Z', - downloads: 318, - features: {}, - id: 18_445, - num: '0.4.2', - updated_at: '2015-12-16T00:01:56Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'nanomsg', - created_at: '2015-10-29T22:13:45Z', - downloads: 168, - features: {}, - id: 17_384, - num: '0.4.1', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'nanomsg', - created_at: '2015-07-23T05:54:44Z', - downloads: 311, - features: {}, - id: 13_574, - num: '0.4.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'nanomsg', - created_at: '2015-04-18T20:45:03Z', - downloads: 237, - features: {}, - id: 9014, - num: '0.3.4', - updated_at: '2015-12-15T00:03:39Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'nanomsg', - created_at: '2015-04-06T18:57:47Z', - downloads: 99, - features: {}, - id: 8236, - num: '0.3.3', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'nanomsg', - created_at: '2015-03-26T06:51:10Z', - downloads: 98, - features: {}, - id: 7190, - num: '0.3.2', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'Apache-2.0', - }, - { - crateId: 'nanomsg', - created_at: '2015-02-12T20:20:32Z', - downloads: 95, - features: {}, - id: 4944, - num: '0.3.1', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT/Apache-2.0', - }, - { - crateId: 'nanomsg', - created_at: '2014-12-08T16:21:01Z', - downloads: 102, - features: {}, - id: 940, - num: '0.3.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'nanomsg', - created_at: '2014-12-08T02:08:06Z', - downloads: 79, - features: {}, - id: 924, - num: '0.2.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'unicorn-rpc', - created_at: '2014-12-08T02:08:06Z', - downloads: 79, - features: {}, - id: 28_674, - num: '0.2.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'external_mixin', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'external_mixin_umbrella', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'Inflector', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'kinetic-rust', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '0.0.16', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'libc', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'mock-build-deps', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'mock-dev-deps', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'nanomsg-sys', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'nom', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'postgres', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'quickcheck', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'quickcheck_macros', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'rs-es', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'rust-crypto', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'rust-htslib', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'rust_mixin', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'rusted_cypher', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'rustful', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'rustless', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'serde', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'zlib', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'rustful', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'rustful', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'rustful', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, - { - crateId: 'rustful', - created_at: '2014-12-08T02:08:06Z', - downloads: 0, - features: {}, - num: '1.0.0', - updated_at: '2015-12-11T23:54:29Z', - yanked: false, - license: 'MIT', - }, -]; diff --git a/mirage/models/api-token.js b/mirage/models/api-token.js deleted file mode 100644 index b31ed19815c..00000000000 --- a/mirage/models/api-token.js +++ /dev/null @@ -1,5 +0,0 @@ -import { belongsTo, Model } from 'miragejs'; - -export default Model.extend({ - user: belongsTo(), -}); diff --git a/mirage/models/category-slug.js b/mirage/models/category-slug.js deleted file mode 100644 index db502f1428e..00000000000 --- a/mirage/models/category-slug.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Model } from 'miragejs'; - -export default Model.extend({}); diff --git a/mirage/models/category.js b/mirage/models/category.js deleted file mode 100644 index db502f1428e..00000000000 --- a/mirage/models/category.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Model } from 'miragejs'; - -export default Model.extend({}); diff --git a/mirage/models/crate-owner-invitation.js b/mirage/models/crate-owner-invitation.js deleted file mode 100644 index d5c705900c7..00000000000 --- a/mirage/models/crate-owner-invitation.js +++ /dev/null @@ -1,7 +0,0 @@ -import { belongsTo, Model } from 'miragejs'; - -export default Model.extend({ - crate: belongsTo(), - invitee: belongsTo('user'), - inviter: belongsTo('user'), -}); diff --git a/mirage/models/crate-ownership.js b/mirage/models/crate-ownership.js deleted file mode 100644 index f02485af15c..00000000000 --- a/mirage/models/crate-ownership.js +++ /dev/null @@ -1,7 +0,0 @@ -import { belongsTo, Model } from 'miragejs'; - -export default Model.extend({ - crate: belongsTo(), - team: belongsTo(), - user: belongsTo(), -}); diff --git a/mirage/models/crate.js b/mirage/models/crate.js deleted file mode 100644 index 0eac04bc161..00000000000 --- a/mirage/models/crate.js +++ /dev/null @@ -1,7 +0,0 @@ -import { hasMany, Model } from 'miragejs'; - -export default Model.extend({ - categories: hasMany(), - keywords: hasMany(), - versions: hasMany(), -}); diff --git a/mirage/models/dependency.js b/mirage/models/dependency.js deleted file mode 100644 index cc33c827100..00000000000 --- a/mirage/models/dependency.js +++ /dev/null @@ -1,6 +0,0 @@ -import { belongsTo, Model } from 'miragejs'; - -export default Model.extend({ - crate: belongsTo(), - version: belongsTo(), -}); diff --git a/mirage/models/keyword.js b/mirage/models/keyword.js deleted file mode 100644 index db502f1428e..00000000000 --- a/mirage/models/keyword.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Model } from 'miragejs'; - -export default Model.extend({}); diff --git a/mirage/models/mirage-session.js b/mirage/models/mirage-session.js deleted file mode 100644 index 8f2c0d402a3..00000000000 --- a/mirage/models/mirage-session.js +++ /dev/null @@ -1,13 +0,0 @@ -import { belongsTo, Model } from 'miragejs'; - -/** - * This is a mirage-only model, that is used to keep track of the current - * session and the associated `user` model, because in route handlers we don't - * have access to the cookie data that the actual API is using for these things. - * - * This mock implementation means that there can only ever exist one - * session at a time. - */ -export default Model.extend({ - user: belongsTo(), -}); diff --git a/mirage/models/owned-crate.js b/mirage/models/owned-crate.js deleted file mode 100644 index db502f1428e..00000000000 --- a/mirage/models/owned-crate.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Model } from 'miragejs'; - -export default Model.extend({}); diff --git a/mirage/models/team.js b/mirage/models/team.js deleted file mode 100644 index db502f1428e..00000000000 --- a/mirage/models/team.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Model } from 'miragejs'; - -export default Model.extend({}); diff --git a/mirage/models/user.js b/mirage/models/user.js deleted file mode 100644 index ecfbd627b7e..00000000000 --- a/mirage/models/user.js +++ /dev/null @@ -1,5 +0,0 @@ -import { hasMany, Model } from 'miragejs'; - -export default Model.extend({ - followedCrates: hasMany('crate'), -}); diff --git a/mirage/models/version-download.js b/mirage/models/version-download.js deleted file mode 100644 index 2ed283ffd9b..00000000000 --- a/mirage/models/version-download.js +++ /dev/null @@ -1,5 +0,0 @@ -import { belongsTo, Model } from 'miragejs'; - -export default Model.extend({ - version: belongsTo(), -}); diff --git a/mirage/models/version.js b/mirage/models/version.js deleted file mode 100644 index c9fe6d7b193..00000000000 --- a/mirage/models/version.js +++ /dev/null @@ -1,6 +0,0 @@ -import { belongsTo, Model } from 'miragejs'; - -export default Model.extend({ - crate: belongsTo(), - publishedBy: belongsTo('user'), -}); diff --git a/mirage/route-handlers/-utils.js b/mirage/route-handlers/-utils.js deleted file mode 100644 index cb727b41e98..00000000000 --- a/mirage/route-handlers/-utils.js +++ /dev/null @@ -1,49 +0,0 @@ -import { Response } from 'miragejs'; -import semverParse from 'semver/functions/parse'; -import semverSort from 'semver/functions/rsort'; - -export function notFound() { - return new Response( - 404, - { 'Content-Type': 'application/json' }, - { - errors: [{ detail: 'Not Found' }], - }, - ); -} - -export function pageParams(request) { - const { queryParams } = request; - - const page = parseInt(queryParams.page || '1'); - const perPage = parseInt(queryParams.per_page || '10'); - - const start = (page - 1) * perPage; - const end = start + perPage; - - return { page, perPage, start, end }; -} - -export function compareStrings(a, b) { - return a < b ? -1 : a > b ? 1 : 0; -} - -export function compareIsoDates(a, b) { - let aDate = new Date(a); - let bDate = new Date(b); - return aDate < bDate ? -1 : aDate > bDate ? 1 : 0; -} - -export function releaseTracks(versions) { - let versionNums = versions.models.filter(it => !it.yanked).map(it => it.num); - semverSort(versionNums, { loose: true }); - let tracks = {}; - for (let num of versionNums) { - let semver = semverParse(num, { loose: true }); - if (!semver || semver.prerelease.length !== 0) continue; - let name = semver.major == 0 ? `0.${semver.minor}` : `${semver.major}`; - if (name in tracks) continue; - tracks[name] = { highest: num }; - } - return tracks; -} diff --git a/mirage/route-handlers/categories.js b/mirage/route-handlers/categories.js deleted file mode 100644 index 635fb51ebf7..00000000000 --- a/mirage/route-handlers/categories.js +++ /dev/null @@ -1,30 +0,0 @@ -import { compareStrings, notFound, pageParams } from './-utils'; - -export function register(server) { - server.get('/api/v1/categories', function (schema, request) { - let { start, end } = pageParams(request); - - let allCategories = schema.categories.all().sort((a, b) => compareStrings(a.category, b.category)); - let categories = allCategories.slice(start, end); - let total = allCategories.length; - - return { ...this.serialize(categories), meta: { total } }; - }); - - server.get('/api/v1/categories/:category_id', function (schema, request) { - let catId = request.params.category_id; - let category = schema.categories.find(catId); - return category ?? notFound(); - }); - - server.get('/api/v1/category_slugs', function (schema) { - let allCategories = schema.categories.all().sort((a, b) => compareStrings(a.category, b.category)); - return { - category_slugs: this.serialize(allCategories).categories.map(cat => ({ - id: cat.id, - slug: cat.slug, - description: cat.description, - })), - }; - }); -} diff --git a/mirage/route-handlers/crates.js b/mirage/route-handlers/crates.js deleted file mode 100644 index e78dc3405a8..00000000000 --- a/mirage/route-handlers/crates.js +++ /dev/null @@ -1,468 +0,0 @@ -import { Response } from 'miragejs'; - -import { getSession } from '../utils/session'; -import { compareIsoDates, compareStrings, notFound, pageParams, releaseTracks } from './-utils'; - -function toCanonicalName(name) { - return name.toLowerCase().replace(/-/g, '_'); -} - -export function list(schema, request) { - const { start, end } = pageParams(request); - - let crates = schema.crates.all(); - - if (request.queryParams.following === '1') { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - crates = user.followedCrates; - } - - if (request.queryParams.letter) { - let letter = request.queryParams.letter.toLowerCase(); - crates = crates.filter(crate => crate.name[0].toLowerCase() === letter); - } - - if (request.queryParams.q) { - let q = request.queryParams.q.toLowerCase(); - crates = crates.filter(crate => crate.name.toLowerCase().includes(q)); - } - - if (request.queryParams.user_id) { - let userId = parseInt(request.queryParams.user_id, 10); - crates = crates.filter(crate => schema.crateOwnerships.findBy({ crateId: crate.id, userId })); - } - - if (request.queryParams.team_id) { - let teamId = parseInt(request.queryParams.team_id, 10); - crates = crates.filter(crate => schema.crateOwnerships.findBy({ crateId: crate.id, teamId })); - } - - let { ids } = request.queryParams; - if (ids) { - crates = crates.filter(crate => ids.includes(crate.name)); - } - - if (request.queryParams.sort === 'alpha') { - crates = crates.sort((a, b) => compareStrings(a.id.toLowerCase(), b.id.toLowerCase())); - } else if (request.queryParams.sort === 'recent-downloads') { - crates = crates.sort((a, b) => b.recent_downloads - a.recent_downloads); - } - - return { ...this.serialize(crates.slice(start, end)), meta: { total: crates.length } }; -} - -export function register(server) { - server.get('/api/v1/crates', list); - - server.get('/api/v1/crates/:name', function (schema, request) { - let { name } = request.params; - let canonicalName = toCanonicalName(name); - let crate = schema.crates.all().models.find(it => toCanonicalName(it.name) === canonicalName); - if (!crate) return notFound(); - let serialized = this.serialize(crate); - let includes = request.queryParams?.include ?? ''; - let includeDefaultVersion = includes.includes('default_version') && !includes.includes('versions'); - return { - categories: null, - keywords: null, - versions: null, - ...serialized, - ...(serialized.crate.categories && this.serialize(crate.categories)), - ...(serialized.crate.keywords && this.serialize(crate.keywords)), - ...(serialized.crate.versions && this.serialize(crate.versions.sort((a, b) => Number(b.id) - Number(a.id)))), - // `default_version` share the same key `versions` - ...(includeDefaultVersion && { - versions: [serialized.crate.default_version].map( - num => this.serialize(schema.versions.findBy({ num })).version, - ), - }), - }; - }); - - server.delete('/api/v1/crates/:name', (schema, request) => { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - let { name } = request.params; - let crate = schema.crates.findBy({ name }); - if (!crate) { - return new Response(404, {}, { errors: [{ detail: `crate \`${name}\` does not exist` }] }); - } - - crate.destroy(); - - return ''; - }); - - server.get('/api/v1/crates/:name/following', (schema, request) => { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - let { name } = request.params; - let crate = schema.crates.findBy({ name }); - if (!crate) { - return new Response(404, {}, { errors: [{ detail: 'Not Found' }] }); - } - - let following = user.followedCrates.includes(crate); - - return { following }; - }); - - server.put('/api/v1/crates/:name/follow', (schema, request) => { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - let { name } = request.params; - let crate = schema.crates.findBy({ name }); - if (!crate) { - return new Response(404, {}, { errors: [{ detail: 'Not Found' }] }); - } - - user.followedCrates.add(crate); - user.save(); - - return { ok: true }; - }); - - server.delete('/api/v1/crates/:name/follow', (schema, request) => { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - let { name } = request.params; - let crate = schema.crates.findBy({ name }); - if (!crate) { - return new Response(404, {}, { errors: [{ detail: 'Not Found' }] }); - } - - user.followedCrates.remove(crate); - user.save(); - - return { ok: true }; - }); - - server.get('/api/v1/crates/:name/versions', function (schema, request) { - let { name } = request.params; - let crate = schema.crates.findBy({ name }); - if (!crate) return notFound(); - - let versions = crate.versions; - let { nums } = request.queryParams; - if (nums) { - versions = versions.filter(version => nums.includes(version.num)); - } - versions = versions.sort((a, b) => compareIsoDates(b.created_at, a.created_at)); - let total = versions.length; - let include = request.queryParams?.include ?? ''; - let release_tracks = include.split(',').includes('release_tracks') && releaseTracks(crate.versions); - let resp = { - ...this.serialize(versions), - meta: { total, next_page: null }, - }; - if (release_tracks && Object.keys(release_tracks).length !== 0) { - resp.meta.release_tracks = release_tracks; - } - return resp; - }); - - server.get('/api/v1/crates/:name/:version/authors', (schema, request) => { - let { name } = request.params; - let crate = schema.crates.findBy({ name }); - if (!crate) return notFound(); - - let num = request.params.version; - let version = schema.versions.findBy({ crateId: crate.id, num }); - if (!version) - return new Response( - 404, - {}, - { errors: [{ detail: `crate \`${crate.name}\` does not have a version \`${num}\`` }] }, - ); - - return { meta: { names: [] }, users: [] }; - }); - - server.get('/api/v1/crates/:name/:version/dependencies', (schema, request) => { - let { name } = request.params; - let crate = schema.crates.findBy({ name }); - if (!crate) return notFound(); - - let num = request.params.version; - let version = schema.versions.findBy({ crateId: crate.id, num }); - if (!version) - return new Response( - 404, - {}, - { errors: [{ detail: `crate \`${crate.name}\` does not have a version \`${num}\`` }] }, - ); - - return schema.dependencies.where({ versionId: version.id }); - }); - - server.get('/api/v1/crates/:name/:version/downloads', function (schema, request) { - let { name } = request.params; - let crate = schema.crates.findBy({ name }); - if (!crate) return notFound(); - - let num = request.params.version; - let version = schema.versions.findBy({ crateId: crate.id, num }); - if (!version) - return new Response( - 404, - {}, - { errors: [{ detail: `crate \`${crate.name}\` does not have a version \`${num}\`` }] }, - ); - - return schema.versionDownloads.where({ versionId: version.id }); - }); - - server.get('/api/v1/crates/:name/owner_user', function (schema, request) { - let { name } = request.params; - let crate = schema.crates.findBy({ name }); - if (!crate) return notFound(); - - let ownerships = schema.crateOwnerships.where({ crateId: crate.id }).filter(it => it.userId).models; - - return { - users: ownerships.map(it => { - let json = this.serialize(it.user, 'user').user; - json.kind = 'user'; - return json; - }), - }; - }); - - server.get('/api/v1/crates/:name/owner_team', function (schema, request) { - let { name } = request.params; - let crate = schema.crates.findBy({ name }); - if (!crate) return notFound(); - - let ownerships = schema.crateOwnerships.where({ crateId: crate.id }).filter(it => it.teamId).models; - - return { - teams: ownerships.map(it => { - let json = this.serialize(it.team, 'team').team; - json.kind = 'team'; - return json; - }), - }; - }); - - server.get('/api/v1/crates/:name/reverse_dependencies', function (schema, request) { - let { name } = request.params; - let crate = schema.crates.findBy({ name }); - if (!crate) return notFound(); - - let { start, end } = pageParams(request); - - let allDependencies = schema.dependencies.where({ crateId: crate.id }); - let dependencies = allDependencies.slice(start, end); - let total = allDependencies.length; - - let versions = schema.versions.find(dependencies.models.map(it => it.versionId)); - - return { - ...this.serialize(dependencies), - ...this.serialize(versions), - meta: { total }, - }; - }); - - server.get('/api/v1/crates/:name/downloads', function (schema, request) { - let { name } = request.params; - let crate = schema.crates.findBy({ name }); - if (!crate) return notFound(); - - let versionDownloads = schema.versionDownloads.all().filter(it => it.version.crateId === crate.id); - - return { ...this.serialize(versionDownloads), meta: { extra_downloads: crate._extra_downloads } }; - }); - - server.put('/api/v1/crates/:name/owners', (schema, request) => { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - let { name } = request.params; - let crate = schema.crates.findBy({ name }); - - if (!crate) { - return notFound(); - } - - const body = JSON.parse(request.requestBody); - - let users = []; - let teams = []; - let msgs = []; - for (let login of body.owners) { - if (login.includes(':')) { - let team = schema.teams.findBy({ login }); - if (!team) { - return new Response(404, {}, { errors: [{ detail: `could not find team with login \`${login}\`` }] }); - } - - teams.push(team); - msgs.push(`team ${login} has been added as an owner of crate ${crate.name}`); - } else { - let user = schema.users.findBy({ login }); - if (!user) { - return new Response(404, {}, { errors: [{ detail: `could not find user with login \`${login}\`` }] }); - } - - users.push(user); - msgs.push(`user ${login} has been invited to be an owner of crate ${crate.name}`); - } - } - - for (let team of teams) { - schema.crateOwnerships.create({ crate, team }); - } - - for (let invitee of users) { - schema.crateOwnerInvitations.create({ crate, inviter: user, invitee }); - } - - let msg = msgs.join(','); - return { ok: true, msg }; - }); - - server.delete('/api/v1/crates/:name/owners', (schema, request) => { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - let { name } = request.params; - let crate = schema.crates.findBy({ name }); - - if (!crate) { - return notFound(); - } - - const body = JSON.parse(request.requestBody); - const [ownerId] = body.owners; - const owner = schema.users.findBy({ login: ownerId }) || schema.teams.findBy({ login: ownerId }); - - if (!owner) { - return notFound(); - } - - return { ok: true, msg: 'owners successfully removed' }; - }); - - server.get('/api/v1/crates/:name/:version', function (schema, request) { - let { name } = request.params; - let crate = schema.crates.findBy({ name }); - if (!crate) return notFound(); - - let num = request.params.version; - let version = schema.versions.findBy({ crateId: crate.id, num }); - if (!version) { - return new Response( - 404, - {}, - { errors: [{ detail: `crate \`${crate.name}\` does not have a version \`${num}\`` }] }, - ); - } - return this.serialize(version); - }); - - server.patch('/api/v1/crates/:name/:version', function (schema, request) { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - const { name, version: num } = request.params; - const crate = schema.crates.findBy({ name }); - if (!crate) { - return notFound(); - } - - const version = schema.versions.findBy({ crateId: crate.id, num }); - if (!version) { - return notFound(); - } - - const body = JSON.parse(request.requestBody); - version.update({ - yanked: body.version.yanked, - yank_message: body.version.yanked ? body.version.yank_message || null : null, - }); - - return this.serialize(version); - }); - - server.delete('/api/v1/crates/:name/:version/yank', (schema, request) => { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - const { name, version: num } = request.params; - const crate = schema.crates.findBy({ name }); - if (!crate) { - return notFound(); - } - - const version = schema.versions.findBy({ crateId: crate.id, num }); - if (!version) { - return notFound(); - } - - version.update({ yanked: true }); - - return { ok: true }; - }); - - server.put('/api/v1/crates/:name/:version/unyank', (schema, request) => { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - const { name, version: num } = request.params; - const crate = schema.crates.findBy({ name }); - if (!crate) { - return notFound(); - } - - const version = schema.versions.findBy({ crateId: crate.id, num }); - if (!version) { - return notFound(); - } - - version.update({ yanked: false, yank_message: null }); - - return { ok: true }; - }); - - server.get('/api/v1/crates/:name/:version/readme', (schema, request) => { - const { name, version: num } = request.params; - const crate = schema.crates.findBy({ name }); - if (!crate) { - return new Response(403, { 'Content-Type': 'text/html' }, ''); - } - - const version = schema.versions.findBy({ crateId: crate.id, num }); - if (!version || !version.readme) { - return new Response(403, { 'Content-Type': 'text/html' }, ''); - } - - return new Response(200, { 'Content-Type': 'text/html' }, version.readme); - }); -} diff --git a/mirage/route-handlers/docs-rs.js b/mirage/route-handlers/docs-rs.js deleted file mode 100644 index 1dce1d72fd3..00000000000 --- a/mirage/route-handlers/docs-rs.js +++ /dev/null @@ -1,5 +0,0 @@ -export function register(server) { - server.get('https://docs.rs/crate/:crate/:version/status.json', function () { - return {}; - }); -} diff --git a/mirage/route-handlers/invites.js b/mirage/route-handlers/invites.js deleted file mode 100644 index 4271dc71e1a..00000000000 --- a/mirage/route-handlers/invites.js +++ /dev/null @@ -1,49 +0,0 @@ -import { Response } from 'miragejs'; - -import { getSession } from '../utils/session'; -import { notFound } from './-utils'; - -export function register(server) { - server.get('/api/private/crate_owner_invitations', function (schema, request) { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - let invites; - if (request.queryParams['crate_name']) { - let crate = schema.crates.findBy({ name: request.queryParams['crate_name'] }); - if (!crate) return notFound(); - - invites = schema.crateOwnerInvitations.where({ crateId: crate.id }); - } else if (request.queryParams['invitee_id']) { - let inviteeId = request.queryParams['invitee_id']; - if (inviteeId !== user.id) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - invites = schema.crateOwnerInvitations.where({ inviteeId }); - } else { - return new Response(400, {}, { errors: [{ detail: 'missing or invalid filter' }] }); - } - - let perPage = 10; - let start = request.queryParams['__start__'] ?? 0; - let end = start + perPage; - - let nextPage = null; - if (invites.length > end) { - let url = new URL(request.url, 'https://crates.io'); - url.searchParams.set('__start__', end); - nextPage = url.search; - } - - invites = invites.slice(start, end); - - let response = this.serialize(invites); - response.users ??= []; - response.meta = { next_page: nextPage }; - - return response; - }); -} diff --git a/mirage/route-handlers/keywords.js b/mirage/route-handlers/keywords.js deleted file mode 100644 index 079eaba0609..00000000000 --- a/mirage/route-handlers/keywords.js +++ /dev/null @@ -1,19 +0,0 @@ -import { notFound, pageParams } from './-utils'; - -export function register(server) { - server.get('/api/v1/keywords', function (schema, request) { - let { start, end } = pageParams(request); - - let allKeywords = schema.keywords.all().sort((a, b) => a.crates_cnt - b.crates_cnt); - let keywords = allKeywords.slice(start, end); - let total = allKeywords.length; - - return { ...this.serialize(keywords), meta: { total } }; - }); - - server.get('/api/v1/keywords/:keyword_id', (schema, request) => { - let keywordId = request.params.keyword_id; - let keyword = schema.keywords.find(keywordId); - return keyword ?? notFound(); - }); -} diff --git a/mirage/route-handlers/me.js b/mirage/route-handlers/me.js deleted file mode 100644 index 16059579936..00000000000 --- a/mirage/route-handlers/me.js +++ /dev/null @@ -1,183 +0,0 @@ -import { Response } from 'miragejs'; - -import { getSession } from '../utils/session'; - -export function register(server) { - server.get('/api/v1/me', function (schema) { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - let ownerships = schema.crateOwnerships.where({ userId: user.id }).models; - - let json = this.serialize(user); - - json.owned_crates = ownerships.map(ownership => ({ - id: ownership.crate.id, - name: ownership.crate.name, - email_notifications: ownership.emailNotifications, - })); - - return json; - }); - - server.get('/api/v1/me/tokens', function (schema, request) { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - let expiredAfter = new Date(); - if (request.queryParams.expired_days) { - expiredAfter.setUTCDate(expiredAfter.getUTCDate() - request.queryParams.expired_days); - } - - return schema.apiTokens - .where({ userId: user.id }) - .filter(token => !token.expiredAt || new Date(token.expiredAt) > expiredAfter) - .sort((a, b) => Number(b.id) - Number(a.id)); - }); - - server.put('/api/v1/me/tokens', function (schema) { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - let { - name, - crateScopes = null, - endpointScopes = null, - expiredAt = null, - } = this.normalizedRequestAttrs('api-token'); - - let token = server.create('api-token', { - user, - name, - crateScopes, - endpointScopes, - expiredAt, - createdAt: new Date().toISOString(), - }); - - let json = this.serialize(token); - json.api_token.revoked = false; - json.api_token.token = token.token; - return json; - }); - - server.get('/api/v1/me/tokens/:tokenId', function (schema, request) { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - let { tokenId } = request.params; - let token = schema.apiTokens.findBy({ id: tokenId, userId: user.id }); - - if (!token) { - return new Response(404); - } - - return this.serialize(token); - }); - - server.delete('/api/v1/me/tokens/:tokenId', function (schema, request) { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - let { tokenId } = request.params; - let token = schema.apiTokens.findBy({ id: tokenId, userId: user.id }); - if (token) token.destroy(); - - return {}; - }); - - server.get('/api/v1/me/updates', function (schema, request) { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - let allVersions = schema.versions - .all() - .filter(version => user.followedCrates.includes(version.crate)) - .sort((a, b) => Number(b.id) - Number(a.id)); - - let page = Number(request.queryParams.page) || 1; - let perPage = 10; - - let begin = (page - 1) * perPage; - let end = begin + perPage; - - let versions = allVersions.slice(begin, end); - - let totalCount = allVersions.length; - let totalPages = Math.ceil(totalCount / perPage); - let more = page < totalPages; - - return { ...this.serialize(versions), meta: { more } }; - }); - - server.put('/api/v1/confirm/:token', (schema, request) => { - let { token } = request.params; - - let user = schema.users.findBy({ emailVerificationToken: token }); - if (!user) { - return new Response(400, {}, { errors: [{ detail: 'Email belonging to token not found.' }] }); - } - - user.update({ emailVerified: true, emailVerificationToken: null }); - - return { ok: true }; - }); - - server.get('/api/v1/me/crate_owner_invitations', function (schema) { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - return schema.crateOwnerInvitations.where({ inviteeId: user.id }); - }); - - server.put('/api/v1/me/crate_owner_invitations/:crate_id', (schema, request) => { - let { user } = getSession(schema); - if (!user) { - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - let body = JSON.parse(request.requestBody); - let { accepted, crate_id: crateId } = body.crate_owner_invite; - - let invite = schema.crateOwnerInvitations.findBy({ crateId, inviteeId: user.id }); - if (!invite) { - return new Response(404); - } - - if (accepted) { - server.create('crate-ownership', { crate: invite.crate, user }); - } - - invite.destroy(); - - return { crate_owner_invitation: { crate_id: crateId, accepted } }; - }); - - server.put('/api/v1/me/crate_owner_invitations/accept/:token', (schema, request) => { - let { token } = request.params; - - let invite = schema.crateOwnerInvitations.findBy({ token }); - if (!invite) { - return new Response(404); - } - - server.create('crate-ownership', { crate: invite.crate, user: invite.invitee }); - invite.destroy(); - - return { crate_owner_invitation: { crate_id: invite.crateId, accepted: true } }; - }); -} diff --git a/mirage/route-handlers/metadata.js b/mirage/route-handlers/metadata.js deleted file mode 100644 index 72bf61014bb..00000000000 --- a/mirage/route-handlers/metadata.js +++ /dev/null @@ -1,9 +0,0 @@ -const EXAMPLE_SHA1 = '5048d31943118c6d67359bd207d307c854e82f45'; - -export function register(server) { - server.get('/api/v1/site_metadata', { - commit: EXAMPLE_SHA1, - deployed_sha: EXAMPLE_SHA1, - read_only: false, - }); -} diff --git a/mirage/route-handlers/session.js b/mirage/route-handlers/session.js deleted file mode 100644 index 3b5d21710dd..00000000000 --- a/mirage/route-handlers/session.js +++ /dev/null @@ -1,12 +0,0 @@ -import { getSession } from '../utils/session'; - -export function register(server) { - server.del('/api/private/session', function (schema) { - let { session } = getSession(schema); - if (session) { - session.destroy(); - } - - return { ok: true }; - }); -} diff --git a/mirage/route-handlers/summary.js b/mirage/route-handlers/summary.js deleted file mode 100644 index d21ea0352e4..00000000000 --- a/mirage/route-handlers/summary.js +++ /dev/null @@ -1,41 +0,0 @@ -import { compareIsoDates } from './-utils'; - -export function summary(schema) { - let crates = schema.crates.all(); - - let just_updated = crates.sort((a, b) => compareIsoDates(b.updated_at, a.updated_at)).slice(0, 10); - let most_downloaded = crates.sort((a, b) => b.downloads - a.downloads).slice(0, 10); - let new_crates = crates.sort((a, b) => compareIsoDates(b.created_at, a.created_at)).slice(0, 10); - let most_recently_downloaded = crates.sort((a, b) => b.recent_downloads - a.recent_downloads).slice(0, 10); - - let num_crates = crates.length; - // eslint-disable-next-line unicorn/no-array-reduce - let num_downloads = crates.models.reduce((sum, crate) => sum + crate.downloads, 0); - - let popular_categories = schema.categories - .all() - .sort((a, b) => b.crates_cnt - a.crates_cnt) - .slice(0, 10); - let popular_keywords = schema.keywords - .all() - .sort((a, b) => b.crates_cnt - a.crates_cnt) - .slice(0, 10); - - return { - just_updated: this.serialize(just_updated).crates.map(it => ({ ...it, versions: null })), - most_downloaded: this.serialize(most_downloaded).crates.map(it => ({ ...it, versions: null })), - new_crates: this.serialize(new_crates).crates.map(it => ({ ...it, versions: null })), - most_recently_downloaded: this.serialize(most_recently_downloaded).crates.map(it => ({ - ...it, - versions: null, - })), - num_crates, - num_downloads, - popular_categories: this.serialize(popular_categories).categories, - popular_keywords: this.serialize(popular_keywords).keywords, - }; -} - -export function register(server) { - server.get('/api/v1/summary', summary); -} diff --git a/mirage/route-handlers/teams.js b/mirage/route-handlers/teams.js deleted file mode 100644 index 650a0ee396f..00000000000 --- a/mirage/route-handlers/teams.js +++ /dev/null @@ -1,9 +0,0 @@ -import { notFound } from './-utils'; - -export function register(server) { - server.get('/api/v1/teams/:team_id', (schema, request) => { - let login = request.params.team_id; - let team = schema.teams.findBy({ login }); - return team ?? notFound(); - }); -} diff --git a/mirage/route-handlers/users.js b/mirage/route-handlers/users.js deleted file mode 100644 index 92f594c4ccb..00000000000 --- a/mirage/route-handlers/users.js +++ /dev/null @@ -1,65 +0,0 @@ -import { Response } from 'miragejs'; - -import { getSession } from '../utils/session'; -import { notFound } from './-utils'; - -export function register(server) { - server.get('/api/v1/users/:user_id', (schema, request) => { - let login = request.params.user_id; - let user = schema.users.findBy({ login }); - return user ?? notFound(); - }); - - server.put('/api/v1/users/:user_id', (schema, request) => { - let { user } = getSession(schema); - if (!user) { - // unfortunately, it's hard to tell from the Rust code if this is the correct response - // in this case, but since it's used elsewhere I will assume for now that it's correct. - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - if (user.id !== request.params.user_id) { - return new Response(400, {}, { errors: [{ detail: 'current user does not match requested user' }] }); - } - - let json = JSON.parse(request.requestBody); - if (!json || !json.user) { - return new Response(400, {}, { errors: [{ detail: 'invalid json request' }] }); - } - - if (json.user.publish_notifications !== undefined) { - user.update({ publishNotifications: json.user.publish_notifications }); - } - - if (json.user.email !== undefined) { - if (!json.user.email) { - return new Response(400, {}, { errors: [{ detail: 'empty email rejected' }] }); - } - - user.update({ - email: json.user.email, - emailVerified: false, - emailVerificationToken: 'secret123', - }); - } - - return { ok: true }; - }); - - server.put('/api/v1/users/:user_id/resend', (schema, request) => { - let { user } = getSession(schema); - if (!user) { - // unfortunately, it's hard to tell from the Rust code if this is the correct response - // in this case, but since it's used elsewhere I will assume for now that it's correct. - return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] }); - } - - if (user.id !== request.params.user_id) { - return new Response(400, {}, { errors: [{ detail: 'current user does not match requested user' }] }); - } - - // let's pretend that we're sending an email here... :D - - return { ok: true }; - }); -} diff --git a/mirage/scenarios/default.js b/mirage/scenarios/default.js deleted file mode 100644 index 38aa00cd26b..00000000000 --- a/mirage/scenarios/default.js +++ /dev/null @@ -1,9 +0,0 @@ -import window from 'ember-window-mock'; - -export default function (server) { - let user = server.create('user'); - server.create('mirage-session', { user }); - window.localStorage.setItem('isLoggedIn', '1'); - - server.loadFixtures(); -} diff --git a/mirage/serializers/api-token.js b/mirage/serializers/api-token.js deleted file mode 100644 index de7cf95cb90..00000000000 --- a/mirage/serializers/api-token.js +++ /dev/null @@ -1,32 +0,0 @@ -import BaseSerializer from './application'; - -export default BaseSerializer.extend({ - getHashForResource() { - let [hash, addToIncludes] = BaseSerializer.prototype.getHashForResource.apply(this, arguments); - - if (Array.isArray(hash)) { - for (let resource of hash) { - this._adjust(resource); - } - } else { - this._adjust(hash); - } - - return [hash, addToIncludes]; - }, - - _adjust(hash) { - hash.id = Number(hash.id); - if (hash.created_at) { - hash.created_at = new Date(hash.created_at).toISOString(); - } - if (hash.expired_at) { - hash.expired_at = new Date(hash.expired_at).toISOString(); - } - if (hash.last_used_at) { - hash.last_used_at = new Date(hash.last_used_at).toISOString(); - } - delete hash.token; - delete hash.user_id; - }, -}); diff --git a/mirage/serializers/application.js b/mirage/serializers/application.js deleted file mode 100644 index 35c7341275c..00000000000 --- a/mirage/serializers/application.js +++ /dev/null @@ -1,26 +0,0 @@ -import { ActiveModelSerializer } from 'miragejs'; - -export default ActiveModelSerializer.extend({ - getHashForResource(resource) { - let isModel = this.isModel(resource); - let hash = ActiveModelSerializer.prototype.getHashForResource.apply(this, arguments); - - if (isModel) { - let links = this.links(resource); - if (links) { - hash[0].links = links; - } - } else { - for (let i = 0; i < hash[0].length && i < resource.models.length; i++) { - let links = this.links(resource.models[i]); - if (links) { - hash[0][i].links = links; - } - } - } - - return hash; - }, - - links() {}, -}); diff --git a/mirage/serializers/category.js b/mirage/serializers/category.js deleted file mode 100644 index 5d3cacb04eb..00000000000 --- a/mirage/serializers/category.js +++ /dev/null @@ -1,24 +0,0 @@ -import BaseSerializer from './application'; - -export default BaseSerializer.extend({ - getHashForResource() { - let [hash, addToIncludes] = BaseSerializer.prototype.getHashForResource.apply(this, arguments); - - if (Array.isArray(hash)) { - for (let resource of hash) { - this._adjust(resource); - } - } else { - this._adjust(hash); - } - - return [hash, addToIncludes]; - }, - - _adjust(hash) { - let allCrates = this.schema.crates.all(); - let associatedCrates = allCrates.filter(it => it.categoryIds.includes(hash.id)); - - hash.crates_cnt ??= associatedCrates.length; - }, -}); diff --git a/mirage/serializers/crate-owner-invitation.js b/mirage/serializers/crate-owner-invitation.js deleted file mode 100644 index 984bdaed1d3..00000000000 --- a/mirage/serializers/crate-owner-invitation.js +++ /dev/null @@ -1,35 +0,0 @@ -import BaseSerializer from './application'; - -export default BaseSerializer.extend({ - // eslint-disable-next-line ember/avoid-leaking-state-in-ember-objects - include: ['inviter', 'invitee'], - - getHashForResource() { - let [hash, addToIncludes] = BaseSerializer.prototype.getHashForResource.apply(this, arguments); - - if (Array.isArray(hash)) { - for (let resource of hash) { - this._adjust(resource); - } - } else { - this._adjust(hash); - } - - addToIncludes.sort((a, b) => a.id - b.id); - - return [hash, addToIncludes]; - }, - - _adjust(hash) { - delete hash.id; - delete hash.token; - - hash.crate_id = Number(hash.crate_id); - - let crate = this.schema.crates.find(hash.crate_id); - hash.crate_name = crate.name; - - hash.invitee_id = Number(hash.invitee_id); - hash.inviter_id = Number(hash.inviter_id); - }, -}); diff --git a/mirage/serializers/crate.js b/mirage/serializers/crate.js deleted file mode 100644 index 8cf4a30bf68..00000000000 --- a/mirage/serializers/crate.js +++ /dev/null @@ -1,110 +0,0 @@ -import prerelease from 'semver/functions/prerelease'; -import semverSort from 'semver/functions/rsort'; - -import { compareIsoDates } from '../route-handlers/-utils'; -import BaseSerializer from './application'; - -const VALID_INCLUDE_MODEL = new Set([ - 'versions', - 'default_version', - 'keywords', - 'categories', - /*, 'badges', 'downloads' */ -]); - -export default BaseSerializer.extend({ - include(request) { - let include = request.queryParams.include; - return include == null || include === 'full' - ? VALID_INCLUDE_MODEL.values() - : include.split(',').filter(it => VALID_INCLUDE_MODEL.has(it)); - }, - attrs: [ - 'badges', - 'categories', - 'created_at', - 'description', - 'documentation', - 'downloads', - 'recent_downloads', - 'homepage', - 'id', - 'keywords', - 'links', - 'newest_version', - 'name', - 'repository', - 'updated_at', - 'versions', - ], - - links(crate) { - return { - owner_user: `/api/v1/crates/${crate.name}/owner_user`, - owner_team: `/api/v1/crates/${crate.name}/owner_team`, - reverse_dependencies: `/api/v1/crates/${crate.name}/reverse_dependencies`, - version_downloads: `/api/v1/crates/${crate.name}/downloads`, - versions: `/api/v1/crates/${crate.name}/versions`, - }; - }, - - getHashForResource() { - let [hash, addToIncludes] = BaseSerializer.prototype.getHashForResource.apply(this, arguments); - let includes = [...this.include(this.request)]; - - if (Array.isArray(hash)) { - for (let resource of hash) { - this._adjust(resource, includes); - } - } else { - this._adjust(hash, includes); - } - - return [hash, addToIncludes]; - }, - - _adjust(hash, includes) { - let versions = this.schema.versions.where({ crateId: hash.id }); - if (versions.length === 0) { - throw new Error(`crate \`${hash.name}\` has no associated versions`); - } - - let versionsByNum = Object.fromEntries(versions.models.map(it => [it.num, it])); - let versionNums = Object.keys(versionsByNum); - semverSort(versionNums, { loose: true }); - hash.default_version = - versionNums.find(it => !prerelease(it, { loose: true }) && !versionsByNum[it].yanked) ?? - versionNums.find(it => !versionsByNum[it].yanked) ?? - versionNums[0]; - hash.yanked = versionsByNum[hash.default_version]?.yanked ?? false; - - if (includes.includes('versions')) { - versions = versions.filter(it => !it.yanked); - versionNums = versionNums.filter(it => !versionsByNum[it].yanked); - hash.max_version = versionNums[0] ?? '0.0.0'; - hash.max_stable_version = versionNums.find(it => !prerelease(it, { loose: true })) ?? null; - - let newestVersions = versions.models.sort((a, b) => compareIsoDates(b.updated_at, a.updated_at)); - hash.newest_version = newestVersions[0]?.num ?? '0.0.0'; - - hash.versions = hash.version_ids; - } else { - hash.max_version = '0.0.0'; - hash.newest_version = '0.0.0'; - hash.max_stable_version = null; - hash.versions = null; - } - delete hash.version_ids; - - hash.id = hash.name; - - hash.categories = includes.includes('categories') ? hash.category_ids : null; - delete hash.category_ids; - - hash.keywords = includes.includes('keywords') ? hash.keyword_ids : null; - delete hash.keyword_ids; - - delete hash.team_owner_ids; - delete hash.user_owner_ids; - }, -}); diff --git a/mirage/serializers/dependency.js b/mirage/serializers/dependency.js deleted file mode 100644 index 1cd79ca7b91..00000000000 --- a/mirage/serializers/dependency.js +++ /dev/null @@ -1,22 +0,0 @@ -import BaseSerializer from './application'; - -export default BaseSerializer.extend({ - getHashForResource() { - let [hash, addToIncludes] = BaseSerializer.prototype.getHashForResource.apply(this, arguments); - - if (Array.isArray(hash)) { - for (let resource of hash) { - this._adjust(resource); - } - } else { - this._adjust(hash); - } - - return [hash, addToIncludes]; - }, - - _adjust(hash) { - let crate = this.schema.crates.find(hash.crate_id); - hash.crate_id = crate.name; - }, -}); diff --git a/mirage/serializers/keyword.js b/mirage/serializers/keyword.js deleted file mode 100644 index bad201b12cf..00000000000 --- a/mirage/serializers/keyword.js +++ /dev/null @@ -1,24 +0,0 @@ -import BaseSerializer from './application'; - -export default BaseSerializer.extend({ - getHashForResource() { - let [hash, addToIncludes] = BaseSerializer.prototype.getHashForResource.apply(this, arguments); - - if (Array.isArray(hash)) { - for (let resource of hash) { - this._adjust(resource); - } - } else { - this._adjust(hash); - } - - return [hash, addToIncludes]; - }, - - _adjust(hash) { - let allCrates = this.schema.crates.all(); - let associatedCrates = allCrates.filter(it => it.keywordIds.includes(hash.id)); - - hash.crates_cnt ??= associatedCrates.length; - }, -}); diff --git a/mirage/serializers/team.js b/mirage/serializers/team.js deleted file mode 100644 index 6e56fd31e2d..00000000000 --- a/mirage/serializers/team.js +++ /dev/null @@ -1,22 +0,0 @@ -import BaseSerializer from './application'; - -export default BaseSerializer.extend({ - getHashForResource() { - let [hash, addToIncludes] = BaseSerializer.prototype.getHashForResource.apply(this, arguments); - - if (Array.isArray(hash)) { - for (let resource of hash) { - this._adjust(resource); - } - } else { - this._adjust(hash); - } - - return [hash, addToIncludes]; - }, - - _adjust(hash) { - hash.id = Number(hash.id); - delete hash.org; - }, -}); diff --git a/mirage/serializers/user.js b/mirage/serializers/user.js deleted file mode 100644 index d86d8012ad8..00000000000 --- a/mirage/serializers/user.js +++ /dev/null @@ -1,35 +0,0 @@ -import BaseSerializer from './application'; - -export default BaseSerializer.extend({ - getHashForResource() { - let [hash, addToIncludes] = BaseSerializer.prototype.getHashForResource.apply(this, arguments); - - let removePrivateData = this.request.url !== '/api/v1/me'; - - if (Array.isArray(hash)) { - for (let resource of hash) { - this._adjust(resource, { removePrivateData }); - } - } else { - this._adjust(hash, { removePrivateData }); - } - - return [hash, addToIncludes]; - }, - - _adjust(hash, { removePrivateData }) { - hash.id = Number(hash.id); - - if (removePrivateData) { - delete hash.email; - delete hash.email_verified; - delete hash.is_admin; - delete hash.publish_notifications; - } else { - hash.email_verification_sent = hash.email_verified || Boolean(hash.email_verification_token); - } - - delete hash.email_verification_token; - delete hash.followed_crate_ids; - }, -}); diff --git a/mirage/serializers/version-download.js b/mirage/serializers/version-download.js deleted file mode 100644 index 53b0582efcf..00000000000 --- a/mirage/serializers/version-download.js +++ /dev/null @@ -1,23 +0,0 @@ -import BaseSerializer from './application'; - -export default BaseSerializer.extend({ - getHashForResource() { - let [hash, addToIncludes] = BaseSerializer.prototype.getHashForResource.apply(this, arguments); - - if (Array.isArray(hash)) { - for (let resource of hash) { - this._adjust(resource); - } - } else { - this._adjust(hash); - } - - return [hash, addToIncludes]; - }, - - _adjust(hash) { - hash.version = hash.version_id; - delete hash.version_id; - delete hash.id; - }, -}); diff --git a/mirage/serializers/version.js b/mirage/serializers/version.js deleted file mode 100644 index 7b7b69a3e22..00000000000 --- a/mirage/serializers/version.js +++ /dev/null @@ -1,65 +0,0 @@ -/* eslint-disable ember/avoid-leaking-state-in-ember-objects */ - -import BaseSerializer from './application'; - -export default BaseSerializer.extend({ - attrs: [ - 'crate_id', - 'created_at', - 'downloads', - 'features', - 'id', - 'links', - 'num', - 'updated_at', - 'yanked', - 'yank_message', - 'license', - 'crate_size', - 'rust_version', - ], - - include: ['publishedBy'], - - links(version) { - return { - dependencies: `/api/v1/crates/${version.crate.name}/${version.num}/dependencies`, - version_downloads: `/api/v1/crates/${version.crate.name}/${version.num}/downloads`, - }; - }, - - getHashForResource() { - let [hash, addToIncludes] = BaseSerializer.prototype.getHashForResource.apply(this, arguments); - - if (Array.isArray(hash)) { - for (let resource of hash) { - this._adjust(resource, addToIncludes); - } - } else { - this._adjust(hash, addToIncludes); - } - - addToIncludes = addToIncludes.filter(it => it.modelName !== 'user'); - - return [hash, addToIncludes]; - }, - - _adjust(hash, includes) { - let crate = this.schema.crates.find(hash.crate_id); - - hash.dl_path = `/api/v1/crates/${crate.name}/${hash.num}/download`; - hash.readme_path = `/api/v1/crates/${crate.name}/${hash.num}/readme`; - hash.crate = crate.name; - - if (hash.published_by_id) { - let user = includes.find(it => it.modelName === 'user' && it.id === hash.published_by_id); - hash.published_by = this.getHashForIncludedResource(user)[0].users[0]; - } else { - hash.published_by = null; - } - - delete hash.crate_id; - delete hash.published_by_id; - delete hash.readme; - }, -}); diff --git a/mirage/utils/session.js b/mirage/utils/session.js deleted file mode 100644 index 88c98dab816..00000000000 --- a/mirage/utils/session.js +++ /dev/null @@ -1,9 +0,0 @@ -export function getSession(schema) { - let session = schema.mirageSessions.first(); - if (!session || Date.parse(session.expires) < Date.now()) { - return {}; - } - - let user = schema.users.find(session.userId); - return { session, user }; -} diff --git a/package.json b/package.json index aeb50c87c39..ef3c6d78640 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,6 @@ "ember-cli-head": "2.0.0", "ember-cli-htmlbars": "6.3.0", "ember-cli-inject-live-reload": "2.1.0", - "ember-cli-mirage": "3.0.4", "ember-cli-notifications": "9.1.0", "ember-click-outside": "6.1.1", "ember-concurrency": "4.0.2", @@ -101,6 +100,7 @@ "ember-event-helpers": "0.1.1", "ember-exam": "9.0.0", "ember-fetch": "8.1.2", + "ember-inflector": "5.0.2", "ember-keyboard": "9.0.1", "ember-link": "3.3.0", "ember-load-initializers": "3.0.1", @@ -129,7 +129,6 @@ "globby": "14.0.2", "loader.js": "4.7.0", "match-json": "1.3.7", - "miragejs": "0.1.48", "msw": "2.7.0", "normalize.css": "8.0.1", "nyc": "17.1.0", @@ -147,8 +146,7 @@ "ember-get-config": "2.1.1", "ember-inflector": "5.0.2", "ember-modifier": "4.2.0", - "ember-svg-jar>cheerio": "1.0.0-rc.12", - "miragejs": "0.1.48" + "ember-svg-jar>cheerio": "1.0.0-rc.12" }, "pnpm": { "peerDependencyRules": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68b43af7074..45497a473f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,6 @@ overrides: ember-inflector: 5.0.2 ember-modifier: 4.2.0 ember-svg-jar>cheerio: 1.0.0-rc.12 - miragejs: 0.1.48 importers: @@ -157,9 +156,6 @@ importers: ember-cli-inject-live-reload: specifier: 2.1.0 version: 2.1.0 - ember-cli-mirage: - specifier: 3.0.4 - version: 3.0.4(@ember-data/model@5.3.9(koxcr2evquefgswwm6nj66fy4q))(@ember/test-helpers@4.0.5(@babel/core@7.26.7)(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1)))(ember-data@5.3.9(@ember/string@3.1.1)(@ember/test-helpers@4.0.5(@babel/core@7.26.7)(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1)))(@ember/test-waiters@3.1.0)(ember-inflector@5.0.2(@babel/core@7.26.7))(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1))(qunit@2.24.1))(ember-qunit@9.0.1(@ember/test-helpers@4.0.5(@babel/core@7.26.7)(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1)))(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1))(qunit@2.24.1))(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1))(miragejs@0.1.48)(webpack@5.97.1) ember-cli-notifications: specifier: 9.1.0 version: 9.1.0(@babel/core@7.26.7)(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1)) @@ -190,6 +186,9 @@ importers: ember-fetch: specifier: 8.1.2 version: 8.1.2 + ember-inflector: + specifier: 5.0.2 + version: 5.0.2(@babel/core@7.26.7) ember-keyboard: specifier: 9.0.1 version: 9.0.1(@babel/core@7.26.7)(@ember/test-helpers@4.0.5(@babel/core@7.26.7)(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1)))(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1)) @@ -274,9 +273,6 @@ importers: match-json: specifier: 1.3.7 version: 1.3.7 - miragejs: - specifier: 0.1.48 - version: 0.1.48 msw: specifier: 2.7.0 version: 2.7.0(@types/node@22.10.10)(typescript@5.7.3) @@ -324,7 +320,7 @@ importers: version: 2.7.0(@types/node@22.10.10)(typescript@5.7.3) semver: specifier: ^7.6.3 - version: 7.6.3 + version: 7.7.0 devDependencies: happy-dom: specifier: 16.5.3 @@ -1775,9 +1771,6 @@ packages: '@mermaid-js/parser@0.3.0': resolution: {integrity: sha512-HsvL6zgE5sUPGgkIDlmAWR1HTNHz2Iy11BAWPTa4Jjabkpguy4Ze2gzfLrg6pdRuBvFwgUYyxiaNqZwrEEXepA==} - '@miragejs/pretender-node-polyfill@0.1.2': - resolution: {integrity: sha512-M/BexG/p05C5lFfMunxo/QcgIJnMT2vDVCd00wNqK2ImZONIlEETZwWJu1QtLxtmYlSHlCFl3JNzp0tLe7OJ5g==} - '@mrmlnc/readdir-enhanced@2.2.1': resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==} engines: {node: '>=4'} @@ -4499,26 +4492,6 @@ packages: resolution: {integrity: sha512-QkLGcYv1WRK35g4MWu/uIeJ5Suk2eJXKtZ+8s+qE7C9INmpCPyPxzaqZABquYzcWNzIdw6kYwz3NWAFdKYFxwg==} engines: {node: ^4.5 || 6.* || >= 7.*} - ember-cli-mirage@3.0.4: - resolution: {integrity: sha512-JpfZJIrvUAcwOVQ44aAzlYSbGiO4/nqnVAbzAKU4kztqgYvYGBa27FX5WxfpIGZMBdnt6OKh78rsimChWo6f/Q==} - engines: {node: 16.* || >= 18} - peerDependencies: - '@ember-data/model': '*' - '@ember/test-helpers': '*' - ember-data: '*' - ember-qunit: '*' - ember-source: '>= 3.28.0' - miragejs: 0.1.48 - peerDependenciesMeta: - '@ember-data/model': - optional: true - '@ember/test-helpers': - optional: true - ember-data: - optional: true - ember-qunit: - optional: true - ember-cli-normalize-entity-name@1.0.0: resolution: {integrity: sha512-rF4P1rW2P1gVX1ynZYPmuIf7TnAFDiJmIUFI1Xz16VYykUAyiOCme0Y22LeZq8rTzwBMiwBwoE3RO4GYWehXZA==} @@ -4657,10 +4630,6 @@ packages: peerDependencies: ember-source: ^3.25.0 || >=4.0.0 - ember-get-config@2.1.1: - resolution: {integrity: sha512-uNmv1cPG/4qsac8oIf5txJ2FZ8p88LEpG4P3dNcjsJS98Y8hd0GPMFwVqpnzI78Lz7VYRGQWY4jnE4qm5R3j4g==} - engines: {node: 12.* || 14.* || >= 16} - ember-in-element-polyfill@1.0.1: resolution: {integrity: sha512-eHs+7D7PuQr8a1DPqsJTsEyo3FZ1XuH6WEZaEBPDa9s0xLlwByCNKl8hi1EbXOgvgEZNHHi9Rh0vjxyfakrlgg==} engines: {node: 10.* || >= 12} @@ -5141,9 +5110,6 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true - fake-xml-http-request@2.1.2: - resolution: {integrity: sha512-HaFMBi7r+oEC9iJNpc3bvcW7Z7iLmM26hPDmlb0mFwyANSsOQAtJxbdWsXITKOzZUyMYK0zYCv3h5yDj9TsiXg==} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -5856,9 +5822,6 @@ packages: infer-owner@1.0.4: resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} - inflected@2.1.0: - resolution: {integrity: sha512-hAEKNxvHf2Iq3H60oMBHkB4wl5jn3TPF3+fXek/sRwAB5gP9xWs4r7aweSF95f99HFoz69pnZTcu8f0SIHV18w==} - inflection@2.0.1: resolution: {integrity: sha512-wzkZHqpb4eGrOKBl34xy3umnYHx8Si5R1U4fwmdxLo5gdH6mEK8gclckTj/qWqy4Je0bsDYe/qazZYuO7xe3XQ==} engines: {node: '>=14.0.0'} @@ -6755,10 +6718,6 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - miragejs@0.1.48: - resolution: {integrity: sha512-MGZAq0Q3OuRYgZKvlB69z4gLN4G3PvgC4A2zhkCXCXrLD5wm2cCnwNB59xOBVA+srZ0zEes6u+VylcPIkB4SqA==} - engines: {node: 6.* || 8.* || >= 10.*} - mississippi@3.0.0: resolution: {integrity: sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==} engines: {node: '>=4.0.0'} @@ -7554,9 +7513,6 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - pretender@3.4.7: - resolution: {integrity: sha512-jkPAvt1BfRi0RKamweJdEcnjkeu7Es8yix3bJ+KgBC5VpG/Ln4JE3hYN6vJym4qprm8Xo5adhWpm3HCoft1dOw==} - prettier-linter-helpers@1.0.0: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} @@ -11219,8 +11175,6 @@ snapshots: dependencies: langium: 3.0.0 - '@miragejs/pretender-node-polyfill@0.1.2': {} - '@mrmlnc/readdir-enhanced@2.2.1': dependencies: call-me-maybe: 1.0.2 @@ -14814,29 +14768,6 @@ snapshots: ember-cli-lodash-subset@2.0.1: {} - ember-cli-mirage@3.0.4(@ember-data/model@5.3.9(koxcr2evquefgswwm6nj66fy4q))(@ember/test-helpers@4.0.5(@babel/core@7.26.7)(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1)))(ember-data@5.3.9(@ember/string@3.1.1)(@ember/test-helpers@4.0.5(@babel/core@7.26.7)(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1)))(@ember/test-waiters@3.1.0)(ember-inflector@5.0.2(@babel/core@7.26.7))(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1))(qunit@2.24.1))(ember-qunit@9.0.1(@ember/test-helpers@4.0.5(@babel/core@7.26.7)(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1)))(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1))(qunit@2.24.1))(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1))(miragejs@0.1.48)(webpack@5.97.1): - dependencies: - '@babel/core': 7.26.7(supports-color@8.1.1) - '@embroider/macros': 1.16.10 - broccoli-file-creator: 2.1.1 - broccoli-funnel: 3.0.8 - broccoli-merge-trees: 4.2.0 - ember-auto-import: 2.10.0(webpack@5.97.1) - ember-cli-babel: 8.2.0(@babel/core@7.26.7) - ember-get-config: 2.1.1 - ember-inflector: 5.0.2(@babel/core@7.26.7) - ember-source: 6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1) - miragejs: 0.1.48 - optionalDependencies: - '@ember-data/model': 5.3.9(koxcr2evquefgswwm6nj66fy4q) - '@ember/test-helpers': 4.0.5(@babel/core@7.26.7)(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1)) - ember-data: 5.3.9(@ember/string@3.1.1)(@ember/test-helpers@4.0.5(@babel/core@7.26.7)(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1)))(@ember/test-waiters@3.1.0)(ember-inflector@5.0.2(@babel/core@7.26.7))(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1))(qunit@2.24.1) - ember-qunit: 9.0.1(@ember/test-helpers@4.0.5(@babel/core@7.26.7)(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1)))(ember-source@6.0.1(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.97.1))(qunit@2.24.1) - transitivePeerDependencies: - - '@glint/template' - - supports-color - - webpack - ember-cli-normalize-entity-name@1.0.0: dependencies: silent-error: 1.1.1 @@ -15257,14 +15188,6 @@ snapshots: transitivePeerDependencies: - supports-color - ember-get-config@2.1.1: - dependencies: - '@embroider/macros': 1.16.10 - ember-cli-babel: 7.26.11 - transitivePeerDependencies: - - '@glint/template' - - supports-color - ember-in-element-polyfill@1.0.1: dependencies: debug: 4.4.0(supports-color@8.1.1) @@ -16101,8 +16024,6 @@ snapshots: transitivePeerDependencies: - supports-color - fake-xml-http-request@2.1.2: {} - fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -16988,8 +16909,6 @@ snapshots: infer-owner@1.0.4: {} - inflected@2.1.0: {} - inflection@2.0.1: {} inflection@3.0.2: {} @@ -17967,13 +17886,6 @@ snapshots: minipass@7.1.2: {} - miragejs@0.1.48: - dependencies: - '@miragejs/pretender-node-polyfill': 0.1.2 - inflected: 2.1.0 - lodash: 4.17.21 - pretender: 3.4.7 - mississippi@3.0.0: dependencies: concat-stream: 1.6.2 @@ -18884,11 +18796,6 @@ snapshots: prelude-ls@1.2.1: {} - pretender@3.4.7: - dependencies: - fake-xml-http-request: 2.1.2 - route-recognizer: 0.3.4 - prettier-linter-helpers@1.0.0: dependencies: fast-diff: 1.3.0 diff --git a/tests/helpers/index.js b/tests/helpers/index.js index 3f948e689e0..c9e14ebba6a 100644 --- a/tests/helpers/index.js +++ b/tests/helpers/index.js @@ -1,10 +1,8 @@ import { setupApplicationTest as upstreamSetupApplicationTest } from 'ember-qunit'; import { setupSentryMock } from './sentry'; -import setupMirage from './setup-mirage'; import setupMSW from './setup-msw'; -export { default as setupMirage } from './setup-mirage'; export { setupTest, setupRenderingTest } from 'ember-qunit'; // see http://emberjs.github.io/rfcs/0637-customizable-test-setups.html @@ -15,8 +13,6 @@ export function setupApplicationTest(hooks, options = {}) { if (msw) { setupMSW(hooks); - } else { - setupMirage(hooks); } setupSentryMock(hooks); diff --git a/tests/helpers/setup-mirage.js b/tests/helpers/setup-mirage.js deleted file mode 100644 index 2672edd6e31..00000000000 --- a/tests/helpers/setup-mirage.js +++ /dev/null @@ -1,19 +0,0 @@ -import { setupMirage } from 'ember-cli-mirage/test-support'; -import window from 'ember-window-mock'; -import { setupWindowMock } from 'ember-window-mock/test-support'; - -import { setupFakeTimers } from './fake-timers'; - -export default function (hooks) { - setupMirage(hooks); - setupWindowMock(hooks); - setupFakeTimers(hooks, '2017-11-20T12:00:00'); - - // To have deterministic visual tests, the seed has to be constant - hooks.beforeEach(function () { - this.authenticateAs = user => { - this.server.create('mirage-session', { user }); - window.localStorage.setItem('isLoggedIn', '1'); - }; - }); -} diff --git a/tests/mirage/categories/get-by-id-test.js b/tests/mirage/categories/get-by-id-test.js deleted file mode 100644 index 04f4dfcbac9..00000000000 --- a/tests/mirage/categories/get-by-id-test.js +++ /dev/null @@ -1,57 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/categories/:id', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 404 for unknown categories', async function (assert) { - let response = await fetch('/api/v1/categories/foo'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('returns a category object for known categories', async function (assert) { - this.server.create('category', { - category: 'no-std', - description: 'Crates that are able to function without the Rust standard library.', - }); - - let response = await fetch('/api/v1/categories/no-std'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - category: { - id: 'no-std', - category: 'no-std', - crates_cnt: 0, - created_at: '2010-06-16T21:30:45Z', - description: 'Crates that are able to function without the Rust standard library.', - slug: 'no-std', - }, - }); - }); - - test('calculates `crates_cnt` correctly', async function (assert) { - this.server.create('category', { category: 'cli' }); - this.server.createList('crate', 7, { categoryIds: ['cli'] }); - this.server.create('category', { category: 'not-cli' }); - this.server.createList('crate', 3, { categoryIds: ['not-cli'] }); - - let response = await fetch('/api/v1/categories/cli'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - category: { - category: 'cli', - crates_cnt: 7, - created_at: '2010-06-16T21:30:45Z', - description: 'This is the description for the category called "cli"', - id: 'cli', - slug: 'cli', - }, - }); - }); -}); diff --git a/tests/mirage/categories/list-test.js b/tests/mirage/categories/list-test.js deleted file mode 100644 index d19a6bf057c..00000000000 --- a/tests/mirage/categories/list-test.js +++ /dev/null @@ -1,92 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/categories', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('empty case', async function (assert) { - let response = await fetch('/api/v1/categories'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - categories: [], - meta: { - total: 0, - }, - }); - }); - - test('returns a paginated categories list', async function (assert) { - this.server.create('category', { - category: 'no-std', - description: 'Crates that are able to function without the Rust standard library.', - }); - this.server.createList('category', 2); - - let response = await fetch('/api/v1/categories'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - categories: [ - { - id: 'category-1', - category: 'Category 1', - crates_cnt: 0, - created_at: '2010-06-16T21:30:45Z', - description: 'This is the description for the category called "Category 1"', - slug: 'category-1', - }, - { - id: 'category-2', - category: 'Category 2', - crates_cnt: 0, - created_at: '2010-06-16T21:30:45Z', - description: 'This is the description for the category called "Category 2"', - slug: 'category-2', - }, - { - id: 'no-std', - category: 'no-std', - crates_cnt: 0, - created_at: '2010-06-16T21:30:45Z', - description: 'Crates that are able to function without the Rust standard library.', - slug: 'no-std', - }, - ], - meta: { - total: 3, - }, - }); - }); - - test('never returns more than 10 results', async function (assert) { - this.server.createList('category', 25); - - let response = await fetch('/api/v1/categories'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.strictEqual(responsePayload.categories.length, 10); - assert.strictEqual(responsePayload.meta.total, 25); - }); - - test('supports `page` and `per_page` parameters', async function (assert) { - this.server.createList('category', 25, { - category: i => `cat-${String(i + 1).padStart(2, '0')}`, - }); - - let response = await fetch('/api/v1/categories?page=2&per_page=5'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.strictEqual(responsePayload.categories.length, 5); - assert.deepEqual( - responsePayload.categories.map(it => it.id), - ['cat-06', 'cat-07', 'cat-08', 'cat-09', 'cat-10'], - ); - assert.strictEqual(responsePayload.meta.total, 25); - }); -}); diff --git a/tests/mirage/category-slugs/list-test.js b/tests/mirage/category-slugs/list-test.js deleted file mode 100644 index 573ecb105cc..00000000000 --- a/tests/mirage/category-slugs/list-test.js +++ /dev/null @@ -1,57 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/category_slugs', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('empty case', async function (assert) { - let response = await fetch('/api/v1/category_slugs'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - category_slugs: [], - }); - }); - - test('returns a category slugs list', async function (assert) { - this.server.create('category', { - category: 'no-std', - description: 'Crates that are able to function without the Rust standard library.', - }); - this.server.createList('category', 2); - - let response = await fetch('/api/v1/category_slugs'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - category_slugs: [ - { - description: 'This is the description for the category called "Category 1"', - id: 'category-1', - slug: 'category-1', - }, - { - description: 'This is the description for the category called "Category 2"', - id: 'category-2', - slug: 'category-2', - }, - { - description: 'Crates that are able to function without the Rust standard library.', - id: 'no-std', - slug: 'no-std', - }, - ], - }); - }); - - test('has no pagination', async function (assert) { - this.server.createList('category', 25); - - let response = await fetch('/api/v1/category_slugs'); - assert.strictEqual(response.status, 200); - assert.strictEqual((await response.json()).category_slugs.length, 25); - }); -}); diff --git a/tests/mirage/confirm/put-by-id-test.js b/tests/mirage/confirm/put-by-id-test.js deleted file mode 100644 index 543b6cd0dad..00000000000 --- a/tests/mirage/confirm/put-by-id-test.js +++ /dev/null @@ -1,45 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | PUT /api/v1/confirm/:token', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns `ok: true` for a known token (unauthenticated)', async function (assert) { - let user = this.server.create('user', { emailVerificationToken: 'foo' }); - assert.false(user.emailVerified); - - let response = await fetch('/api/v1/confirm/foo', { method: 'PUT' }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { ok: true }); - - user.reload(); - assert.true(user.emailVerified); - }); - - test('returns `ok: true` for a known token (authenticated)', async function (assert) { - let user = this.server.create('user', { emailVerificationToken: 'foo' }); - assert.false(user.emailVerified); - - this.server.create('mirage-session', { user }); - - let response = await fetch('/api/v1/confirm/foo', { method: 'PUT' }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { ok: true }); - - user.reload(); - assert.true(user.emailVerified); - }); - - test('returns an error for unknown tokens', async function (assert) { - let response = await fetch('/api/v1/confirm/unknown', { method: 'PUT' }); - assert.strictEqual(response.status, 400); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'Email belonging to token not found.' }], - }); - }); -}); diff --git a/tests/mirage/crates/add-owner-test.js b/tests/mirage/crates/add-owner-test.js deleted file mode 100644 index 05463d2710e..00000000000 --- a/tests/mirage/crates/add-owner-test.js +++ /dev/null @@ -1,119 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -const ADD_USER_BODY = JSON.stringify({ owners: ['john-doe'] }); - -module('Mirage | PUT /api/v1/crates/:name/owners', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 403 if unauthenticated', async function (assert) { - let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body: ADD_USER_BODY }); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); - - test('returns 404 for unknown crates', async function (assert) { - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body: ADD_USER_BODY }); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('can add new owner', async function (assert) { - let user = this.server.create('user'); - this.authenticateAs(user); - - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('crate-ownership', { crate, user }); - - let user2 = this.server.create('user'); - - let body = JSON.stringify({ owners: [user2.login] }); - let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - ok: true, - msg: 'user user-2 has been invited to be an owner of crate foo', - }); - - let owners = this.server.schema.crateOwnerships.where({ crateId: crate.id }); - assert.strictEqual(owners.length, 1); - assert.strictEqual(owners.models[0].userId, user.id); - - let invites = this.server.schema.crateOwnerInvitations.where({ crateId: crate.id }); - assert.strictEqual(invites.length, 1); - assert.strictEqual(invites.models[0].inviterId, user.id); - assert.strictEqual(invites.models[0].inviteeId, user2.id); - }); - - test('can add team owner', async function (assert) { - let user = this.server.create('user'); - this.authenticateAs(user); - - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('crate-ownership', { crate, user }); - - let team = this.server.create('team'); - - let body = JSON.stringify({ owners: [team.login] }); - let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - ok: true, - msg: 'team github:rust-lang:team-1 has been added as an owner of crate foo', - }); - - let owners = this.server.schema.crateOwnerships.where({ crateId: crate.id }); - assert.strictEqual(owners.length, 2); - assert.strictEqual(owners.models[0].userId, user.id); - assert.strictEqual(owners.models[0].teamId, null); - assert.strictEqual(owners.models[1].userId, null); - assert.strictEqual(owners.models[1].teamId, user.id); - - let invites = this.server.schema.crateOwnerInvitations.where({ crateId: crate.id }); - assert.strictEqual(invites.length, 0); - }); - - test('can add multiple owners', async function (assert) { - let user = this.server.create('user'); - this.authenticateAs(user); - - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('crate-ownership', { crate, user }); - - let team = this.server.create('team'); - let user2 = this.server.create('user'); - let user3 = this.server.create('user'); - - let body = JSON.stringify({ owners: [user2.login, team.login, user3.login] }); - let response = await fetch('/api/v1/crates/foo/owners', { method: 'PUT', body }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - ok: true, - msg: 'user user-2 has been invited to be an owner of crate foo,team github:rust-lang:team-1 has been added as an owner of crate foo,user user-3 has been invited to be an owner of crate foo', - }); - - let owners = this.server.schema.crateOwnerships.where({ crateId: crate.id }); - assert.strictEqual(owners.length, 2); - assert.strictEqual(owners.models[0].userId, user.id); - assert.strictEqual(owners.models[0].teamId, null); - assert.strictEqual(owners.models[1].userId, null); - assert.strictEqual(owners.models[1].teamId, user.id); - - let invites = this.server.schema.crateOwnerInvitations.where({ crateId: crate.id }); - assert.strictEqual(invites.length, 2); - assert.strictEqual(invites.models[0].inviterId, user.id); - assert.strictEqual(invites.models[0].inviteeId, user2.id); - assert.strictEqual(invites.models[1].inviterId, user.id); - assert.strictEqual(invites.models[1].inviteeId, user3.id); - }); -}); diff --git a/tests/mirage/crates/delete-test.js b/tests/mirage/crates/delete-test.js deleted file mode 100644 index 20a56924f32..00000000000 --- a/tests/mirage/crates/delete-test.js +++ /dev/null @@ -1,42 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | DELETE /api/v1/crates/:name', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 403 if unauthenticated', async function (assert) { - let response = await fetch('/api/v1/crates/foo', { method: 'DELETE' }); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); - - test('returns 404 for unknown crates', async function (assert) { - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/foo', { method: 'DELETE' }); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'crate `foo` does not exist' }] }); - }); - - test('deletes crates', async function (assert) { - let user = this.server.create('user'); - this.authenticateAs(user); - - let crate = this.server.create('crate', { name: 'foo' }); - this.server.create('crate-ownership', { crate, user }); - - let response = await fetch('/api/v1/crates/foo', { method: 'DELETE' }); - assert.strictEqual(response.status, 204); - assert.deepEqual(await response.text(), ''); - - assert.strictEqual(this.server.schema.crates.findBy({ name: 'foo' }), null); - }); -}); diff --git a/tests/mirage/crates/downloads-test.js b/tests/mirage/crates/downloads-test.js deleted file mode 100644 index be9f46ef23e..00000000000 --- a/tests/mirage/crates/downloads-test.js +++ /dev/null @@ -1,63 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/crates/:id/downloads', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 404 for unknown crates', async function (assert) { - let response = await fetch('/api/v1/crates/foo/downloads'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('empty case', async function (assert) { - this.server.create('crate', { name: 'rand' }); - - let response = await fetch('/api/v1/crates/rand/downloads'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - version_downloads: [], - meta: { - extra_downloads: [], - }, - }); - }); - - test('returns a list of version downloads belonging to the specified crate version', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - let versions = this.server.createList('version', 2, { crate }); - this.server.create('version-download', { version: versions[0], date: '2020-01-13' }); - this.server.create('version-download', { version: versions[1], date: '2020-01-14' }); - this.server.create('version-download', { version: versions[1], date: '2020-01-15' }); - - let response = await fetch('/api/v1/crates/rand/downloads'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - version_downloads: [ - { - date: '2020-01-13', - downloads: 9380, - version: '1', - }, - { - date: '2020-01-14', - downloads: 16_415, - version: '2', - }, - { - date: '2020-01-15', - downloads: 23_450, - version: '2', - }, - ], - meta: { - extra_downloads: [], - }, - }); - }); -}); diff --git a/tests/mirage/crates/follow/delete-test.js b/tests/mirage/crates/follow/delete-test.js deleted file mode 100644 index 13e7dc02bc4..00000000000 --- a/tests/mirage/crates/follow/delete-test.js +++ /dev/null @@ -1,44 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | DELETE /api/v1/crates/:crateId/follow', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 403 if unauthenticated', async function (assert) { - let response = await fetch('/api/v1/crates/foo/follow', { method: 'DELETE' }); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); - - test('returns 404 for unknown crates', async function (assert) { - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/foo/follow', { method: 'DELETE' }); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('makes the authenticated user unfollow the crate', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - - let user = this.server.create('user', { followedCrates: [crate] }); - this.authenticateAs(user); - - assert.deepEqual(user.followedCrateIds, [crate.id]); - - let response = await fetch('/api/v1/crates/rand/follow', { method: 'DELETE' }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { ok: true }); - - user.reload(); - assert.deepEqual(user.followedCrateIds, []); - }); -}); diff --git a/tests/mirage/crates/follow/get-test.js b/tests/mirage/crates/follow/get-test.js deleted file mode 100644 index c0b4cb15a27..00000000000 --- a/tests/mirage/crates/follow/get-test.js +++ /dev/null @@ -1,50 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/crates/:crateId/following', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 403 if unauthenticated', async function (assert) { - let response = await fetch('/api/v1/crates/foo/following'); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); - - test('returns 404 for unknown crates', async function (assert) { - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/foo/following'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('returns true if the authenticated user follows the crate', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - - let user = this.server.create('user', { followedCrates: [crate] }); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/rand/following'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { following: true }); - }); - - test('returns false if the authenticated user is not following the crate', async function (assert) { - this.server.create('crate', { name: 'rand' }); - - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/rand/following'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { following: false }); - }); -}); diff --git a/tests/mirage/crates/follow/put-test.js b/tests/mirage/crates/follow/put-test.js deleted file mode 100644 index f13af61b067..00000000000 --- a/tests/mirage/crates/follow/put-test.js +++ /dev/null @@ -1,44 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | PUT /api/v1/crates/:crateId/follow', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 403 if unauthenticated', async function (assert) { - let response = await fetch('/api/v1/crates/foo/follow', { method: 'PUT' }); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); - - test('returns 404 for unknown crates', async function (assert) { - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/foo/follow', { method: 'PUT' }); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('makes the authenticated user follow the crate', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - - let user = this.server.create('user'); - this.authenticateAs(user); - - assert.deepEqual(user.followedCrateIds, []); - - let response = await fetch('/api/v1/crates/rand/follow', { method: 'PUT' }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { ok: true }); - - user.reload(); - assert.deepEqual(user.followedCrateIds, [crate.id]); - }); -}); diff --git a/tests/mirage/crates/get-by-id-test.js b/tests/mirage/crates/get-by-id-test.js deleted file mode 100644 index 323532474f1..00000000000 --- a/tests/mirage/crates/get-by-id-test.js +++ /dev/null @@ -1,330 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/crates/:id', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 404 for unknown crates', async function (assert) { - let response = await fetch('/api/v1/crates/foo'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('returns a crate object for known crates', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { crate, num: '1.0.0-beta.1' }); - - let response = await fetch('/api/v1/crates/rand'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - categories: [], - crate: { - badges: [], - categories: [], - created_at: '2010-06-16T21:30:45Z', - default_version: '1.0.0-beta.1', - description: 'This is the description for the crate called "rand"', - documentation: null, - downloads: 0, - homepage: null, - id: 'rand', - keywords: [], - links: { - owner_team: '/api/v1/crates/rand/owner_team', - owner_user: '/api/v1/crates/rand/owner_user', - reverse_dependencies: '/api/v1/crates/rand/reverse_dependencies', - version_downloads: '/api/v1/crates/rand/downloads', - versions: '/api/v1/crates/rand/versions', - }, - max_version: '1.0.0-beta.1', - max_stable_version: null, - name: 'rand', - newest_version: '1.0.0-beta.1', - repository: null, - updated_at: '2017-02-24T12:34:56Z', - versions: ['1'], - yanked: false, - }, - keywords: [], - versions: [ - { - id: '1', - crate: 'rand', - crate_size: 0, - created_at: '2010-06-16T21:30:45Z', - dl_path: '/api/v1/crates/rand/1.0.0-beta.1/download', - downloads: 0, - license: 'MIT/Apache-2.0', - links: { - dependencies: '/api/v1/crates/rand/1.0.0-beta.1/dependencies', - version_downloads: '/api/v1/crates/rand/1.0.0-beta.1/downloads', - }, - num: '1.0.0-beta.1', - published_by: null, - readme_path: '/api/v1/crates/rand/1.0.0-beta.1/readme', - rust_version: null, - updated_at: '2017-02-24T12:34:56Z', - yanked: false, - yank_message: null, - }, - ], - }); - }); - - test('works for non-canonical names', async function (assert) { - let crate = this.server.create('crate', { name: 'foo-bar' }); - this.server.create('version', { crate, num: '1.0.0-beta.1' }); - - let response = await fetch('/api/v1/crates/foo_bar'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - categories: [], - crate: { - badges: [], - categories: [], - created_at: '2010-06-16T21:30:45Z', - default_version: '1.0.0-beta.1', - description: 'This is the description for the crate called "foo-bar"', - documentation: null, - downloads: 0, - homepage: null, - id: 'foo-bar', - keywords: [], - links: { - owner_team: '/api/v1/crates/foo-bar/owner_team', - owner_user: '/api/v1/crates/foo-bar/owner_user', - reverse_dependencies: '/api/v1/crates/foo-bar/reverse_dependencies', - version_downloads: '/api/v1/crates/foo-bar/downloads', - versions: '/api/v1/crates/foo-bar/versions', - }, - max_version: '1.0.0-beta.1', - max_stable_version: null, - name: 'foo-bar', - newest_version: '1.0.0-beta.1', - repository: null, - updated_at: '2017-02-24T12:34:56Z', - versions: ['1'], - yanked: false, - }, - keywords: [], - versions: [ - { - id: '1', - crate: 'foo-bar', - crate_size: 0, - created_at: '2010-06-16T21:30:45Z', - dl_path: '/api/v1/crates/foo-bar/1.0.0-beta.1/download', - downloads: 0, - license: 'MIT/Apache-2.0', - links: { - dependencies: '/api/v1/crates/foo-bar/1.0.0-beta.1/dependencies', - version_downloads: '/api/v1/crates/foo-bar/1.0.0-beta.1/downloads', - }, - num: '1.0.0-beta.1', - published_by: null, - readme_path: '/api/v1/crates/foo-bar/1.0.0-beta.1/readme', - rust_version: null, - updated_at: '2017-02-24T12:34:56Z', - yanked: false, - yank_message: null, - }, - ], - }); - }); - - test('includes related versions', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.1.0' }); - this.server.create('version', { crate, num: '1.2.0' }); - - let response = await fetch('/api/v1/crates/rand'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.deepEqual(responsePayload.crate.versions, ['1', '2', '3']); - assert.deepEqual(responsePayload.versions, [ - { - id: '3', - crate: 'rand', - crate_size: 325_926, - created_at: '2010-06-16T21:30:45Z', - dl_path: '/api/v1/crates/rand/1.2.0/download', - downloads: 7404, - license: 'Apache-2.0', - links: { - dependencies: '/api/v1/crates/rand/1.2.0/dependencies', - version_downloads: '/api/v1/crates/rand/1.2.0/downloads', - }, - num: '1.2.0', - published_by: null, - readme_path: '/api/v1/crates/rand/1.2.0/readme', - rust_version: null, - updated_at: '2017-02-24T12:34:56Z', - yanked: false, - yank_message: null, - }, - { - id: '2', - crate: 'rand', - crate_size: 162_963, - created_at: '2010-06-16T21:30:45Z', - dl_path: '/api/v1/crates/rand/1.1.0/download', - downloads: 3702, - license: 'MIT', - links: { - dependencies: '/api/v1/crates/rand/1.1.0/dependencies', - version_downloads: '/api/v1/crates/rand/1.1.0/downloads', - }, - num: '1.1.0', - published_by: null, - readme_path: '/api/v1/crates/rand/1.1.0/readme', - rust_version: null, - updated_at: '2017-02-24T12:34:56Z', - yanked: false, - yank_message: null, - }, - { - id: '1', - crate: 'rand', - crate_size: 0, - created_at: '2010-06-16T21:30:45Z', - dl_path: '/api/v1/crates/rand/1.0.0/download', - downloads: 0, - license: 'MIT/Apache-2.0', - links: { - dependencies: '/api/v1/crates/rand/1.0.0/dependencies', - version_downloads: '/api/v1/crates/rand/1.0.0/downloads', - }, - num: '1.0.0', - published_by: null, - readme_path: '/api/v1/crates/rand/1.0.0/readme', - rust_version: null, - updated_at: '2017-02-24T12:34:56Z', - yanked: false, - yank_message: null, - }, - ]); - }); - - test('includes related categories', async function (assert) { - this.server.create('category', { category: 'no-std' }); - this.server.create('category', { category: 'cli' }); - let crate = this.server.create('crate', { name: 'rand', categoryIds: ['no-std'] }); - this.server.create('version', { crate }); - - let response = await fetch('/api/v1/crates/rand'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.deepEqual(responsePayload.crate.categories, ['no-std']); - assert.deepEqual(responsePayload.categories, [ - { - id: 'no-std', - category: 'no-std', - crates_cnt: 1, - created_at: '2010-06-16T21:30:45Z', - description: 'This is the description for the category called "no-std"', - slug: 'no-std', - }, - ]); - }); - - test('includes related keywords', async function (assert) { - this.server.create('keyword', { keyword: 'no-std' }); - this.server.create('keyword', { keyword: 'cli' }); - let crate = this.server.create('crate', { name: 'rand', keywordIds: ['no-std'] }); - this.server.create('version', { crate }); - - let response = await fetch('/api/v1/crates/rand'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.deepEqual(responsePayload.crate.keywords, ['no-std']); - assert.deepEqual(responsePayload.keywords, [ - { - crates_cnt: 1, - id: 'no-std', - keyword: 'no-std', - }, - ]); - }); - - test('without versions included', async function (assert) { - this.server.create('category', { category: 'no-std' }); - this.server.create('category', { category: 'cli' }); - this.server.create('keyword', { keyword: 'no-std' }); - this.server.create('keyword', { keyword: 'cli' }); - let crate = this.server.create('crate', { name: 'rand', categoryIds: ['no-std'], keywordIds: ['no-std'] }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.1.0' }); - this.server.create('version', { crate, num: '1.2.0' }); - - let req = await fetch('/api/v1/crates/rand'); - let expected = await req.json(); - - let response = await fetch('/api/v1/crates/rand?include=keywords,categories'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.deepEqual(responsePayload, { - ...expected, - crate: { - ...expected.crate, - max_version: '0.0.0', - newest_version: '0.0.0', - max_stable_version: null, - versions: null, - }, - versions: null, - }); - }); - test('includes default_version', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.1.0' }); - this.server.create('version', { crate, num: '1.2.0' }); - - let req = await fetch('/api/v1/crates/rand'); - let expected = await req.json(); - - let response = await fetch('/api/v1/crates/rand?include=default_version'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - let default_version = expected.versions.find(it => it.num === responsePayload.crate.default_version); - assert.deepEqual(responsePayload, { - ...expected, - crate: { - ...expected.crate, - categories: null, - keywords: null, - max_version: '0.0.0', - newest_version: '0.0.0', - max_stable_version: null, - versions: null, - }, - categories: null, - keywords: null, - versions: [default_version], - }); - - let resp_both = await fetch('/api/v1/crates/rand?include=versions,default_version'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await resp_both.json(), { - ...expected, - crate: { - ...expected.crate, - categories: null, - keywords: null, - }, - categories: null, - keywords: null, - }); - }); -}); diff --git a/tests/mirage/crates/list-test.js b/tests/mirage/crates/list-test.js deleted file mode 100644 index 23e4f630432..00000000000 --- a/tests/mirage/crates/list-test.js +++ /dev/null @@ -1,228 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/crates', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('empty case', async function (assert) { - let response = await fetch('/api/v1/crates'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - crates: [], - meta: { - total: 0, - }, - }); - }); - - test('returns a paginated crates list', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { - crate, - created_at: '2020-11-06T12:34:56Z', - num: '1.0.0', - updated_at: '2020-11-06T12:34:56Z', - }); - this.server.create('version', { - crate, - created_at: '2020-12-25T12:34:56Z', - num: '2.0.0-beta.1', - updated_at: '2020-12-25T12:34:56Z', - }); - - let response = await fetch('/api/v1/crates'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - crates: [ - { - id: 'rand', - badges: [], - categories: [], - created_at: '2010-06-16T21:30:45Z', - default_version: '1.0.0', - description: 'This is the description for the crate called "rand"', - documentation: null, - downloads: 0, - homepage: null, - keywords: [], - links: { - owner_team: '/api/v1/crates/rand/owner_team', - owner_user: '/api/v1/crates/rand/owner_user', - reverse_dependencies: '/api/v1/crates/rand/reverse_dependencies', - version_downloads: '/api/v1/crates/rand/downloads', - versions: '/api/v1/crates/rand/versions', - }, - max_version: '2.0.0-beta.1', - max_stable_version: '1.0.0', - name: 'rand', - newest_version: '2.0.0-beta.1', - repository: null, - updated_at: '2017-02-24T12:34:56Z', - versions: ['1', '2'], - yanked: false, - }, - ], - meta: { - total: 1, - }, - }); - }); - - test('never returns more than 10 results', async function (assert) { - let crates = this.server.createList('crate', 25); - this.server.createList('version', crates.length, { crate: i => crates[i] }); - - let response = await fetch('/api/v1/crates'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.strictEqual(responsePayload.crates.length, 10); - assert.strictEqual(responsePayload.meta.total, 25); - }); - - test('supports `page` and `per_page` parameters', async function (assert) { - let crates = this.server.createList('crate', 25, { - name: i => `crate-${String(i + 1).padStart(2, '0')}`, - }); - this.server.createList('version', crates.length, { crate: i => crates[i] }); - - let response = await fetch('/api/v1/crates?page=2&per_page=5'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.strictEqual(responsePayload.crates.length, 5); - assert.deepEqual( - responsePayload.crates.map(it => it.id), - ['crate-06', 'crate-07', 'crate-08', 'crate-09', 'crate-10'], - ); - assert.strictEqual(responsePayload.meta.total, 25); - }); - - test('supports a `letter` parameter', async function (assert) { - let foo = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate: foo }); - let bar = this.server.create('crate', { name: 'bar' }); - this.server.create('version', { crate: bar }); - let baz = this.server.create('crate', { name: 'BAZ' }); - this.server.create('version', { crate: baz }); - - let response = await fetch('/api/v1/crates?letter=b'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.strictEqual(responsePayload.crates.length, 2); - assert.deepEqual( - responsePayload.crates.map(it => it.id), - ['bar', 'BAZ'], - ); - assert.strictEqual(responsePayload.meta.total, 2); - }); - - test('supports a `q` parameter', async function (assert) { - let crate1 = this.server.create('crate', { name: '123456' }); - this.server.create('version', { crate: crate1 }); - let crate2 = this.server.create('crate', { name: '00123' }); - this.server.create('version', { crate: crate2 }); - let crate3 = this.server.create('crate', { name: '87654' }); - this.server.create('version', { crate: crate3 }); - - let response = await fetch('/api/v1/crates?q=123'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.strictEqual(responsePayload.crates.length, 2); - assert.deepEqual( - responsePayload.crates.map(it => it.id), - ['123456', '00123'], - ); - assert.strictEqual(responsePayload.meta.total, 2); - }); - - test('supports a `user_id` parameter', async function (assert) { - let user1 = this.server.create('user'); - let user2 = this.server.create('user'); - - let foo = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate: foo }); - let bar = this.server.create('crate', { name: 'bar' }); - this.server.create('crate-ownership', { crate: bar, user: user1 }); - this.server.create('version', { crate: bar }); - let baz = this.server.create('crate', { name: 'baz' }); - this.server.create('crate-ownership', { crate: baz, user: user2 }); - this.server.create('version', { crate: baz }); - - let response = await fetch(`/api/v1/crates?user_id=${user1.id}`); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.strictEqual(responsePayload.crates.length, 1); - assert.strictEqual(responsePayload.crates[0].id, 'bar'); - assert.strictEqual(responsePayload.meta.total, 1); - }); - - test('supports a `team_id` parameter', async function (assert) { - let team1 = this.server.create('team'); - let team2 = this.server.create('team'); - - let foo = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate: foo }); - let bar = this.server.create('crate', { name: 'bar' }); - this.server.create('crate-ownership', { crate: bar, team: team1 }); - this.server.create('version', { crate: bar }); - let baz = this.server.create('crate', { name: 'baz' }); - this.server.create('crate-ownership', { crate: baz, team: team2 }); - this.server.create('version', { crate: baz }); - - let response = await fetch(`/api/v1/crates?team_id=${team1.id}`); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.strictEqual(responsePayload.crates.length, 1); - assert.strictEqual(responsePayload.crates[0].id, 'bar'); - assert.strictEqual(responsePayload.meta.total, 1); - }); - - test('supports a `following` parameter', async function (assert) { - let foo = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate: foo }); - let bar = this.server.create('crate', { name: 'bar' }); - this.server.create('version', { crate: bar }); - - let user = this.server.create('user', { followedCrates: [bar] }); - this.authenticateAs(user); - - let response = await fetch(`/api/v1/crates?following=1`); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.strictEqual(responsePayload.crates.length, 1); - assert.strictEqual(responsePayload.crates[0].id, 'bar'); - assert.strictEqual(responsePayload.meta.total, 1); - }); - - test('supports multiple `ids[]` parameters', async function (assert) { - let foo = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate: foo }); - let bar = this.server.create('crate', { name: 'bar' }); - this.server.create('version', { crate: bar }); - let baz = this.server.create('crate', { name: 'baz' }); - this.server.create('version', { crate: baz }); - let other = this.server.create('crate', { name: 'other' }); - this.server.create('version', { crate: other }); - - let response = await fetch(`/api/v1/crates?ids[]=foo&ids[]=bar&ids[]=baz&ids[]=baz&ids[]=unknown`); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.strictEqual(responsePayload.crates.length, 3); - assert.strictEqual(responsePayload.crates[0].id, 'foo'); - assert.strictEqual(responsePayload.crates[1].id, 'bar'); - assert.strictEqual(responsePayload.crates[2].id, 'baz'); - assert.strictEqual(responsePayload.meta.total, 3); - }); -}); diff --git a/tests/mirage/crates/owner-team-test.js b/tests/mirage/crates/owner-team-test.js deleted file mode 100644 index 25e9a8e10fe..00000000000 --- a/tests/mirage/crates/owner-team-test.js +++ /dev/null @@ -1,48 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/crates/:id/owner_team', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 404 for unknown crates', async function (assert) { - let response = await fetch('/api/v1/crates/foo/owner_team'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('empty case', async function (assert) { - this.server.create('crate', { name: 'rand' }); - - let response = await fetch('/api/v1/crates/rand/owner_team'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - teams: [], - }); - }); - - test('returns the list of teams that own the specified crate', async function (assert) { - let team = this.server.create('team', { name: 'maintainers' }); - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('crate-ownership', { crate, team }); - - let response = await fetch('/api/v1/crates/rand/owner_team'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - teams: [ - { - id: 1, - avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', - kind: 'team', - login: 'github:rust-lang:maintainers', - name: 'maintainers', - url: 'https://github.com/rust-lang', - }, - ], - }); - }); -}); diff --git a/tests/mirage/crates/owner-user-test.js b/tests/mirage/crates/owner-user-test.js deleted file mode 100644 index 43d3b4af95a..00000000000 --- a/tests/mirage/crates/owner-user-test.js +++ /dev/null @@ -1,48 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/crates/:id/owner_user', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 404 for unknown crates', async function (assert) { - let response = await fetch('/api/v1/crates/foo/owner_user'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('empty case', async function (assert) { - this.server.create('crate', { name: 'rand' }); - - let response = await fetch('/api/v1/crates/rand/owner_user'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - users: [], - }); - }); - - test('returns the list of users that own the specified crate', async function (assert) { - let user = this.server.create('user', { name: 'John Doe' }); - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('crate-ownership', { crate, user }); - - let response = await fetch('/api/v1/crates/rand/owner_user'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - users: [ - { - id: 1, - avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', - kind: 'user', - login: 'john-doe', - name: 'John Doe', - url: 'https://github.com/john-doe', - }, - ], - }); - }); -}); diff --git a/tests/mirage/crates/reverse-dependencies-test.js b/tests/mirage/crates/reverse-dependencies-test.js deleted file mode 100644 index 4976cf6cecd..00000000000 --- a/tests/mirage/crates/reverse-dependencies-test.js +++ /dev/null @@ -1,170 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/crates/:id/reverse_dependencies', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 404 for unknown crates', async function (assert) { - let response = await fetch('/api/v1/crates/foo/reverse_dependencies'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('empty case', async function (assert) { - this.server.create('crate', { name: 'rand' }); - - let response = await fetch('/api/v1/crates/rand/reverse_dependencies'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - dependencies: [], - versions: [], - meta: { - total: 0, - }, - }); - }); - - test('returns a paginated list of crate versions depending to the specified crate', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - - this.server.create('dependency', { - crate, - version: this.server.create('version', { - crate: this.server.create('crate', { name: 'bar' }), - }), - }); - - this.server.create('dependency', { - crate, - version: this.server.create('version', { - crate: this.server.create('crate', { name: 'baz' }), - }), - }); - - let response = await fetch('/api/v1/crates/foo/reverse_dependencies'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - dependencies: [ - { - id: '1', - crate_id: 'foo', - default_features: false, - features: [], - kind: 'dev', - optional: true, - req: '^0.1.0', - target: null, - version_id: '1', - }, - { - id: '2', - crate_id: 'foo', - default_features: false, - features: [], - kind: 'normal', - optional: true, - req: '^2.1.3', - target: null, - version_id: '2', - }, - ], - versions: [ - { - id: '1', - crate: 'bar', - crate_size: 0, - created_at: '2010-06-16T21:30:45Z', - dl_path: '/api/v1/crates/bar/1.0.0/download', - downloads: 0, - license: 'MIT/Apache-2.0', - links: { - dependencies: '/api/v1/crates/bar/1.0.0/dependencies', - version_downloads: '/api/v1/crates/bar/1.0.0/downloads', - }, - num: '1.0.0', - published_by: null, - readme_path: '/api/v1/crates/bar/1.0.0/readme', - rust_version: null, - updated_at: '2017-02-24T12:34:56Z', - yanked: false, - yank_message: null, - }, - { - id: '2', - crate: 'baz', - crate_size: 162_963, - created_at: '2010-06-16T21:30:45Z', - dl_path: '/api/v1/crates/baz/1.0.1/download', - downloads: 3702, - license: 'MIT', - links: { - dependencies: '/api/v1/crates/baz/1.0.1/dependencies', - version_downloads: '/api/v1/crates/baz/1.0.1/downloads', - }, - num: '1.0.1', - published_by: null, - readme_path: '/api/v1/crates/baz/1.0.1/readme', - rust_version: null, - updated_at: '2017-02-24T12:34:56Z', - yanked: false, - yank_message: null, - }, - ], - meta: { - total: 2, - }, - }); - }); - - test('never returns more than 10 results', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - - this.server.createList('dependency', 25, { - crate, - version: () => - this.server.create('version', { - crate: () => this.server.create('crate', { name: 'bar' }), - }), - }); - - let response = await fetch('/api/v1/crates/foo/reverse_dependencies'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.strictEqual(responsePayload.dependencies.length, 10); - assert.strictEqual(responsePayload.versions.length, 10); - assert.strictEqual(responsePayload.meta.total, 25); - }); - - test('supports `page` and `per_page` parameters', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - - let crates = this.server.createList('crate', 25, { - name: i => `crate-${String(i + 1).padStart(2, '0')}`, - }); - let versions = this.server.createList('version', crates.length, { - crate: i => crates[i], - }); - this.server.createList('dependency', versions.length, { - crate, - versionId: i => versions[i].id, - }); - - let response = await fetch('/api/v1/crates/foo/reverse_dependencies?page=2&per_page=5'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.strictEqual(responsePayload.dependencies.length, 5); - assert.deepEqual( - responsePayload.versions.map(it => it.crate), - // offset by one because we created the `foo` crate first - ['crate-07', 'crate-08', 'crate-09', 'crate-10', 'crate-11'], - ); - assert.strictEqual(responsePayload.meta.total, 25); - }); -}); diff --git a/tests/mirage/crates/versions/authors-test.js b/tests/mirage/crates/versions/authors-test.js deleted file mode 100644 index 8f8bbab45bb..00000000000 --- a/tests/mirage/crates/versions/authors-test.js +++ /dev/null @@ -1,53 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/crates/:name/:version/authors', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 404 for unknown crates', async function (assert) { - let response = await fetch('/api/v1/crates/foo/1.0.0/authors'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('returns 404 for unknown versions', async function (assert) { - this.server.create('crate', { name: 'rand' }); - - let response = await fetch('/api/v1/crates/rand/1.0.0/authors'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'crate `rand` does not have a version `1.0.0`' }] }); - }); - - test('empty case', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { crate, num: '1.0.0' }); - - let response = await fetch('/api/v1/crates/rand/1.0.0/authors'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - meta: { - names: [], - }, - users: [], - }); - }); - - test('returns a list of authors belonging to the specified crate version', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { crate, num: '1.0.0' }); - - let response = await fetch('/api/v1/crates/rand/1.0.0/authors'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - meta: { - names: [], - }, - users: [], - }); - }); -}); diff --git a/tests/mirage/crates/versions/dependencies-test.js b/tests/mirage/crates/versions/dependencies-test.js deleted file mode 100644 index 823663ee2ea..00000000000 --- a/tests/mirage/crates/versions/dependencies-test.js +++ /dev/null @@ -1,88 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/crates/:name/:version/dependencies', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 404 for unknown crates', async function (assert) { - let response = await fetch('/api/v1/crates/foo/1.0.0/dependencies'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('returns 404 for unknown versions', async function (assert) { - this.server.create('crate', { name: 'rand' }); - - let response = await fetch('/api/v1/crates/rand/1.0.0/dependencies'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'crate `rand` does not have a version `1.0.0`' }] }); - }); - - test('empty case', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { crate, num: '1.0.0' }); - - let response = await fetch('/api/v1/crates/rand/1.0.0/dependencies'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - dependencies: [], - }); - }); - - test('returns a list of dependencies belonging to the specified crate version', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - let version = this.server.create('version', { crate, num: '1.0.0' }); - - let foo = this.server.create('crate', { name: 'foo' }); - this.server.create('dependency', { crate: foo, version }); - let bar = this.server.create('crate', { name: 'bar' }); - this.server.create('dependency', { crate: bar, version }); - let baz = this.server.create('crate', { name: 'baz' }); - this.server.create('dependency', { crate: baz, version }); - - let response = await fetch('/api/v1/crates/rand/1.0.0/dependencies'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - dependencies: [ - { - id: '1', - crate_id: 'foo', - default_features: false, - features: [], - kind: 'dev', - optional: true, - req: '^0.1.0', - target: null, - version_id: '1', - }, - { - id: '2', - crate_id: 'bar', - default_features: false, - features: [], - kind: 'normal', - optional: true, - req: '^2.1.3', - target: null, - version_id: '1', - }, - { - id: '3', - crate_id: 'baz', - default_features: false, - features: [], - kind: 'normal', - optional: true, - req: '0.3.7', - target: null, - version_id: '1', - }, - ], - }); - }); -}); diff --git a/tests/mirage/crates/versions/downloads-test.js b/tests/mirage/crates/versions/downloads-test.js deleted file mode 100644 index 59ea3125726..00000000000 --- a/tests/mirage/crates/versions/downloads-test.js +++ /dev/null @@ -1,66 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/crates/:name/:version/downloads', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 404 for unknown crates', async function (assert) { - let response = await fetch('/api/v1/crates/foo/1.0.0/downloads'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('returns 404 for unknown versions', async function (assert) { - this.server.create('crate', { name: 'rand' }); - - let response = await fetch('/api/v1/crates/rand/1.0.0/downloads'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'crate `rand` does not have a version `1.0.0`' }] }); - }); - - test('empty case', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { crate, num: '1.0.0' }); - - let response = await fetch('/api/v1/crates/rand/1.0.0/downloads'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - version_downloads: [], - }); - }); - - test('returns a list of version downloads belonging to the specified crate version', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - let version = this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version-download', { version, date: '2020-01-13' }); - this.server.create('version-download', { version, date: '2020-01-14' }); - this.server.create('version-download', { version, date: '2020-01-15' }); - - let response = await fetch('/api/v1/crates/rand/1.0.0/downloads'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - version_downloads: [ - { - date: '2020-01-13', - downloads: 9380, - version: '1', - }, - { - date: '2020-01-14', - downloads: 16_415, - version: '1', - }, - { - date: '2020-01-15', - downloads: 23_450, - version: '1', - }, - ], - }); - }); -}); diff --git a/tests/mirage/crates/versions/get-by-num-test.js b/tests/mirage/crates/versions/get-by-num-test.js deleted file mode 100644 index 27496883322..00000000000 --- a/tests/mirage/crates/versions/get-by-num-test.js +++ /dev/null @@ -1,57 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/crates/:name/:version', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 404 for unknown crates', async function (assert) { - let response = await fetch('/api/v1/crates/foo/1.0.0-beta.1'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('returns 404 for unknown versions', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { crate, num: '1.0.0-alpha.1' }); - let response = await fetch('/api/v1/crates/rand/1.0.0-beta.1'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'crate `rand` does not have a version `1.0.0-beta.1`' }], - }); - }); - - test('returns a version object for known version', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { crate, num: '1.0.0-beta.1' }); - - let response = await fetch('/api/v1/crates/rand/1.0.0-beta.1'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - version: { - crate: 'rand', - crate_size: 0, - created_at: '2010-06-16T21:30:45Z', - dl_path: '/api/v1/crates/rand/1.0.0-beta.1/download', - downloads: 0, - id: '1', - license: 'MIT/Apache-2.0', - links: { - dependencies: '/api/v1/crates/rand/1.0.0-beta.1/dependencies', - version_downloads: '/api/v1/crates/rand/1.0.0-beta.1/downloads', - }, - num: '1.0.0-beta.1', - published_by: null, - readme_path: '/api/v1/crates/rand/1.0.0-beta.1/readme', - rust_version: null, - updated_at: '2017-02-24T12:34:56Z', - yank_message: null, - yanked: false, - }, - }); - }); -}); diff --git a/tests/mirage/crates/versions/list-test.js b/tests/mirage/crates/versions/list-test.js deleted file mode 100644 index 42fe9fbd90b..00000000000 --- a/tests/mirage/crates/versions/list-test.js +++ /dev/null @@ -1,155 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/crates/:name/versions', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 404 for unknown crates', async function (assert) { - let response = await fetch('/api/v1/crates/foo/versions'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('empty case', async function (assert) { - this.server.create('crate', { name: 'rand' }); - - let response = await fetch('/api/v1/crates/rand/versions'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - versions: [], - meta: { total: 0, next_page: null }, - }); - }); - - test('returns all versions belonging to the specified crate', async function (assert) { - let user = this.server.create('user'); - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.1.0', publishedBy: user }); - this.server.create('version', { crate, num: '1.2.0', rust_version: '1.69' }); - - let response = await fetch('/api/v1/crates/rand/versions'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - versions: [ - { - id: '1', - crate: 'rand', - crate_size: 0, - created_at: '2010-06-16T21:30:45Z', - dl_path: '/api/v1/crates/rand/1.0.0/download', - downloads: 0, - license: 'MIT/Apache-2.0', - links: { - dependencies: '/api/v1/crates/rand/1.0.0/dependencies', - version_downloads: '/api/v1/crates/rand/1.0.0/downloads', - }, - num: '1.0.0', - published_by: null, - readme_path: '/api/v1/crates/rand/1.0.0/readme', - rust_version: null, - updated_at: '2017-02-24T12:34:56Z', - yanked: false, - yank_message: null, - }, - { - id: '2', - crate: 'rand', - crate_size: 162_963, - created_at: '2010-06-16T21:30:45Z', - dl_path: '/api/v1/crates/rand/1.1.0/download', - downloads: 3702, - license: 'MIT', - links: { - dependencies: '/api/v1/crates/rand/1.1.0/dependencies', - version_downloads: '/api/v1/crates/rand/1.1.0/downloads', - }, - num: '1.1.0', - published_by: { - id: 1, - avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', - login: 'user-1', - name: 'User 1', - url: 'https://github.com/user-1', - }, - readme_path: '/api/v1/crates/rand/1.1.0/readme', - rust_version: null, - updated_at: '2017-02-24T12:34:56Z', - yanked: false, - yank_message: null, - }, - { - id: '3', - crate: 'rand', - crate_size: 325_926, - created_at: '2010-06-16T21:30:45Z', - dl_path: '/api/v1/crates/rand/1.2.0/download', - downloads: 7404, - license: 'Apache-2.0', - links: { - dependencies: '/api/v1/crates/rand/1.2.0/dependencies', - version_downloads: '/api/v1/crates/rand/1.2.0/downloads', - }, - num: '1.2.0', - published_by: null, - readme_path: '/api/v1/crates/rand/1.2.0/readme', - rust_version: '1.69', - updated_at: '2017-02-24T12:34:56Z', - yanked: false, - yank_message: null, - }, - ], - meta: { total: 3, next_page: null }, - }); - }); - - test('supports multiple `ids[]` parameters', async function (assert) { - let user = this.server.create('user'); - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.1.0', publishedBy: user }); - this.server.create('version', { crate, num: '1.2.0', rust_version: '1.69' }); - let response = await fetch('/api/v1/crates/rand/versions?nums[]=1.0.0&nums[]=1.2.0'); - assert.strictEqual(response.status, 200); - let json = await response.json(); - assert.deepEqual( - json.versions.map(v => v.num), - ['1.0.0', '1.2.0'], - ); - }); - - test('include `release_tracks` meta', async function (assert) { - let user = this.server.create('user'); - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { crate, num: '0.0.1' }); - this.server.create('version', { crate, num: '0.0.2', yanked: true }); - this.server.create('version', { crate, num: '1.0.0' }); - this.server.create('version', { crate, num: '1.1.0', publishedBy: user }); - this.server.create('version', { crate, num: '1.2.0', rust_version: '1.69', yanked: true }); - - let req = await fetch('/api/v1/crates/rand/versions'); - let expected = await req.json(); - - let response = await fetch('/api/v1/crates/rand/versions?include=release_tracks'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - ...expected, - meta: { - ...expected.meta, - release_tracks: { - '0.0': { - highest: '0.0.1', - }, - 1: { - highest: '1.1.0', - }, - }, - }, - }); - }); -}); diff --git a/tests/mirage/crates/versions/patch-test.js b/tests/mirage/crates/versions/patch-test.js deleted file mode 100644 index 00472129f38..00000000000 --- a/tests/mirage/crates/versions/patch-test.js +++ /dev/null @@ -1,120 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -const YANK_BODY = JSON.stringify({ - version: { - yanked: true, - yank_message: 'some reason', - }, -}); - -const UNYANK_BODY = JSON.stringify({ - version: { - yanked: false, - }, -}); - -module('Mirage | PATCH /api/v1/crates/:name/:version', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 403 if unauthenticated', async function (assert) { - let response = await fetch('/api/v1/crates/foo/1.0.0', { method: 'PATCH', body: YANK_BODY }); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); - - test('returns 404 for unknown crates', async function (assert) { - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/foo/1.0.0', { method: 'PATCH', body: YANK_BODY }); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('returns 404 for unknown versions', async function (assert) { - this.server.create('crate', { name: 'foo' }); - - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/foo/1.0.0', { method: 'PATCH', body: YANK_BODY }); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('yanks the version', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - let version = this.server.create('version', { crate, num: '1.0.0', yanked: false }); - assert.false(version.yanked); - assert.strictEqual(version.yank_message, null); - - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/foo/1.0.0', { method: 'PATCH', body: YANK_BODY }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - version: { - crate: 'foo', - crate_size: 0, - created_at: '2010-06-16T21:30:45Z', - dl_path: '/api/v1/crates/foo/1.0.0/download', - downloads: 0, - id: '1', - license: 'MIT/Apache-2.0', - links: { - dependencies: '/api/v1/crates/foo/1.0.0/dependencies', - version_downloads: '/api/v1/crates/foo/1.0.0/downloads', - }, - num: '1.0.0', - published_by: null, - readme_path: '/api/v1/crates/foo/1.0.0/readme', - rust_version: null, - updated_at: '2017-02-24T12:34:56Z', - yank_message: 'some reason', - yanked: true, - }, - }); - - user.reload(); - assert.true(version.yanked); - assert.strictEqual(version.yank_message, 'some reason'); - - response = await fetch('/api/v1/crates/foo/1.0.0', { method: 'PATCH', body: UNYANK_BODY }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - version: { - crate: 'foo', - crate_size: 0, - created_at: '2010-06-16T21:30:45Z', - dl_path: '/api/v1/crates/foo/1.0.0/download', - downloads: 0, - id: '1', - license: 'MIT/Apache-2.0', - links: { - dependencies: '/api/v1/crates/foo/1.0.0/dependencies', - version_downloads: '/api/v1/crates/foo/1.0.0/downloads', - }, - num: '1.0.0', - published_by: null, - readme_path: '/api/v1/crates/foo/1.0.0/readme', - rust_version: null, - updated_at: '2017-02-24T12:34:56Z', - yank_message: null, - yanked: false, - }, - }); - - user.reload(); - assert.false(version.yanked); - assert.strictEqual(version.yank_message, null); - }); -}); diff --git a/tests/mirage/crates/versions/readme-test.js b/tests/mirage/crates/versions/readme-test.js deleted file mode 100644 index 03746524f95..00000000000 --- a/tests/mirage/crates/versions/readme-test.js +++ /dev/null @@ -1,45 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/crates/:name/:version/readme', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 404 for unknown crates', async function (assert) { - let response = await fetch('/api/v1/crates/foo/1.0.0/readme'); - assert.strictEqual(response.status, 403); - assert.strictEqual(await response.text(), ''); - }); - - test('returns 404 for unknown versions', async function (assert) { - this.server.create('crate', { name: 'rand' }); - - let response = await fetch('/api/v1/crates/rand/1.0.0/readme'); - assert.strictEqual(response.status, 403); - assert.strictEqual(await response.text(), ''); - }); - - test('returns 404 for versions without README', async function (assert) { - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { crate, num: '1.0.0' }); - - let response = await fetch('/api/v1/crates/rand/1.0.0/readme'); - assert.strictEqual(response.status, 403); - assert.strictEqual(await response.text(), ''); - }); - - test('returns the README as raw HTML', async function (assert) { - let readme = 'lorem ipsum est dolor!'; - - let crate = this.server.create('crate', { name: 'rand' }); - this.server.create('version', { crate, num: '1.0.0', readme: readme }); - - let response = await fetch('/api/v1/crates/rand/1.0.0/readme'); - assert.strictEqual(response.status, 200); - assert.strictEqual(await response.text(), readme); - }); -}); diff --git a/tests/mirage/crates/versions/yank/unyank-test.js b/tests/mirage/crates/versions/yank/unyank-test.js deleted file mode 100644 index b5fa969a79e..00000000000 --- a/tests/mirage/crates/versions/yank/unyank-test.js +++ /dev/null @@ -1,57 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../../helpers'; -import setupMirage from '../../../../helpers/setup-mirage'; - -module('Mirage | PUT /api/v1/crates/:name/unyank', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 403 if unauthenticated', async function (assert) { - let response = await fetch('/api/v1/crates/foo/1.0.0/unyank', { method: 'PUT' }); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); - - test('returns 404 for unknown crates', async function (assert) { - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/foo/1.0.0/unyank', { method: 'PUT' }); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('returns 404 for unknown versions', async function (assert) { - this.server.create('crate', { name: 'foo' }); - - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/foo/1.0.0/unyank', { method: 'PUT' }); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('unyanks the version', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - let version = this.server.create('version', { crate, num: '1.0.0', yanked: true, yank_message: 'some reason' }); - assert.true(version.yanked); - assert.strictEqual(version.yank_message, 'some reason'); - - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/foo/1.0.0/unyank', { method: 'PUT' }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { ok: true }); - - user.reload(); - assert.false(version.yanked); - assert.strictEqual(version.yank_message, null); - }); -}); diff --git a/tests/mirage/crates/versions/yank/yank-test.js b/tests/mirage/crates/versions/yank/yank-test.js deleted file mode 100644 index a08af7055c2..00000000000 --- a/tests/mirage/crates/versions/yank/yank-test.js +++ /dev/null @@ -1,55 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../../helpers'; -import setupMirage from '../../../../helpers/setup-mirage'; - -module('Mirage | DELETE /api/v1/crates/:name/yank', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 403 if unauthenticated', async function (assert) { - let response = await fetch('/api/v1/crates/foo/1.0.0/yank', { method: 'DELETE' }); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); - - test('returns 404 for unknown crates', async function (assert) { - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/foo/1.0.0/yank', { method: 'DELETE' }); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('returns 404 for unknown versions', async function (assert) { - this.server.create('crate', { name: 'foo' }); - - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/foo/1.0.0/yank', { method: 'DELETE' }); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('yanks the version', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - let version = this.server.create('version', { crate, num: '1.0.0', yanked: false }); - assert.false(version.yanked); - - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/crates/foo/1.0.0/yank', { method: 'DELETE' }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { ok: true }); - - user.reload(); - assert.true(version.yanked); - }); -}); diff --git a/tests/mirage/keywords/get-by-id-test.js b/tests/mirage/keywords/get-by-id-test.js deleted file mode 100644 index 751cc873dc5..00000000000 --- a/tests/mirage/keywords/get-by-id-test.js +++ /dev/null @@ -1,48 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/keywords/:id', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 404 for unknown keywords', async function (assert) { - let response = await fetch('/api/v1/keywords/foo'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('returns a keyword object for known keywords', async function (assert) { - this.server.create('keyword', { keyword: 'cli' }); - - let response = await fetch('/api/v1/keywords/cli'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - keyword: { - id: 'cli', - crates_cnt: 0, - keyword: 'cli', - }, - }); - }); - - test('calculates `crates_cnt` correctly', async function (assert) { - this.server.create('keyword', { keyword: 'cli' }); - this.server.createList('crate', 7, { keywordIds: ['cli'] }); - this.server.create('keyword', { keyword: 'not-cli' }); - this.server.createList('crate', 3, { keywordIds: ['not-cli'] }); - - let response = await fetch('/api/v1/keywords/cli'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - keyword: { - id: 'cli', - crates_cnt: 7, - keyword: 'cli', - }, - }); - }); -}); diff --git a/tests/mirage/keywords/list-test.js b/tests/mirage/keywords/list-test.js deleted file mode 100644 index 6e63c55a853..00000000000 --- a/tests/mirage/keywords/list-test.js +++ /dev/null @@ -1,80 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/keywords', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('empty case', async function (assert) { - let response = await fetch('/api/v1/keywords'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - keywords: [], - meta: { - total: 0, - }, - }); - }); - - test('returns a paginated keywords list', async function (assert) { - this.server.create('keyword', { keyword: 'api' }); - this.server.createList('keyword', 2); - - let response = await fetch('/api/v1/keywords'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - keywords: [ - { - id: 'api', - crates_cnt: 0, - keyword: 'api', - }, - { - id: 'keyword-2', - crates_cnt: 0, - keyword: 'keyword-2', - }, - { - id: 'keyword-3', - crates_cnt: 0, - keyword: 'keyword-3', - }, - ], - meta: { - total: 3, - }, - }); - }); - - test('never returns more than 10 results', async function (assert) { - this.server.createList('keyword', 25); - - let response = await fetch('/api/v1/keywords'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.strictEqual(responsePayload.keywords.length, 10); - assert.strictEqual(responsePayload.meta.total, 25); - }); - - test('supports `page` and `per_page` parameters', async function (assert) { - this.server.createList('keyword', 25, { - keyword: i => `k${String(i + 1).padStart(2, '0')}`, - }); - - let response = await fetch('/api/v1/keywords?page=2&per_page=5'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.strictEqual(responsePayload.keywords.length, 5); - assert.deepEqual( - responsePayload.keywords.map(it => it.id), - ['k06', 'k07', 'k08', 'k09', 'k10'], - ); - assert.strictEqual(responsePayload.meta.total, 25); - }); -}); diff --git a/tests/mirage/me/crate-owner-invitations/list-test.js b/tests/mirage/me/crate-owner-invitations/list-test.js deleted file mode 100644 index 4811dd866f0..00000000000 --- a/tests/mirage/me/crate-owner-invitations/list-test.js +++ /dev/null @@ -1,101 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/me/crate_owner_invitations', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('empty case', async function (assert) { - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let response = await fetch('/api/v1/me/crate_owner_invitations'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { crate_owner_invitations: [] }); - }); - - test('returns the list of invitations for the authenticated user', async function (assert) { - let nanomsg = this.server.create('crate', { name: 'nanomsg' }); - this.server.create('version', { crate: nanomsg }); - - let ember = this.server.create('crate', { name: 'ember-rs' }); - this.server.create('version', { crate: ember }); - - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let inviter = this.server.create('user', { name: 'janed' }); - this.server.create('crate-owner-invitation', { - crate: nanomsg, - createdAt: '2016-12-24T12:34:56Z', - invitee: user, - inviter, - }); - - let inviter2 = this.server.create('user', { name: 'wycats' }); - this.server.create('crate-owner-invitation', { - crate: ember, - createdAt: '2020-12-31T12:34:56Z', - invitee: user, - inviter: inviter2, - }); - - let response = await fetch('/api/v1/me/crate_owner_invitations'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - crate_owner_invitations: [ - { - crate_id: Number(nanomsg.id), - crate_name: 'nanomsg', - created_at: '2016-12-24T12:34:56Z', - expires_at: '2017-01-24T12:34:56Z', - invitee_id: Number(user.id), - inviter_id: Number(inviter.id), - }, - { - crate_id: Number(ember.id), - crate_name: 'ember-rs', - created_at: '2020-12-31T12:34:56Z', - expires_at: '2017-01-24T12:34:56Z', - invitee_id: Number(user.id), - inviter_id: Number(inviter2.id), - }, - ], - users: [ - { - avatar: user.avatar, - id: Number(user.id), - login: user.login, - name: user.name, - url: user.url, - }, - { - avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', - id: Number(inviter.id), - login: 'janed', - name: 'janed', - url: 'https://github.com/janed', - }, - { - avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', - id: Number(inviter2.id), - login: 'wycats', - name: 'wycats', - url: 'https://github.com/wycats', - }, - ], - }); - }); - - test('returns an error if unauthenticated', async function (assert) { - let response = await fetch('/api/v1/me/crate_owner_invitations'); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); -}); diff --git a/tests/mirage/me/get-test.js b/tests/mirage/me/get-test.js deleted file mode 100644 index 2d1f63e7668..00000000000 --- a/tests/mirage/me/get-test.js +++ /dev/null @@ -1,63 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/me', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns the `user` resource including the private fields', async function (assert) { - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let response = await fetch('/api/v1/me'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - user: { - id: 1, - avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', - email: 'user-1@crates.io', - email_verification_sent: true, - email_verified: true, - is_admin: false, - login: 'user-1', - name: 'User 1', - publish_notifications: true, - url: 'https://github.com/user-1', - }, - owned_crates: [], - }); - }); - - test('returns a list of `owned_crates`', async function (assert) { - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let [crate1, , crate3] = this.server.createList('crate', 3); - - this.server.create('crate-ownership', { crate: crate1, user }); - this.server.create('crate-ownership', { crate: crate3, user }); - - let response = await fetch('/api/v1/me'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.deepEqual(responsePayload.owned_crates, [ - { id: crate1.id, name: 'crate-0', email_notifications: true }, - { id: crate3.id, name: 'crate-2', email_notifications: true }, - ]); - }); - - test('returns an error if unauthenticated', async function (assert) { - this.server.create('user'); - - let response = await fetch('/api/v1/me'); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); -}); diff --git a/tests/mirage/me/tokens/create-test.js b/tests/mirage/me/tokens/create-test.js deleted file mode 100644 index 7b28c309e08..00000000000 --- a/tests/mirage/me/tokens/create-test.js +++ /dev/null @@ -1,115 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | PUT /api/v1/me/tokens', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('creates a new API token', async function (assert) { - this.clock.setSystemTime(new Date('2017-11-20T11:23:45Z')); - - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let body = JSON.stringify({ api_token: { name: 'foooo' } }); - let response = await fetch('/api/v1/me/tokens', { method: 'PUT', body }); - assert.strictEqual(response.status, 200); - - let token = this.server.schema.apiTokens.all().models[0]; - assert.ok(token); - - assert.deepEqual(await response.json(), { - api_token: { - id: 1, - crate_scopes: null, - created_at: '2017-11-20T11:23:45.000Z', - endpoint_scopes: null, - expired_at: null, - last_used_at: null, - name: 'foooo', - revoked: false, - token: token.token, - }, - }); - }); - - test('creates a new API token with scopes', async function (assert) { - this.clock.setSystemTime(new Date('2017-11-20T11:23:45Z')); - - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let body = JSON.stringify({ - api_token: { - name: 'foooo', - crate_scopes: ['serde', 'serde-*'], - endpoint_scopes: ['publish-update'], - }, - }); - let response = await fetch('/api/v1/me/tokens', { method: 'PUT', body }); - assert.strictEqual(response.status, 200); - - let token = this.server.schema.apiTokens.all().models[0]; - assert.ok(token); - - assert.deepEqual(await response.json(), { - api_token: { - id: 1, - crate_scopes: ['serde', 'serde-*'], - created_at: '2017-11-20T11:23:45.000Z', - endpoint_scopes: ['publish-update'], - expired_at: null, - last_used_at: null, - name: 'foooo', - revoked: false, - token: token.token, - }, - }); - }); - - test('creates a new API token with expiry date', async function (assert) { - this.clock.setSystemTime(new Date('2017-11-20T11:23:45Z')); - - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let body = JSON.stringify({ - api_token: { - name: 'foooo', - expired_at: '2023-12-24T12:34:56Z', - }, - }); - let response = await fetch('/api/v1/me/tokens', { method: 'PUT', body }); - assert.strictEqual(response.status, 200); - - let token = this.server.schema.apiTokens.all().models[0]; - assert.ok(token); - - assert.deepEqual(await response.json(), { - api_token: { - id: 1, - crate_scopes: null, - created_at: '2017-11-20T11:23:45.000Z', - endpoint_scopes: null, - expired_at: '2023-12-24T12:34:56.000Z', - last_used_at: null, - name: 'foooo', - revoked: false, - token: token.token, - }, - }); - }); - - test('returns an error if unauthenticated', async function (assert) { - let body = JSON.stringify({ api_token: {} }); - let response = await fetch('/api/v1/me/tokens', { method: 'PUT', body }); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); -}); diff --git a/tests/mirage/me/tokens/delete-by-id-test.js b/tests/mirage/me/tokens/delete-by-id-test.js deleted file mode 100644 index d6d61209404..00000000000 --- a/tests/mirage/me/tokens/delete-by-id-test.js +++ /dev/null @@ -1,36 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | DELETE /api/v1/me/tokens/:tokenId', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('revokes an API token', async function (assert) { - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let token = this.server.create('api-token', { user }); - - let response = await fetch(`/api/v1/me/tokens/${token.id}`, { method: 'DELETE' }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), {}); - - let tokens = this.server.schema.apiTokens.all().models; - assert.strictEqual(tokens.length, 0); - }); - - test('returns an error if unauthenticated', async function (assert) { - let user = this.server.create('user'); - let token = this.server.create('api-token', { user }); - - let response = await fetch(`/api/v1/me/tokens/${token.id}`, { method: 'DELETE' }); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); -}); diff --git a/tests/mirage/me/tokens/list-test.js b/tests/mirage/me/tokens/list-test.js deleted file mode 100644 index ab2450cef8a..00000000000 --- a/tests/mirage/me/tokens/list-test.js +++ /dev/null @@ -1,77 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/me/tokens', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns the list of API token for the authenticated `user`', async function (assert) { - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - this.server.create('api-token', { - user, - createdAt: '2017-11-19T12:59:22Z', - crateScopes: ['serde', 'serde-*'], - endpointScopes: ['publish-update'], - }); - this.server.create('api-token', { user, createdAt: '2017-11-19T13:59:22Z', expiredAt: '2023-11-20T10:59:22Z' }); - this.server.create('api-token', { user, createdAt: '2017-11-19T14:59:22Z' }); - this.server.create('api-token', { user, createdAt: '2017-11-19T15:59:22Z', expiredAt: '2017-11-20T10:59:22Z' }); - - let response = await fetch('/api/v1/me/tokens'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - api_tokens: [ - { - id: 3, - crate_scopes: null, - created_at: '2017-11-19T14:59:22.000Z', - endpoint_scopes: null, - expired_at: null, - last_used_at: null, - name: 'API Token 3', - }, - { - id: 2, - crate_scopes: null, - created_at: '2017-11-19T13:59:22.000Z', - endpoint_scopes: null, - expired_at: '2023-11-20T10:59:22.000Z', - last_used_at: null, - name: 'API Token 2', - }, - { - id: 1, - crate_scopes: ['serde', 'serde-*'], - created_at: '2017-11-19T12:59:22.000Z', - endpoint_scopes: ['publish-update'], - expired_at: null, - last_used_at: null, - name: 'API Token 1', - }, - ], - }); - }); - - test('empty list case', async function (assert) { - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let response = await fetch('/api/v1/me/tokens'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { api_tokens: [] }); - }); - - test('returns an error if unauthenticated', async function (assert) { - let response = await fetch('/api/v1/me/tokens'); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); -}); diff --git a/tests/mirage/me/updates/list-test.js b/tests/mirage/me/updates/list-test.js deleted file mode 100644 index b67762befd5..00000000000 --- a/tests/mirage/me/updates/list-test.js +++ /dev/null @@ -1,91 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/me/updates', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 403 for unauthenticated user', async function (assert) { - let response = await fetch('/api/v1/me/updates'); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); - - test('returns latest versions of followed crates', async function (assert) { - let foo = this.server.create('crate', { name: 'foo' }); - this.server.create('version', { crate: foo, num: '1.2.3' }); - - let bar = this.server.create('crate', { name: 'bar' }); - this.server.create('version', { crate: bar, num: '0.8.6' }); - - let user = this.server.create('user', { followedCrates: [foo] }); - this.authenticateAs(user); - - let response = await fetch('/api/v1/me/updates'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - versions: [ - { - id: '1', - crate: 'foo', - crate_size: 0, - created_at: '2010-06-16T21:30:45Z', - dl_path: '/api/v1/crates/foo/1.2.3/download', - downloads: 0, - license: 'MIT/Apache-2.0', - links: { - dependencies: '/api/v1/crates/foo/1.2.3/dependencies', - version_downloads: '/api/v1/crates/foo/1.2.3/downloads', - }, - num: '1.2.3', - published_by: null, - readme_path: '/api/v1/crates/foo/1.2.3/readme', - rust_version: null, - updated_at: '2017-02-24T12:34:56Z', - yanked: false, - yank_message: null, - }, - ], - meta: { - more: false, - }, - }); - }); - - test('empty case', async function (assert) { - let user = this.server.create('user'); - this.authenticateAs(user); - - let response = await fetch('/api/v1/me/updates'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - versions: [], - meta: { more: false }, - }); - }); - - test('supports pagination', async function (assert) { - let crate = this.server.create('crate', { name: 'foo' }); - this.server.createList('version', 25, { crate }); - - let user = this.server.create('user', { followedCrates: [crate] }); - this.authenticateAs(user); - - let response = await fetch('/api/v1/me/updates?page=2'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - assert.strictEqual(responsePayload.versions.length, 10); - assert.deepEqual( - responsePayload.versions.map(it => it.id), - ['15', '14', '13', '12', '11', '10', '9', '8', '7', '6'], - ); - assert.deepEqual(responsePayload.meta, { more: true }); - }); -}); diff --git a/tests/mirage/private/crate-owner-invitations/get-test.js b/tests/mirage/private/crate-owner-invitations/get-test.js deleted file mode 100644 index 9ea3a225262..00000000000 --- a/tests/mirage/private/crate-owner-invitations/get-test.js +++ /dev/null @@ -1,229 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | GET /api/private/crate_owner_invitations', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('happy path (invitee_id)', async function (assert) { - let nanomsg = this.server.create('crate', { name: 'nanomsg' }); - this.server.create('version', { crate: nanomsg }); - - let ember = this.server.create('crate', { name: 'ember-rs' }); - this.server.create('version', { crate: ember }); - - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let inviter = this.server.create('user', { name: 'janed' }); - this.server.create('crate-owner-invitation', { - crate: nanomsg, - createdAt: '2016-12-24T12:34:56Z', - invitee: user, - inviter, - }); - - let inviter2 = this.server.create('user', { name: 'wycats' }); - this.server.create('crate-owner-invitation', { - crate: ember, - createdAt: '2020-12-31T12:34:56Z', - invitee: user, - inviter: inviter2, - }); - - let response = await fetch(`/api/private/crate_owner_invitations?invitee_id=${user.id}`); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - crate_owner_invitations: [ - { - crate_id: Number(nanomsg.id), - crate_name: 'nanomsg', - created_at: '2016-12-24T12:34:56Z', - expires_at: '2017-01-24T12:34:56Z', - invitee_id: Number(user.id), - inviter_id: Number(inviter.id), - }, - { - crate_id: Number(ember.id), - crate_name: 'ember-rs', - created_at: '2020-12-31T12:34:56Z', - expires_at: '2017-01-24T12:34:56Z', - invitee_id: Number(user.id), - inviter_id: Number(inviter2.id), - }, - ], - users: [ - { - avatar: user.avatar, - id: Number(user.id), - login: user.login, - name: user.name, - url: user.url, - }, - { - avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', - id: Number(inviter.id), - login: 'janed', - name: 'janed', - url: 'https://github.com/janed', - }, - { - avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', - id: Number(inviter2.id), - login: 'wycats', - name: 'wycats', - url: 'https://github.com/wycats', - }, - ], - meta: { - next_page: null, - }, - }); - }); - - test('happy path with empty response (invitee_id)', async function (assert) { - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let response = await fetch(`/api/private/crate_owner_invitations?invitee_id=${user.id}`); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - crate_owner_invitations: [], - users: [], - meta: { - next_page: null, - }, - }); - }); - - test('happy path with pagination (invitee_id)', async function (assert) { - let inviter = this.server.create('user'); - - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - for (let i = 0; i < 15; i++) { - let crate = this.server.create('crate'); - this.server.create('version', { crate }); - this.server.create('crate-owner-invitation', { crate, invitee: user, inviter }); - } - - let response = await fetch(`/api/private/crate_owner_invitations?invitee_id=${user.id}`); - assert.strictEqual(response.status, 200); - let responseJSON = await response.json(); - assert.strictEqual(responseJSON['crate_owner_invitations'].length, 10); - assert.ok(responseJSON.meta['next_page']); - - response = await fetch(`/api/private/crate_owner_invitations${responseJSON.meta['next_page']}`); - assert.strictEqual(response.status, 200); - responseJSON = await response.json(); - assert.strictEqual(responseJSON['crate_owner_invitations'].length, 5); - assert.strictEqual(responseJSON.meta['next_page'], null); - }); - - test('happy path (crate_name)', async function (assert) { - let nanomsg = this.server.create('crate', { name: 'nanomsg' }); - this.server.create('version', { crate: nanomsg }); - - let ember = this.server.create('crate', { name: 'ember-rs' }); - this.server.create('version', { crate: ember }); - - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let inviter = this.server.create('user', { name: 'janed' }); - this.server.create('crate-owner-invitation', { - crate: nanomsg, - createdAt: '2016-12-24T12:34:56Z', - invitee: user, - inviter, - }); - - let inviter2 = this.server.create('user', { name: 'wycats' }); - this.server.create('crate-owner-invitation', { - crate: ember, - createdAt: '2020-12-31T12:34:56Z', - invitee: user, - inviter: inviter2, - }); - - let response = await fetch(`/api/private/crate_owner_invitations?crate_name=ember-rs`); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - crate_owner_invitations: [ - { - crate_id: Number(ember.id), - crate_name: 'ember-rs', - created_at: '2020-12-31T12:34:56Z', - expires_at: '2017-01-24T12:34:56Z', - invitee_id: Number(user.id), - inviter_id: Number(inviter2.id), - }, - ], - users: [ - { - avatar: user.avatar, - id: Number(user.id), - login: user.login, - name: user.name, - url: user.url, - }, - { - avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', - id: Number(inviter2.id), - login: 'wycats', - name: 'wycats', - url: 'https://github.com/wycats', - }, - ], - meta: { - next_page: null, - }, - }); - }); - - test('returns 403 if unauthenticated', async function (assert) { - let response = await fetch(`/api/private/crate_owner_invitations?invitee_id=42`); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); - - test('returns 400 if query params are missing', async function (assert) { - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let response = await fetch(`/api/private/crate_owner_invitations`); - assert.strictEqual(response.status, 400); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'missing or invalid filter' }], - }); - }); - - test("returns 404 if crate can't be found", async function (assert) { - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let response = await fetch(`/api/private/crate_owner_invitations?crate_name=foo`); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'Not Found' }], - }); - }); - - test('returns 403 if requesting for other user', async function (assert) { - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let response = await fetch(`/api/private/crate_owner_invitations?invitee_id=${user.id + 1}`); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { - errors: [{ detail: 'must be logged in to perform that action' }], - }); - }); -}); diff --git a/tests/mirage/private/session/delete-test.js b/tests/mirage/private/session/delete-test.js deleted file mode 100644 index 84e2cddf83d..00000000000 --- a/tests/mirage/private/session/delete-test.js +++ /dev/null @@ -1,30 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../../helpers'; -import setupMirage from '../../../helpers/setup-mirage'; - -module('Mirage | DELETE /api/private/session', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 200 when authenticated', async function (assert) { - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let response = await fetch('/api/private/session', { method: 'DELETE' }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { ok: true }); - - assert.notOk(this.server.schema.mirageSessions.first()); - }); - - test('returns 200 when unauthenticated', async function (assert) { - let response = await fetch('/api/private/session', { method: 'DELETE' }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { ok: true }); - - assert.notOk(this.server.schema.mirageSessions.first()); - }); -}); diff --git a/tests/mirage/summary-test.js b/tests/mirage/summary-test.js deleted file mode 100644 index 5686aeb128d..00000000000 --- a/tests/mirage/summary-test.js +++ /dev/null @@ -1,175 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from 'crates-io/tests/helpers'; - -import setupMirage from '../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/summary', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('empty case', async function (assert) { - let response = await fetch('/api/v1/summary'); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - just_updated: [], - most_downloaded: [], - most_recently_downloaded: [], - new_crates: [], - num_crates: 0, - num_downloads: 0, - popular_categories: [], - popular_keywords: [], - }); - }); - - test('returns the data for the front page', async function (assert) { - this.server.createList('category', 15); - this.server.createList('keyword', 25); - let crates = this.server.createList('crate', 20); - this.server.createList('version', crates.length, { crate: i => crates[i] }); - - let response = await fetch('/api/v1/summary'); - assert.strictEqual(response.status, 200); - - let responsePayload = await response.json(); - - assert.strictEqual(responsePayload.just_updated.length, 10); - assert.deepEqual(responsePayload.just_updated[0], { - id: 'crate-0', - badges: [], - categories: [], - created_at: '2010-06-16T21:30:45Z', - default_version: '1.0.0', - description: 'This is the description for the crate called "crate-0"', - documentation: null, - downloads: 0, - homepage: null, - keywords: [], - links: { - owner_team: '/api/v1/crates/crate-0/owner_team', - owner_user: '/api/v1/crates/crate-0/owner_user', - reverse_dependencies: '/api/v1/crates/crate-0/reverse_dependencies', - version_downloads: '/api/v1/crates/crate-0/downloads', - versions: '/api/v1/crates/crate-0/versions', - }, - max_version: '1.0.0', - max_stable_version: '1.0.0', - name: 'crate-0', - newest_version: '1.0.0', - repository: null, - updated_at: '2017-02-24T12:34:56Z', - versions: null, - yanked: false, - }); - - assert.strictEqual(responsePayload.most_downloaded.length, 10); - assert.deepEqual(responsePayload.most_downloaded[0], { - id: 'crate-4', - badges: [], - categories: [], - created_at: '2010-06-16T21:30:45Z', - default_version: '1.0.4', - description: 'This is the description for the crate called "crate-4"', - documentation: null, - downloads: 148_140, - homepage: null, - keywords: [], - links: { - owner_team: '/api/v1/crates/crate-4/owner_team', - owner_user: '/api/v1/crates/crate-4/owner_user', - reverse_dependencies: '/api/v1/crates/crate-4/reverse_dependencies', - version_downloads: '/api/v1/crates/crate-4/downloads', - versions: '/api/v1/crates/crate-4/versions', - }, - max_version: '1.0.4', - max_stable_version: '1.0.4', - name: 'crate-4', - newest_version: '1.0.4', - repository: null, - updated_at: '2017-02-24T12:34:56Z', - versions: null, - yanked: false, - }); - - assert.strictEqual(responsePayload.most_recently_downloaded.length, 10); - assert.deepEqual(responsePayload.most_recently_downloaded[0], { - id: 'crate-0', - badges: [], - categories: [], - created_at: '2010-06-16T21:30:45Z', - default_version: '1.0.0', - description: 'This is the description for the crate called "crate-0"', - documentation: null, - downloads: 0, - homepage: null, - keywords: [], - links: { - owner_team: '/api/v1/crates/crate-0/owner_team', - owner_user: '/api/v1/crates/crate-0/owner_user', - reverse_dependencies: '/api/v1/crates/crate-0/reverse_dependencies', - version_downloads: '/api/v1/crates/crate-0/downloads', - versions: '/api/v1/crates/crate-0/versions', - }, - max_version: '1.0.0', - max_stable_version: '1.0.0', - name: 'crate-0', - newest_version: '1.0.0', - repository: null, - updated_at: '2017-02-24T12:34:56Z', - versions: null, - yanked: false, - }); - - assert.strictEqual(responsePayload.new_crates.length, 10); - assert.deepEqual(responsePayload.new_crates[0], { - id: 'crate-0', - badges: [], - categories: [], - created_at: '2010-06-16T21:30:45Z', - default_version: '1.0.0', - description: 'This is the description for the crate called "crate-0"', - documentation: null, - downloads: 0, - homepage: null, - keywords: [], - links: { - owner_team: '/api/v1/crates/crate-0/owner_team', - owner_user: '/api/v1/crates/crate-0/owner_user', - reverse_dependencies: '/api/v1/crates/crate-0/reverse_dependencies', - version_downloads: '/api/v1/crates/crate-0/downloads', - versions: '/api/v1/crates/crate-0/versions', - }, - max_version: '1.0.0', - max_stable_version: '1.0.0', - name: 'crate-0', - newest_version: '1.0.0', - repository: null, - updated_at: '2017-02-24T12:34:56Z', - versions: null, - yanked: false, - }); - - assert.strictEqual(responsePayload.num_crates, 20); - assert.strictEqual(responsePayload.num_downloads, 1_419_675); - - assert.strictEqual(responsePayload.popular_categories.length, 10); - assert.deepEqual(responsePayload.popular_categories[0], { - id: 'category-0', - category: 'Category 0', - crates_cnt: 0, - created_at: '2010-06-16T21:30:45Z', - description: 'This is the description for the category called "Category 0"', - slug: 'category-0', - }); - - assert.strictEqual(responsePayload.popular_keywords.length, 10); - assert.deepEqual(responsePayload.popular_keywords[0], { - id: 'keyword-1', - crates_cnt: 0, - keyword: 'keyword-1', - }); - }); -}); diff --git a/tests/mirage/teams/get-by-id-test.js b/tests/mirage/teams/get-by-id-test.js deleted file mode 100644 index 85dcd0c98fe..00000000000 --- a/tests/mirage/teams/get-by-id-test.js +++ /dev/null @@ -1,33 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/teams/:id', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 404 for unknown teams', async function (assert) { - let response = await fetch('/api/v1/teams/foo'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('returns a team object for known teams', async function (assert) { - let team = this.server.create('team', { name: 'maintainers' }); - - let response = await fetch(`/api/v1/teams/${team.login}`); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - team: { - id: 1, - avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', - login: 'github:rust-lang:maintainers', - name: 'maintainers', - url: 'https://github.com/rust-lang', - }, - }); - }); -}); diff --git a/tests/mirage/users/get-by-id-test.js b/tests/mirage/users/get-by-id-test.js deleted file mode 100644 index 7164d291f06..00000000000 --- a/tests/mirage/users/get-by-id-test.js +++ /dev/null @@ -1,33 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | GET /api/v1/users/:id', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns 404 for unknown users', async function (assert) { - let response = await fetch('/api/v1/users/foo'); - assert.strictEqual(response.status, 404); - assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] }); - }); - - test('returns a user object for known users', async function (assert) { - let user = this.server.create('user', { name: 'John Doe' }); - - let response = await fetch(`/api/v1/users/${user.login}`); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { - user: { - id: 1, - avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', - login: 'john-doe', - name: 'John Doe', - url: 'https://github.com/john-doe', - }, - }); - }); -}); diff --git a/tests/mirage/users/resend-by-id-test.js b/tests/mirage/users/resend-by-id-test.js deleted file mode 100644 index 7e0111196be..00000000000 --- a/tests/mirage/users/resend-by-id-test.js +++ /dev/null @@ -1,37 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | PUT /api/v1/users/:id/resend', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('returns `ok`', async function (assert) { - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let response = await fetch(`/api/v1/users/${user.id}/resend`, { method: 'PUT' }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { ok: true }); - }); - - test('returns 403 when not logged in', async function (assert) { - let user = this.server.create('user'); - - let response = await fetch(`/api/v1/users/${user.id}/resend`, { method: 'PUT' }); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { errors: [{ detail: 'must be logged in to perform that action' }] }); - }); - - test('returns 400 when requesting the wrong user id', async function (assert) { - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - - let response = await fetch(`/api/v1/users/wrong-id/resend`, { method: 'PUT' }); - assert.strictEqual(response.status, 400); - assert.deepEqual(await response.json(), { errors: [{ detail: 'current user does not match requested user' }] }); - }); -}); diff --git a/tests/mirage/users/update-by-id-test.js b/tests/mirage/users/update-by-id-test.js deleted file mode 100644 index 8019c7d22eb..00000000000 --- a/tests/mirage/users/update-by-id-test.js +++ /dev/null @@ -1,91 +0,0 @@ -import { module, test } from 'qunit'; - -import fetch from 'fetch'; - -import { setupTest } from '../../helpers'; -import setupMirage from '../../helpers/setup-mirage'; - -module('Mirage | PUT /api/v1/users/:id', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - test('updates the user with a new email address', async function (assert) { - let user = this.server.create('user', { email: 'old@email.com' }); - this.server.create('mirage-session', { user }); - - let body = JSON.stringify({ user: { email: 'new@email.com' } }); - let response = await fetch(`/api/v1/users/${user.id}`, { method: 'PUT', body }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { ok: true }); - - user.reload(); - assert.strictEqual(user.email, 'new@email.com'); - assert.false(user.emailVerified); - assert.strictEqual(user.emailVerificationToken, 'secret123'); - }); - - test('updates the `publish_notifications` settings', async function (assert) { - let user = this.server.create('user'); - this.server.create('mirage-session', { user }); - assert.true(user.publishNotifications); - - let body = JSON.stringify({ user: { publish_notifications: false } }); - let response = await fetch(`/api/v1/users/${user.id}`, { method: 'PUT', body }); - assert.strictEqual(response.status, 200); - assert.deepEqual(await response.json(), { ok: true }); - - user.reload(); - assert.false(user.publishNotifications); - }); - - test('returns 403 when not logged in', async function (assert) { - let user = this.server.create('user', { email: 'old@email.com' }); - - let body = JSON.stringify({ user: { email: 'new@email.com' } }); - let response = await fetch(`/api/v1/users/${user.id}`, { method: 'PUT', body }); - assert.strictEqual(response.status, 403); - assert.deepEqual(await response.json(), { errors: [{ detail: 'must be logged in to perform that action' }] }); - - user.reload(); - assert.strictEqual(user.email, 'old@email.com'); - }); - - test('returns 400 when requesting the wrong user id', async function (assert) { - let user = this.server.create('user', { email: 'old@email.com' }); - this.server.create('mirage-session', { user }); - - let body = JSON.stringify({ user: { email: 'new@email.com' } }); - let response = await fetch(`/api/v1/users/wrong-id`, { method: 'PUT', body }); - assert.strictEqual(response.status, 400); - assert.deepEqual(await response.json(), { errors: [{ detail: 'current user does not match requested user' }] }); - - user.reload(); - assert.strictEqual(user.email, 'old@email.com'); - }); - - test('returns 400 when sending an invalid payload', async function (assert) { - let user = this.server.create('user', { email: 'old@email.com' }); - this.server.create('mirage-session', { user }); - - let body = JSON.stringify({}); - let response = await fetch(`/api/v1/users/${user.id}`, { method: 'PUT', body }); - assert.strictEqual(response.status, 400); - assert.deepEqual(await response.json(), { errors: [{ detail: 'invalid json request' }] }); - - user.reload(); - assert.strictEqual(user.email, 'old@email.com'); - }); - - test('returns 400 when sending an empty email address', async function (assert) { - let user = this.server.create('user', { email: 'old@email.com' }); - this.server.create('mirage-session', { user }); - - let body = JSON.stringify({ user: { email: '' } }); - let response = await fetch(`/api/v1/users/${user.id}`, { method: 'PUT', body }); - assert.strictEqual(response.status, 400); - assert.deepEqual(await response.json(), { errors: [{ detail: 'empty email rejected' }] }); - - user.reload(); - assert.strictEqual(user.email, 'old@email.com'); - }); -});