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 ef84af32f57..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
@@ -239,6 +239,30 @@ jobs:
- 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
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
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/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/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
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');
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();
});
});
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');
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();
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');
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');
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');
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();
});
});
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');
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 }) => {
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);
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');
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');
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('/');
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');
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');
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);
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');
});
});
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('/');
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');
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');
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(
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 }) => {
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();
});
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) => {
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');
});
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}`);
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');
});
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');
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() {} };
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 }) => {
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 };
+}
diff --git a/e2e/fixtures/mirage.ts b/e2e/fixtures/mirage.ts
deleted file mode 100644
index 77dc0b45e4d..00000000000
--- a/e2e/fixtures/mirage.ts
+++ /dev/null
@@ -1,174 +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.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 69ac5c04ec8..b768b7f375f 100644
--- a/e2e/helper.ts
+++ b/e2e/helper.ts
@@ -1,6 +1,10 @@
import { test as base } from '@playwright/test';
+import type { MockServiceWorker } from 'playwright-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';
import { A11yPage } from './fixtures/a11y';
import { EmberPage, EmberPageOptions } from './fixtures/ember';
@@ -12,7 +16,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;
@@ -23,22 +31,41 @@ 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' },
],
- mirage: [
- async ({ page }, use) => {
- let mirage = new MiragePage(page);
- await mirage.setup();
- await use(mirage);
- },
- { auto: true, 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/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('/');
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");
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');
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');
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');
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`);
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');
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');
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');
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');
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');
diff --git a/ember-cli-build.js b/ember-cli-build.js
index 023ea06019f..fa2f9f30696 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,11 +74,25 @@ module.exports = function (defaults) {
const { Webpack } = require('@embroider/webpack');
return require('@embroider/compat').compatBuild(app, Webpack, {
+ extraPublicTrees,
staticAddonTrees: true,
staticAddonTestSupportTrees: true,
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`
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/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 54bb7406f55..ef3c6d78640 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",
@@ -77,6 +78,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",
@@ -88,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",
@@ -99,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",
@@ -127,9 +129,10 @@
"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",
+ "playwright-msw": "3.0.1",
"postcss-preset-env": "10.1.3",
"prettier": "3.4.2",
"qunit": "2.24.1",
@@ -143,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/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/mirage/fixtures/categories.js b/packages/crates-io-msw/fixtures/categories.js
similarity index 100%
rename from mirage/fixtures/categories.js
rename to packages/crates-io-msw/fixtures/categories.js
diff --git a/mirage/fixtures/crate-ownerships.js b/packages/crates-io-msw/fixtures/crate-ownerships.js
similarity index 100%
rename from mirage/fixtures/crate-ownerships.js
rename to packages/crates-io-msw/fixtures/crate-ownerships.js
diff --git a/mirage/fixtures/crates.js b/packages/crates-io-msw/fixtures/crates.js
similarity index 94%
rename from mirage/fixtures/crates.js
rename to packages/crates-io-msw/fixtures/crates.js
index 08ff7c14306..b0431da0099 100644
--- a/mirage/fixtures/crates.js
+++ b/packages/crates-io-msw/fixtures/crates.js
@@ -12,11 +12,9 @@ export default [
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',
@@ -28,7 +26,6 @@ export default [
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',
@@ -39,8 +36,6 @@ export default [
downloads: 14,
},
],
- teamOwnerIds: [1, 303],
- userOwnerIds: [2, 303],
},
{
created_at: '2015-02-27T11:52:13Z',
@@ -57,7 +52,6 @@ export default [
repository: 'https://github.com/huonw/external_mixin',
updated_at: '2015-02-27T11:52:13Z',
badges: [],
- versionIds: [],
},
{
created_at: '2015-02-27T11:51:58Z',
@@ -72,7 +66,6 @@ export default [
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',
@@ -86,7 +79,6 @@ export default [
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',
@@ -101,7 +93,6 @@ export default [
name: 'Inflector',
repository: 'https://github.com/whatisinternet/inflector',
updated_at: '2015-10-27T01:51:42Z',
- versionIds: [],
},
{
created_at: '2015-05-21T17:43:38Z',
@@ -115,7 +106,6 @@ export default [
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',
@@ -129,7 +119,6 @@ export default [
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',
@@ -143,7 +132,6 @@ export default [
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',
@@ -157,7 +145,6 @@ export default [
name: 'rustless',
repository: 'https://crates.io/crates/rustless',
updated_at: '2015-10-31T11:49:29Z',
- versionIds: [],
},
{
created_at: '2014-12-05T20:20:39Z',
@@ -171,7 +158,6 @@ export default [
name: 'serde',
repository: 'https://github.com/serde-rs/serde',
updated_at: '2015-10-18T03:10:21Z',
- versionIds: [],
},
{
created_at: '2015-08-26T13:50:58Z',
@@ -185,7 +171,6 @@ export default [
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',
@@ -200,7 +185,6 @@ export default [
name: 'zlib',
repository: null,
updated_at: '2015-01-02T20:54:04Z',
- versionIds: [],
},
{
created_at: '2015-05-08T19:34:16Z',
@@ -215,7 +199,6 @@ export default [
name: 'rustful',
repository: 'https://github.com/Ogeon/rustful',
updated_at: '2015-09-19T21:10:27Z',
- versionIds: [],
},
{
created_at: '2014-11-24T02:34:44Z',
@@ -229,7 +212,6 @@ export default [
name: 'postgres',
repository: 'https://github.com/sfackler/rust-postgres',
updated_at: '2015-11-08T00:48:59Z',
- versionIds: [],
},
{
created_at: '2014-11-21T00:20:47Z',
@@ -243,7 +225,6 @@ export default [
name: 'quickcheck',
repository: 'https://github.com/BurntSushi/quickcheck',
updated_at: '2015-09-20T21:53:38Z',
- versionIds: [],
},
{
created_at: '2014-11-21T00:21:04Z',
@@ -257,7 +238,6 @@ export default [
name: 'quickcheck_macros',
repository: 'https://github.com/BurntSushi/quickcheck',
updated_at: '2015-09-20T21:53:57Z',
- versionIds: [],
},
{
created_at: '2015-08-25T19:15:35Z',
@@ -272,7 +252,6 @@ export default [
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',
@@ -286,7 +265,6 @@ export default [
name: 'nom',
repository: 'https://github.com/Geal/nom',
updated_at: '2015-11-22T22:00:41Z',
- versionIds: [],
},
{
id: 'libc',
diff --git a/mirage/fixtures/dependencies.js b/packages/crates-io-msw/fixtures/dependencies.js
similarity index 100%
rename from mirage/fixtures/dependencies.js
rename to packages/crates-io-msw/fixtures/dependencies.js
diff --git a/mirage/fixtures/keywords.js b/packages/crates-io-msw/fixtures/keywords.js
similarity index 100%
rename from mirage/fixtures/keywords.js
rename to packages/crates-io-msw/fixtures/keywords.js
diff --git a/mirage/fixtures/teams.js b/packages/crates-io-msw/fixtures/teams.js
similarity index 100%
rename from mirage/fixtures/teams.js
rename to packages/crates-io-msw/fixtures/teams.js
diff --git a/mirage/fixtures/users.js b/packages/crates-io-msw/fixtures/users.js
similarity index 100%
rename from mirage/fixtures/users.js
rename to packages/crates-io-msw/fixtures/users.js
diff --git a/mirage/fixtures/version-downloads.js b/packages/crates-io-msw/fixtures/version-downloads.js
similarity index 100%
rename from mirage/fixtures/version-downloads.js
rename to packages/crates-io-msw/fixtures/version-downloads.js
diff --git a/mirage/fixtures/versions.js b/packages/crates-io-msw/fixtures/versions.js
similarity index 100%
rename from mirage/fixtures/versions.js
rename to packages/crates-io-msw/fixtures/versions.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..ac9e93265cf
--- /dev/null
+++ b/packages/crates-io-msw/handlers/api-tokens.js
@@ -0,0 +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, getToken, deleteToken];
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/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' }],
+ });
+});
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' }],
+ });
+});
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' }],
+ });
+});
diff --git a/packages/crates-io-msw/handlers/categories.js b/packages/crates-io-msw/handlers/categories.js
new file mode 100644
index 00000000000..afeffe580ac
--- /dev/null
+++ b/packages/crates-io-msw/handlers/categories.js
@@ -0,0 +1,5 @@
+import getCategory from './categories/get.js';
+import listCategorySlugs from './categories/list-slugs.js';
+import listCategories from './categories/list.js';
+
+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/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/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/handlers/crates.js b/packages/crates-io-msw/handlers/crates.js
new file mode 100644
index 00000000000..2c6d0b4dfb5
--- /dev/null
+++ b/packages/crates-io-msw/handlers/crates.js
@@ -0,0 +1,27 @@
+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';
+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';
+import userOwners from './crates/user-owners.js';
+
+export default [
+ listCrates,
+ getCrate,
+ deleteCrate,
+ following,
+ followCrate,
+ unfollowCrate,
+ addOwners,
+ removeOwners,
+ userOwners,
+ teamOwners,
+ reverseDependencies,
+ downloads,
+];
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);
+});
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);
+});
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: [],
+ },
+ });
+});
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]);
+});
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 });
+});
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,
+ });
+});
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/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);
+});
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);
+});
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',
+ },
+ ],
+ });
+});
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, []);
+});
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',
+ },
+ ],
+ });
+});
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/handlers/invites.js b/packages/crates-io-msw/handlers/invites.js
new file mode 100644
index 00000000000..b8cb2e45de6
--- /dev/null
+++ b/packages/crates-io-msw/handlers/invites.js
@@ -0,0 +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, redeemByToken];
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' }],
+ });
+});
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/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' }],
+ });
+});
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);
+});
diff --git a/packages/crates-io-msw/handlers/keywords.js b/packages/crates-io-msw/handlers/keywords.js
new file mode 100644
index 00000000000..2e668293399
--- /dev/null
+++ b/packages/crates-io-msw/handlers/keywords.js
@@ -0,0 +1,4 @@
+import getKeyword from './keywords/get.js';
+import listKeywords from './keywords/list.js';
+
+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',
+ },
+ });
+});
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/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/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/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/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/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/handlers/users.js b/packages/crates-io-msw/handlers/users.js
new file mode 100644
index 00000000000..51a9a9e05b6
--- /dev/null
+++ b/packages/crates-io-msw/handlers/users.js
@@ -0,0 +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, 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.' }],
+ });
+});
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/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' }],
+ });
+});
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' }] });
+});
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');
+});
diff --git a/packages/crates-io-msw/handlers/versions.js b/packages/crates-io-msw/handlers/versions.js
new file mode 100644
index 00000000000..1eb9c05751f
--- /dev/null
+++ b/packages/crates-io-msw/handlers/versions.js
@@ -0,0 +1,21 @@
+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';
+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,
+ followUpdates,
+];
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,
+ },
+ ],
+ });
+});
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,
+ },
+ ],
+ });
+});
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 });
+});
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,
+ },
+ });
+});
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/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);
+});
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);
+});
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);
+});
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);
+});
diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js
new file mode 100644
index 00000000000..8f320f61f54
--- /dev/null
+++ b/packages/crates-io-msw/index.js
@@ -0,0 +1,57 @@
+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';
+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';
+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';
+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';
+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';
+
+export const handlers = [
+ ...apiTokenHandlers,
+ ...categoryHandlers,
+ ...cratesHandlers,
+ ...docsRsHandlers,
+ ...inviteHandlers,
+ ...keywordHandlers,
+ ...metadataHandlers,
+ ...playgroundHandlers,
+ ...sessionHandlers,
+ ...summaryHandlers,
+ ...teamHandlers,
+ ...userHandlers,
+ ...versionHandlers,
+];
+
+export const db = factory({
+ apiToken,
+ category,
+ crateOwnerInvitation,
+ crateOwnership,
+ crate,
+ dependency,
+ keyword,
+ mswSession,
+ team,
+ user,
+ versionDownload,
+ version,
+});
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/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/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",
+ }
+ `);
+});
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",
+ }
+ `);
+});
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",
+ }
+ `);
+});
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",
+ }
+ `);
+});
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",
+ }
+ `);
+});
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",
+ }
+ `);
+});
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",
+ }
+ `);
+});
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",
+ }
+ `);
+});
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",
+ }
+ `);
+});
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",
+ }
+ `);
+});
diff --git a/packages/crates-io-msw/package.json b/packages/crates-io-msw/package.json
new file mode 100644
index 00000000000..8aa6ba40433
--- /dev/null
+++ b/packages/crates-io-msw/package.json
@@ -0,0 +1,29 @@
+{
+ "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": {
+ "@mswjs/data": "^0.16.2",
+ "msw": "^2.7.0",
+ "semver": "^7.6.3"
+ },
+ "devDependencies": {
+ "happy-dom": "16.5.3",
+ "vitest": "3.0.1"
+ }
+}
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;
+}
diff --git a/packages/crates-io-msw/serializers/category.js b/packages/crates-io-msw/serializers/category.js
new file mode 100644
index 00000000000..5960b99038f
--- /dev/null
+++ b/packages/crates-io-msw/serializers/category.js
@@ -0,0 +1,18 @@
+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;
+}
+
+export function serializeCategorySlug(category) {
+ return {
+ id: category.id,
+ slug: category.slug,
+ description: category.description,
+ };
+}
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/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;
+}
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;
+}
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;
+}
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;
+}
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;
+}
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;
+}
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/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();
+ }
+}
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..c8de6302ede
--- /dev/null
+++ b/packages/crates-io-msw/utils/factory.d.ts
@@ -0,0 +1,7 @@
+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
new file mode 100644
index 00000000000..1c7fb416591
--- /dev/null
+++ b/packages/crates-io-msw/utils/factory.js
@@ -0,0 +1,53 @@
+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 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
+ // 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.
+ 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];
+
+ // Add a counter to each model.
+ modelApi.counter = 0;
+
+ modelApi.mswCreate = modelApi.create;
+ modelApi.create = function (attrs = {}) {
+ preCreate(attrs, ++modelApi.counter);
+ return modelApi.mswCreate(attrs);
+ };
+ }
+
+ 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/utils/handlers.js b/packages/crates-io-msw/utils/handlers.js
new file mode 100644
index 00000000000..f803055a0e4
--- /dev/null
+++ b/packages/crates-io-msw/utils/handlers.js
@@ -0,0 +1,20 @@
+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);
+
+ 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 };
+}
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;
+ };
+}
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;
+}
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;
+}
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 };
+}
diff --git a/packages/crates-io-msw/utils/strings.js b/packages/crates-io-msw/utils/strings.js
new file mode 100644
index 00000000000..23de13c0237
--- /dev/null
+++ b/packages/crates-io-msw/utils/strings.js
@@ -0,0 +1,13 @@
+export function dasherize(str) {
+ return str
+ .replace(/([a-z\d])([A-Z])/g, '$1_$2')
+ .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
new file mode 100644
index 00000000000..a48491f5e0d
--- /dev/null
+++ b/packages/crates-io-msw/utils/strings.test.js
@@ -0,0 +1,36 @@
+import { describe, test } from 'vitest';
+
+import { dasherize, underscore } 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');
+});
+
+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');
+});
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..c40a943a5e7
--- /dev/null
+++ b/packages/crates-io-msw/vitest.setup.js
@@ -0,0 +1,11 @@
+import { setupServer } from 'msw/node';
+import { afterAll, afterEach, beforeAll } from 'vitest';
+
+import { db, handlers } from './index.js';
+
+const server = setupServer(...handlers);
+
+beforeAll(() => server.listen());
+afterEach(() => server.resetHandlers());
+afterEach(() => db.reset());
+afterAll(() => server.close());
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 */
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9bda5fa65b1..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:
@@ -55,6 +54,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
@@ -118,6 +120,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)
@@ -151,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))
@@ -184,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))
@@ -268,15 +273,18 @@ 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)
normalize.css:
specifier: 8.0.1
version: 8.0.1
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)
@@ -302,6 +310,25 @@ importers:
specifier: 5.97.1
version: 5.97.1
+ 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)
+ semver:
+ specifier: ^7.6.3
+ version: 7.7.0
+ 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 +918,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 +1400,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 +1701,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'}
@@ -1581,13 +1771,21 @@ 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'}
+ '@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==}
+
+ '@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 +1805,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 +1914,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 +2082,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==}
@@ -2026,6 +2331,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==}
@@ -2044,6 +2355,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==}
@@ -2071,15 +2385,24 @@ 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==}
+ '@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==}
@@ -2089,6 +2412,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 +2808,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 +3307,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 +3367,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'}
@@ -3026,6 +3390,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==}
@@ -3033,6 +3400,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 +3808,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'}
@@ -3502,6 +3877,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'}
@@ -3779,6 +4157,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==}
@@ -3843,6 +4225,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==}
@@ -4106,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==}
@@ -4264,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}
@@ -4491,6 +4853,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 +5018,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 +5077,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'}
@@ -4736,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==}
@@ -5198,6 +5569,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 +5588,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 +5667,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==}
@@ -5440,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'}
@@ -5630,6 +6009,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 +6487,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 +6508,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'}
@@ -6184,6 +6572,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==}
@@ -6327,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'}
@@ -6374,6 +6761,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 +6785,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 +7024,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 +7230,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'}
@@ -6875,6 +7283,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'}
@@ -7098,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'}
@@ -7202,7 +7614,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 +7897,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 +8119,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 +8266,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 +8285,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 +8300,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 +8535,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 +8683,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 +8882,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 +9081,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 +9128,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 +10058,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 +10731,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 +11084,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
@@ -10552,13 +11175,44 @@ 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
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
+ '@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
+ '@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 +11231,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 +11416,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 +11566,8 @@ snapshots:
dependencies:
'@types/node': 22.10.10
+ '@types/cookie@0.6.0': {}
+
'@types/cors@2.8.17':
dependencies:
'@types/node': 22.10.10
@@ -11241,6 +11963,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': {}
@@ -11255,6 +11981,8 @@ snapshots:
'@types/normalize-package-data@2.4.4': {}
+ '@types/pluralize@0.0.29': {}
+
'@types/q@1.5.8': {}
'@types/qs@6.9.18': {}
@@ -11283,13 +12011,19 @@ 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
+ '@types/uuid@8.3.4': {}
+
'@types/yargs-parser@21.0.3': {}
'@types/yargs@17.0.33':
@@ -11301,6 +12035,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 +12491,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 +13406,8 @@ snapshots:
bytes@3.1.2: {}
+ cac@6.7.14: {}
+
cacache@12.0.4:
dependencies:
bluebird: 3.7.2
@@ -12715,6 +13494,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
@@ -12738,6 +13525,8 @@ snapshots:
chardet@0.7.0: {}
+ charenc@0.0.2: {}
+
charm@1.0.2:
dependencies:
inherits: 2.0.4
@@ -12746,6 +13535,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 +13817,8 @@ snapshots:
cookie@0.7.1: {}
+ cookie@0.7.2: {}
+
cookie@1.0.2: {}
copy-concurrently@1.0.5:
@@ -13111,6 +13904,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
@@ -13441,6 +14236,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: {}
@@ -13489,6 +14288,8 @@ snapshots:
transitivePeerDependencies:
- '@babel/core'
+ deep-eql@5.0.2: {}
+
deep-is@0.1.4: {}
default-require-extensions@3.0.1:
@@ -13967,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
@@ -14410,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)
@@ -14870,6 +15640,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 +15849,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 +15946,8 @@ snapshots:
dependencies:
homedir-polyfill: 1.0.3
+ expect-type@1.1.0: {}
+
express@4.21.2:
dependencies:
accepts: 1.3.8
@@ -15222,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: {}
@@ -15831,6 +16631,8 @@ snapshots:
graceful-fs@4.2.11: {}
+ graphql@16.10.0: {}
+
growly@1.3.0: {}
gzip-size@6.0.0:
@@ -15848,6 +16650,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 +16736,8 @@ snapshots:
dependencies:
function-bind: 1.1.2
+ headers-polyfill@4.0.3: {}
+
heimdalljs-fs-monitor@1.1.1:
dependencies:
callsites: 3.1.0
@@ -16100,8 +16909,6 @@ snapshots:
infer-owner@1.0.4: {}
- inflected@2.1.0: {}
-
inflection@2.0.1: {}
inflection@3.0.2: {}
@@ -16309,6 +17116,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 +17629,8 @@ snapshots:
dependencies:
js-tokens: 4.0.0
+ loupe@3.1.2: {}
+
lower-case@2.0.2:
dependencies:
tslib: 2.8.1
@@ -16840,6 +17651,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
@@ -16906,6 +17721,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: {}
@@ -17065,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
@@ -17130,6 +17944,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 +17977,8 @@ snapshots:
mute-stream@1.0.0: {}
+ mute-stream@2.0.0: {}
+
nan@2.22.0:
optional: true
@@ -17454,6 +18295,8 @@ snapshots:
os-tmpdir@1.0.2: {}
+ outvariant@1.4.3: {}
+
own-keys@1.0.1:
dependencies:
get-intrinsic: 1.2.7
@@ -17621,6 +18464,8 @@ snapshots:
pathe@2.0.2: {}
+ pathval@2.0.0: {}
+
pbkdf2@3.1.2:
dependencies:
create-hash: 1.2.0
@@ -17667,6 +18512,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
@@ -17944,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
@@ -18362,6 +19209,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 +19500,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 +19680,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 +19697,8 @@ snapshots:
statuses@2.0.1: {}
+ std-env@3.8.0: {}
+
stream-browserify@2.0.2:
dependencies:
inherits: 2.0.4
@@ -18841,6 +19719,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 +20082,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 +20220,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 +20415,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 +20671,8 @@ snapshots:
whatwg-mimetype@2.3.0: {}
+ whatwg-mimetype@3.0.0: {}
+
whatwg-mimetype@4.0.0: {}
whatwg-url@14.1.0:
@@ -19784,6 +20741,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/*'
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');
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
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');
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);
});
});
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');
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]');
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');
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');
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');
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');
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', () => {
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');
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);
});
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]');
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');
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');
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(), '/');
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');
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
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();
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');
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(), '/');
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');
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');
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]`);
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);
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"]');
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 };
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');
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}`);
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');
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');
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}`]);
});
});
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;
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) {
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);
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);
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);
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');
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');
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();
diff --git a/tests/helpers/index.js b/tests/helpers/index.js
index b33c551cdb2..c9e14ebba6a 100644
--- a/tests/helpers/index.js
+++ b/tests/helpers/index.js
@@ -1,15 +1,20 @@
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
-export function setupApplicationTest(hooks, options) {
+export function setupApplicationTest(hooks, options = {}) {
+ let { msw } = options;
+
upstreamSetupApplicationTest(hooks, options);
- setupMirage(hooks);
+
+ if (msw) {
+ setupMSW(hooks);
+ }
+
setupSentryMock(hooks);
setupAppTestDataAttr(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/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');
+ };
+ });
+}
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');
- });
-});
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);
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();
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);
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('');
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");
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');
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');
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');
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`);
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');
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-*'],
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');
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');
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');
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);
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) {