Skip to content

Commit b5c866c

Browse files
authored
msw: Implement relevant Trusted Publishing endpoints (#11381)
This commit implements the three `/api/v1/trusted_publishing/github_configs` endpoints (list, create, delete). It adds a dedicated `trustpubGithubConfig` model and corresponding serializer and then uses them to implement the endpoints. As usual with msw, this is a relatively simple implementation, but good enough for our frontend test suite.
1 parent f26e157 commit b5c866c

File tree

11 files changed

+645
-0
lines changed

11 files changed

+645
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import gitHubConfigs from './trustpub/github-configs.js';
2+
3+
export default [...gitHubConfigs];
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import createGitHubConfig from './github-configs/create.js';
2+
import deleteGitHubConfig from './github-configs/delete.js';
3+
import listGitHubConfigs from './github-configs/list.js';
4+
5+
export default [listGitHubConfigs, createGitHubConfig, deleteGitHubConfig];
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { http, HttpResponse } from 'msw';
2+
3+
import { db } from '../../../index.js';
4+
import { serializeGitHubConfig } from '../../../serializers/trustpub/github-config.js';
5+
import { notFound } from '../../../utils/handlers.js';
6+
import { getSession } from '../../../utils/session.js';
7+
8+
export default http.post('/api/v1/trusted_publishing/github_configs', async ({ request }) => {
9+
let { user } = getSession();
10+
if (!user) {
11+
return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 });
12+
}
13+
14+
let body = await request.json();
15+
16+
let { github_config } = body;
17+
if (!github_config) {
18+
return HttpResponse.json({ errors: [{ detail: 'invalid request body' }] }, { status: 400 });
19+
}
20+
21+
let { crate: crateName, repository_owner, repository_name, workflow_filename, environment } = github_config;
22+
if (!crateName || !repository_owner || !repository_name || !workflow_filename) {
23+
return HttpResponse.json({ errors: [{ detail: 'missing required fields' }] }, { status: 400 });
24+
}
25+
26+
let crate = db.crate.findFirst({ where: { name: { equals: crateName } } });
27+
if (!crate) return notFound();
28+
29+
// Check if the user is an owner of the crate
30+
let isOwner = db.crateOwnership.findFirst({
31+
where: {
32+
crate: { id: { equals: crate.id } },
33+
user: { id: { equals: user.id } },
34+
},
35+
});
36+
if (!isOwner) {
37+
return HttpResponse.json({ errors: [{ detail: 'You are not an owner of this crate' }] }, { status: 400 });
38+
}
39+
40+
// Check if the user has a verified email
41+
let hasVerifiedEmail = user.emailVerified;
42+
if (!hasVerifiedEmail) {
43+
let detail = 'You must verify your email address to create a Trusted Publishing config';
44+
return HttpResponse.json({ errors: [{ detail }] }, { status: 403 });
45+
}
46+
47+
// Create a new GitHub config
48+
let config = db.trustpubGithubConfig.create({
49+
crate,
50+
repository_owner,
51+
repository_name,
52+
workflow_filename,
53+
environment: environment ?? null,
54+
created_at: new Date().toISOString(),
55+
});
56+
57+
return HttpResponse.json({
58+
github_config: serializeGitHubConfig(config),
59+
});
60+
});
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import { afterEach, assert, beforeEach, test, vi } from 'vitest';
2+
3+
import { db } from '../../../index.js';
4+
5+
beforeEach(() => {
6+
vi.useFakeTimers();
7+
});
8+
9+
afterEach(() => {
10+
vi.restoreAllMocks();
11+
});
12+
13+
test('happy path', async function () {
14+
vi.setSystemTime(new Date('2023-01-01T00:00:00Z'));
15+
16+
let crate = db.crate.create({ name: 'test-crate' });
17+
db.version.create({ crate });
18+
19+
let user = db.user.create({ emailVerified: true });
20+
db.mswSession.create({ user });
21+
22+
// Create crate ownership
23+
db.crateOwnership.create({
24+
crate,
25+
user,
26+
});
27+
28+
let response = await fetch('/api/v1/trusted_publishing/github_configs', {
29+
method: 'POST',
30+
body: JSON.stringify({
31+
github_config: {
32+
crate: crate.name,
33+
repository_owner: 'rust-lang',
34+
repository_name: 'crates.io',
35+
workflow_filename: 'ci.yml',
36+
},
37+
}),
38+
});
39+
40+
assert.strictEqual(response.status, 200);
41+
assert.deepEqual(await response.json(), {
42+
github_config: {
43+
id: 1,
44+
crate: crate.name,
45+
repository_owner: 'rust-lang',
46+
repository_owner_id: 5_430_905,
47+
repository_name: 'crates.io',
48+
workflow_filename: 'ci.yml',
49+
environment: null,
50+
created_at: '2023-01-01T00:00:00.000Z',
51+
},
52+
});
53+
});
54+
55+
test('happy path with environment', async function () {
56+
vi.setSystemTime(new Date('2023-02-01T00:00:00Z'));
57+
58+
let crate = db.crate.create({ name: 'test-crate-env' });
59+
db.version.create({ crate });
60+
61+
let user = db.user.create({ emailVerified: true });
62+
db.mswSession.create({ user });
63+
64+
// Create crate ownership
65+
db.crateOwnership.create({
66+
crate,
67+
user,
68+
});
69+
70+
let response = await fetch('/api/v1/trusted_publishing/github_configs', {
71+
method: 'POST',
72+
body: JSON.stringify({
73+
github_config: {
74+
crate: crate.name,
75+
repository_owner: 'rust-lang',
76+
repository_name: 'crates.io',
77+
workflow_filename: 'ci.yml',
78+
environment: 'production',
79+
},
80+
}),
81+
});
82+
83+
assert.strictEqual(response.status, 200);
84+
assert.deepEqual(await response.json(), {
85+
github_config: {
86+
id: 1,
87+
crate: crate.name,
88+
repository_owner: 'rust-lang',
89+
repository_owner_id: 5_430_905,
90+
repository_name: 'crates.io',
91+
workflow_filename: 'ci.yml',
92+
environment: 'production',
93+
created_at: '2023-02-01T00:00:00.000Z',
94+
},
95+
});
96+
});
97+
98+
test('returns 403 if unauthenticated', async function () {
99+
let response = await fetch('/api/v1/trusted_publishing/github_configs', {
100+
method: 'POST',
101+
body: JSON.stringify({
102+
github_config: {
103+
crate: 'test-crate',
104+
repository_owner: 'rust-lang',
105+
repository_name: 'crates.io',
106+
workflow_filename: 'ci.yml',
107+
},
108+
}),
109+
});
110+
111+
assert.strictEqual(response.status, 403);
112+
assert.deepEqual(await response.json(), {
113+
errors: [{ detail: 'must be logged in to perform that action' }],
114+
});
115+
});
116+
117+
test('returns 400 if request body is invalid', async function () {
118+
let user = db.user.create();
119+
db.mswSession.create({ user });
120+
121+
let response = await fetch('/api/v1/trusted_publishing/github_configs', {
122+
method: 'POST',
123+
body: JSON.stringify({}),
124+
});
125+
126+
assert.strictEqual(response.status, 400);
127+
assert.deepEqual(await response.json(), {
128+
errors: [{ detail: 'invalid request body' }],
129+
});
130+
});
131+
132+
test('returns 400 if required fields are missing', async function () {
133+
let user = db.user.create();
134+
db.mswSession.create({ user });
135+
136+
let response = await fetch('/api/v1/trusted_publishing/github_configs', {
137+
method: 'POST',
138+
body: JSON.stringify({
139+
github_config: {
140+
crate: 'test-crate',
141+
},
142+
}),
143+
});
144+
145+
assert.strictEqual(response.status, 400);
146+
assert.deepEqual(await response.json(), {
147+
errors: [{ detail: 'missing required fields' }],
148+
});
149+
});
150+
151+
test("returns 404 if crate can't be found", async function () {
152+
let user = db.user.create();
153+
db.mswSession.create({ user });
154+
155+
let response = await fetch('/api/v1/trusted_publishing/github_configs', {
156+
method: 'POST',
157+
body: JSON.stringify({
158+
github_config: {
159+
crate: 'nonexistent',
160+
repository_owner: 'rust-lang',
161+
repository_name: 'crates.io',
162+
workflow_filename: 'ci.yml',
163+
},
164+
}),
165+
});
166+
167+
assert.strictEqual(response.status, 404);
168+
assert.deepEqual(await response.json(), {
169+
errors: [{ detail: 'Not Found' }],
170+
});
171+
});
172+
173+
test('returns 400 if user is not an owner of the crate', async function () {
174+
let crate = db.crate.create({ name: 'test-crate-not-owner' });
175+
db.version.create({ crate });
176+
177+
let user = db.user.create();
178+
db.mswSession.create({ user });
179+
180+
let response = await fetch('/api/v1/trusted_publishing/github_configs', {
181+
method: 'POST',
182+
body: JSON.stringify({
183+
github_config: {
184+
crate: crate.name,
185+
repository_owner: 'rust-lang',
186+
repository_name: 'crates.io',
187+
workflow_filename: 'ci.yml',
188+
},
189+
}),
190+
});
191+
192+
assert.strictEqual(response.status, 400);
193+
assert.deepEqual(await response.json(), {
194+
errors: [{ detail: 'You are not an owner of this crate' }],
195+
});
196+
});
197+
198+
test('returns 403 if user email is not verified', async function () {
199+
let crate = db.crate.create({ name: 'test-crate-unverified' });
200+
db.version.create({ crate });
201+
202+
let user = db.user.create({ emailVerified: false });
203+
db.mswSession.create({ user });
204+
205+
// Create crate ownership
206+
db.crateOwnership.create({
207+
crate,
208+
user,
209+
});
210+
211+
let response = await fetch('/api/v1/trusted_publishing/github_configs', {
212+
method: 'POST',
213+
body: JSON.stringify({
214+
github_config: {
215+
crate: crate.name,
216+
repository_owner: 'rust-lang',
217+
repository_name: 'crates.io',
218+
workflow_filename: 'ci.yml',
219+
},
220+
}),
221+
});
222+
223+
assert.strictEqual(response.status, 403);
224+
assert.deepEqual(await response.json(), {
225+
errors: [{ detail: 'You must verify your email address to create a Trusted Publishing config' }],
226+
});
227+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { http, HttpResponse } from 'msw';
2+
3+
import { db } from '../../../index.js';
4+
import { notFound } from '../../../utils/handlers.js';
5+
import { getSession } from '../../../utils/session.js';
6+
7+
export default http.delete('/api/v1/trusted_publishing/github_configs/:id', ({ params }) => {
8+
let { user } = getSession();
9+
if (!user) {
10+
return HttpResponse.json({ errors: [{ detail: 'must be logged in to perform that action' }] }, { status: 403 });
11+
}
12+
13+
let id = parseInt(params.id);
14+
let config = db.trustpubGithubConfig.findFirst({ where: { id: { equals: id } } });
15+
if (!config) return notFound();
16+
17+
// Check if the user is an owner of the crate
18+
let isOwner = db.crateOwnership.findFirst({
19+
where: {
20+
crate: { id: { equals: config.crate.id } },
21+
user: { id: { equals: user.id } },
22+
},
23+
});
24+
if (!isOwner) {
25+
return HttpResponse.json({ errors: [{ detail: 'You are not an owner of this crate' }] }, { status: 400 });
26+
}
27+
28+
// Delete the config
29+
db.trustpubGithubConfig.delete({ where: { id: { equals: id } } });
30+
31+
return new HttpResponse(null, { status: 204 });
32+
});

0 commit comments

Comments
 (0)