Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Migrate from ember-cli-mirage to msw and @mswjs/data #10393

Draft
wants to merge 51 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
085493b
git: Ignore all `node_modules` folders
Turbo87 Jan 14, 2025
9c2e2d5
Add basic `@crates-io/msw` package
Turbo87 Jan 14, 2025
21a100d
msw: Add `@mswjs/data` mock database
Turbo87 Jan 14, 2025
669348c
msw: Extract custom `factory()` fn
Turbo87 Jan 15, 2025
186cc47
msw: Add support for `preCreate()` fns
Turbo87 Jan 15, 2025
5512963
msw: Add support for `counter` values to the models
Turbo87 Jan 15, 2025
ba1ce3f
msw: Extract `db.reset()` fn
Turbo87 Jan 16, 2025
a3e2442
msw: Import `dasherize()` fn from `@ember/string`
Turbo87 Jan 15, 2025
67428c6
msw: Implement `user` model
Turbo87 Jan 15, 2025
b006e65
msw: Implement `mswSession` model
Turbo87 Jan 15, 2025
73daa5c
msw: Implement `apiToken` model
Turbo87 Jan 17, 2025
92c21fe
msw: Implement `category` model
Turbo87 Jan 20, 2025
f4e86bd
msw: Implement `keyword` model
Turbo87 Jan 20, 2025
e08fb5c
msw: Implement `team` model
Turbo87 Jan 20, 2025
357e90d
msw: Implement `crate` model
Turbo87 Jan 20, 2025
236abda
msw: Implement `version` model
Turbo87 Jan 20, 2025
6656d22
msw: Implement `versionDownload` model
Turbo87 Jan 20, 2025
69cc3c6
msw: Implement `crateOwnerInvitation` model
Turbo87 Jan 20, 2025
495d462
msw: Implement `crateOwnership` model
Turbo87 Jan 20, 2025
7dd09c2
msw: Implement `dependency` model
Turbo87 Jan 20, 2025
ef0d38e
msw: Import `underscore()` fn from `@ember/string`
Turbo87 Jan 21, 2025
64b911a
msw: Implement basic `serializeModel()` fn
Turbo87 Jan 21, 2025
5846dec
msw: Implement `category` serializer
Turbo87 Jan 21, 2025
b061040
msw: Implement `GET /api/v1/categories` request handler
Turbo87 Jan 21, 2025
859b0f4
msw: Implement `GET /api/v1/category_slugs` request handler
Turbo87 Jan 21, 2025
08440f2
msw: Implement `GET /api/v1/categories/:category_id` request handler
Turbo87 Jan 21, 2025
0fe4bbd
msw: Implement `keyword` serializer
Turbo87 Jan 21, 2025
b45017e
msw: Implement `GET /api/v1/keywords` request handler
Turbo87 Jan 21, 2025
a127677
msw: Implement `GET /api/v1/keywords/:keyword_id` request handler
Turbo87 Jan 21, 2025
1b2132a
msw: Implement `DELETE /api/private/session` request handler
Turbo87 Jan 21, 2025
1c655ce
msw: Implement `GET /api/v1/site_metadata` request handler
Turbo87 Jan 15, 2025
b1ce14f
msw: Implement `team` serializer
Turbo87 Jan 21, 2025
34ecd69
msw: Implement `GET /api/v1/teams/:team_id` request handler
Turbo87 Jan 21, 2025
65c6ce3
msw: Implement `user` serializer
Turbo87 Jan 21, 2025
4b7b7a1
msw: Implement `GET /api/v1/users/:user_id` request handler
Turbo87 Jan 21, 2025
fb83e2a
msw: Implement `getSession()` helper fn
Turbo87 Jan 21, 2025
fbd512b
msw: Implement `PUT /api/v1/users/:id` request handler
Turbo87 Jan 21, 2025
4e6a8fd
msw: Implement `PUT /api/v1/users/:user_id/resend` request handler
Turbo87 Jan 21, 2025
dec032e
msw: Add `removePrivateData` option to `user` serializer
Turbo87 Jan 21, 2025
7ce1a5b
msw: Implement `GET /api/v1/me` request handler
Turbo87 Jan 21, 2025
573d726
msw: Implement docs.rs request handler
Turbo87 Jan 22, 2025
1326e72
msw: Implement `invite` serializer
Turbo87 Jan 22, 2025
c786612
msw: Implement `GET /api/private/crate_owner_invitations` request han…
Turbo87 Jan 22, 2025
ee238e9
msw: Implement `GET /api/v1/me/crate_owner_invitations` request handler
Turbo87 Jan 22, 2025
62b36e9
msw: Implement `PUT /api/v1/me/crate_owner_invitations/:crate_id` req…
Turbo87 Jan 22, 2025
783951b
msw: Implement `PUT /api/v1/me/crate_owner_invitations/accept/:token`…
Turbo87 Jan 22, 2025
9d648ce
msw: Implement `PUT /api/v1/confirm/:token` request handler
Turbo87 Jan 22, 2025
d6375ea
msw: Implement `api-token` serializer
Turbo87 Jan 22, 2025
247095d
msw: Implement `PUT /api/v1/me/tokens` request handler
Turbo87 Jan 22, 2025
1f9d61a
msw: Implement `GET /api/v1/me/tokens` request handler
Turbo87 Jan 22, 2025
6adbf6f
msw: Implement `DELETE /api/v1/me/tokens/:tokenId` request handler
Turbo87 Jan 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ jobs:

- run: pnpm install

- run: pnpm --filter "@crates-io/msw" test

- if: github.repository == 'rust-lang/crates.io'
run: pnpm percy exec --parallel -- pnpm test-coverage

Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/tmp

# dependencies
/node_modules
node_modules/
/bower_components
package-lock.json
yarn.lock
Expand Down
5 changes: 5 additions & 0 deletions packages/crates-io-msw/handlers/api-tokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import createToken from './api-tokens/create.js';
import deleteToken from './api-tokens/delete.js';
import listTokens from './api-tokens/list.js';

export default [createToken, listTokens, deleteToken];
27 changes: 27 additions & 0 deletions packages/crates-io-msw/handlers/api-tokens/create.js
Original file line number Diff line number Diff line change
@@ -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 }),
});
});
110 changes: 110 additions & 0 deletions packages/crates-io-msw/handlers/api-tokens/create.test.js
Original file line number Diff line number Diff line change
@@ -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' }],
});
});
21 changes: 21 additions & 0 deletions packages/crates-io-msw/handlers/api-tokens/delete.js
Original file line number Diff line number Diff line change
@@ -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({});
});
28 changes: 28 additions & 0 deletions packages/crates-io-msw/handlers/api-tokens/delete.test.js
Original file line number Diff line number Diff line change
@@ -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' }],
});
});
30 changes: 30 additions & 0 deletions packages/crates-io-msw/handlers/api-tokens/list.js
Original file line number Diff line number Diff line change
@@ -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)),
});
});
78 changes: 78 additions & 0 deletions packages/crates-io-msw/handlers/api-tokens/list.test.js
Original file line number Diff line number Diff line change
@@ -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:00'));
});

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' }],
});
});
5 changes: 5 additions & 0 deletions packages/crates-io-msw/handlers/categories.js
Original file line number Diff line number Diff line change
@@ -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];
15 changes: 15 additions & 0 deletions packages/crates-io-msw/handlers/categories/get.js
Original file line number Diff line number Diff line change
@@ -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) });
});
49 changes: 49 additions & 0 deletions packages/crates-io-msw/handlers/categories/get.test.js
Original file line number Diff line number Diff line change
@@ -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',
},
});
});
Loading
Loading