Skip to content

Commit a61ecf2

Browse files
Upgrade to MSW 2.0 (#1809)
* Bump dependencies * Update msw dependency * Fix MSW related type errors in generation * Pull cookies from correct place * Bot commit: format with prettier * Reduce diff w/ json export from generated code * Don't cache response object * Wrap response returns in functions * Remove stay console.log * Fix type error w/ NotImplemented * Ensure Response isn't constructed w/ body when body should be null --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 50f3a5b commit a61ecf2

File tree

12 files changed

+1455
-786
lines changed

12 files changed

+1455
-786
lines changed

app/msw-mock-api.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,22 @@ const sleep = async (ms: number) => new Promise((res) => setTimeout(res, ms))
5757
export async function startMockAPI() {
5858
// dynamic imports to make extremely sure none of this code ends up in the prod bundle
5959
const { handlers } = await import('@oxide/api-mocks')
60-
const { setupWorker, rest, compose } = await import('msw')
60+
const { http, HttpResponse } = await import('msw')
61+
const { setupWorker } = await import('msw/browser')
6162

6263
// defined in here because it depends on the dynamic import
63-
const interceptAll = rest.all('/v1/*', async (_req, res, ctx) => {
64+
const interceptAll = http.all('/v1/*', async () => {
6465
// random delay on all requests to simulate a real API
6566
await sleep(randInt(200, 400))
6667

6768
if (shouldFail(chaos)) {
6869
// special header lets client indicate chaos failures so we don't get confused
69-
return res(compose(ctx.status(randomStatus()), ctx.set('X-Chaos', '')))
70+
return new HttpResponse(null, {
71+
status: randomStatus(),
72+
headers: {
73+
'X-Chaos': '',
74+
},
75+
})
7076
}
7177
// don't return anything means fall through to the real handlers
7278
})
@@ -77,7 +83,7 @@ export async function startMockAPI() {
7783
// custom handler only to make logging less noisy. unhandled requests still
7884
// pass through to the server
7985
onUnhandledRequest(req) {
80-
const path = req.url.pathname
86+
const path = new URL(req.url).pathname
8187
const ignore = [
8288
path.includes('libs/ui/assets'), // assets obviously loaded from file system
8389
path.startsWith('/forms/'), // lazy loaded forms
@@ -86,7 +92,7 @@ export async function startMockAPI() {
8692
// message format copied from MSW source
8793
console.warn(`[MSW] Warning: captured an API request without a matching request handler:
8894
89-
${req.method} ${req.url.pathname}
95+
${req.method} ${path}
9096
9197
If you want to intercept this unhandled request, create a request handler for it.`)
9298
}

app/test/unit/server.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { rest } from 'msw'
8+
import { http, HttpResponse } from 'msw'
99
import { setupServer } from 'msw/node'
1010

1111
import { handlers } from '@oxide/api-mocks'
@@ -20,18 +20,20 @@ export const server = setupServer(
2020

2121
// Override request handlers in order to test special cases
2222
export function overrideOnce(
23-
method: keyof typeof rest,
23+
method: keyof typeof http,
2424
path: string,
2525
status: number,
2626
body: string | Record<string, unknown>
2727
) {
2828
server.use(
29-
rest[method](path, (_req, res, ctx) =>
30-
// https://mswjs.io/docs/api/response/once
31-
res.once(
32-
ctx.status(status),
33-
typeof body === 'string' ? ctx.text(body) : ctx.json(body)
34-
)
29+
http[method](
30+
path,
31+
() =>
32+
// https://mswjs.io/docs/api/response/once
33+
typeof body === 'string'
34+
? new HttpResponse(body, { status })
35+
: HttpResponse.json(body, { status }),
36+
{ once: true }
3537
)
3638
)
3739
}

libs/api-mocks/msw/db.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import { json } from './util'
1818

1919
const notFoundBody = { error_code: 'ObjectNotFound' } as const
2020
export type NotFound = typeof notFoundBody
21-
export const notFoundErr = json({ error_code: 'ObjectNotFound' } as const, { status: 404 })
21+
export const notFoundErr = () =>
22+
json({ error_code: 'ObjectNotFound' } as const, { status: 404 })
2223

2324
export const lookupById = <T extends { id: string }>(table: T[], id: string) => {
2425
const item = table.find((i) => i.id === id)

libs/api-mocks/msw/handlers.ts

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8+
import { delay } from 'msw'
89
import { v4 as uuid } from 'uuid'
910

1011
import {
@@ -256,10 +257,10 @@ export const handlers = makeHandlers({
256257
return json(newImage, { status: 201 })
257258
},
258259
imageView: ({ path, query }) => lookup.image({ ...path, ...query }),
259-
imageDelete({ path, query, req }) {
260+
imageDelete({ path, query, cookies }) {
260261
// if it's a silo image, you need silo write to delete it
261262
if (!query.project) {
262-
requireRole(req, 'silo', defaultSilo.id, 'collaborator')
263+
requireRole(cookies, 'silo', defaultSilo.id, 'collaborator')
263264
}
264265

265266
const image = lookup.image({ ...path, ...query })
@@ -532,9 +533,9 @@ export const handlers = makeHandlers({
532533

533534
return json(instance, { status: 202 })
534535
},
535-
instanceSerialConsole(_params) {
536-
// TODO: Add support for params
537-
return json(serial, { delay: 3000 })
536+
async instanceSerialConsole(_params) {
537+
await delay(3000)
538+
return json(serial)
538539
},
539540
instanceStart({ path, query }) {
540541
const instance = lookup.instance({ ...path, ...query })
@@ -831,14 +832,14 @@ export const handlers = makeHandlers({
831832
const nics = db.networkInterfaces.filter((n) => n.subnet_id === subnet.id)
832833
return paginated(query, nics)
833834
},
834-
sledPhysicalDiskList({ path, query, req }) {
835-
requireFleetViewer(req)
835+
sledPhysicalDiskList({ path, query, cookies }) {
836+
requireFleetViewer(cookies)
836837
const sled = lookup.sled(path)
837838
const disks = db.physicalDisks.filter((n) => n.sled_id === sled.id)
838839
return paginated(query, disks)
839840
},
840-
physicalDiskList({ query, req }) {
841-
requireFleetViewer(req)
841+
physicalDiskList({ query, cookies }) {
842+
requireFleetViewer(cookies)
842843
return paginated(query, db.physicalDisks)
843844
},
844845
policyView() {
@@ -866,27 +867,27 @@ export const handlers = makeHandlers({
866867

867868
return body
868869
},
869-
rackList: ({ query, req }) => {
870-
requireFleetViewer(req)
870+
rackList: ({ query, cookies }) => {
871+
requireFleetViewer(cookies)
871872
return paginated(query, db.racks)
872873
},
873-
currentUserView({ req }) {
874-
return { ...currentUser(req), silo_name: defaultSilo.name }
874+
currentUserView({ cookies }) {
875+
return { ...currentUser(cookies), silo_name: defaultSilo.name }
875876
},
876-
currentUserGroups({ req }) {
877-
const user = currentUser(req)
877+
currentUserGroups({ cookies }) {
878+
const user = currentUser(cookies)
878879
const memberships = db.groupMemberships.filter((gm) => gm.userId === user.id)
879880
const groupIds = new Set(memberships.map((gm) => gm.groupId))
880881
const groups = db.userGroups.filter((g) => groupIds.has(g.id))
881882
return { items: groups }
882883
},
883-
currentUserSshKeyList({ query, req }) {
884-
const user = currentUser(req)
884+
currentUserSshKeyList({ query, cookies }) {
885+
const user = currentUser(cookies)
885886
const keys = db.sshKeys.filter((k) => k.silo_user_id === user.id)
886887
return paginated(query, keys)
887888
},
888-
currentUserSshKeyCreate({ body, req }) {
889-
const user = currentUser(req)
889+
currentUserSshKeyCreate({ body, cookies }) {
890+
const user = currentUser(cookies)
890891
errIfExists(db.sshKeys, { silo_user_id: user.id, name: body.name })
891892

892893
const newSshKey: Json<Api.SshKey> = {
@@ -904,16 +905,16 @@ export const handlers = makeHandlers({
904905
db.sshKeys = db.sshKeys.filter((i) => i.id !== sshKey.id)
905906
return 204
906907
},
907-
sledView({ path, req }) {
908-
requireFleetViewer(req)
908+
sledView({ path, cookies }) {
909+
requireFleetViewer(cookies)
909910
return lookup.sled(path)
910911
},
911-
sledList({ query, req }) {
912-
requireFleetViewer(req)
912+
sledList({ query, cookies }) {
913+
requireFleetViewer(cookies)
913914
return paginated(query, db.sleds)
914915
},
915-
sledInstanceList({ query, path, req }) {
916-
requireFleetViewer(req)
916+
sledInstanceList({ query, path, cookies }) {
917+
requireFleetViewer(cookies)
917918
const sled = lookupById(db.sleds, path.sledId)
918919
return paginated(
919920
query,
@@ -929,12 +930,12 @@ export const handlers = makeHandlers({
929930
})
930931
)
931932
},
932-
siloList({ query, req }) {
933-
requireFleetViewer(req)
933+
siloList({ query, cookies }) {
934+
requireFleetViewer(cookies)
934935
return paginated(query, db.silos)
935936
},
936-
siloCreate({ body, req }) {
937-
requireFleetViewer(req)
937+
siloCreate({ body, cookies }) {
938+
requireFleetViewer(cookies)
938939
errIfExists(db.silos, { name: body.name })
939940
const newSilo: Json<Api.Silo> = {
940941
id: uuid(),
@@ -945,25 +946,25 @@ export const handlers = makeHandlers({
945946
db.silos.push(newSilo)
946947
return json(newSilo, { status: 201 })
947948
},
948-
siloView({ path, req }) {
949-
requireFleetViewer(req)
949+
siloView({ path, cookies }) {
950+
requireFleetViewer(cookies)
950951
return lookup.silo(path)
951952
},
952-
siloDelete({ path, req }) {
953-
requireFleetViewer(req)
953+
siloDelete({ path, cookies }) {
954+
requireFleetViewer(cookies)
954955
const silo = lookup.silo(path)
955956
db.silos = db.silos.filter((i) => i.id !== silo.id)
956957
return 204
957958
},
958-
siloIdentityProviderList({ query, req }) {
959-
requireFleetViewer(req)
959+
siloIdentityProviderList({ query, cookies }) {
960+
requireFleetViewer(cookies)
960961
const silo = lookup.silo(query)
961962
const idps = db.identityProviders.filter(({ siloId }) => siloId === silo.id).map(toIdp)
962963
return { items: idps }
963964
},
964965

965-
samlIdentityProviderCreate({ query, body, req }) {
966-
requireFleetViewer(req)
966+
samlIdentityProviderCreate({ query, body, cookies }) {
967+
requireFleetViewer(cookies)
967968
const silo = lookup.silo(query)
968969

969970
// this is a bit silly, but errIfExists doesn't handle nested keys like
@@ -1019,8 +1020,8 @@ export const handlers = makeHandlers({
10191020
return paginated(query, db.users)
10201021
},
10211022

1022-
systemPolicyView({ req }) {
1023-
requireFleetViewer(req)
1023+
systemPolicyView({ cookies }) {
1024+
requireFleetViewer(cookies)
10241025

10251026
const role_assignments = db.roleAssignments
10261027
.filter((r) => r.resource_type === 'fleet' && r.resource_id === FLEET_ID)
@@ -1029,7 +1030,7 @@ export const handlers = makeHandlers({
10291030
return { role_assignments }
10301031
},
10311032
systemMetric(params) {
1032-
requireFleetViewer(params.req)
1033+
requireFleetViewer(params.cookies)
10331034
return handleMetrics(params)
10341035
},
10351036
siloMetric: handleMetrics,

libs/api-mocks/msw/util.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* Copyright Oxide Computer Company
77
*/
88
import { differenceInSeconds, subHours } from 'date-fns'
9-
import type { RestRequest } from 'msw'
109

1110
import {
1211
FLEET_ID,
@@ -88,9 +87,12 @@ export function getTimestamps() {
8887
return { time_created: now, time_modified: now }
8988
}
9089

91-
export const unavailableErr = json({ error_code: 'ServiceUnavailable' }, { status: 503 })
90+
export const unavailableErr = () =>
91+
json({ error_code: 'ServiceUnavailable' }, { status: 503 })
9292

9393
export const NotImplemented = () => {
94+
// This doesn't just return the response because it broadens the type to be usable
95+
// directly as a handler
9496
throw json({ error_code: 'NotImplemented' }, { status: 501 })
9597
}
9698

@@ -292,8 +294,8 @@ export const MSW_USER_COOKIE = 'msw-user'
292294
* If cookie is empty or name is not found, return the first user in the list,
293295
* who has admin on everything.
294296
*/
295-
export function currentUser(req: RestRequest): Json<User> {
296-
const name = req.cookies[MSW_USER_COOKIE]
297+
export function currentUser(cookies: Record<string, string>): Json<User> {
298+
const name = cookies[MSW_USER_COOKIE]
297299
return db.users.find((u) => u.display_name === name) ?? db.users[0]
298300
}
299301

@@ -347,8 +349,8 @@ export function userHasRole(
347349
* fleet roles for the user as well as for the user's groups. Do nothing if yes,
348350
* throw 403 if no.
349351
*/
350-
export function requireFleetViewer(req: RestRequest) {
351-
requireRole(req, 'fleet', FLEET_ID, 'viewer')
352+
export function requireFleetViewer(cookies: Record<string, string>) {
353+
requireRole(cookies, 'fleet', FLEET_ID, 'viewer')
352354
}
353355

354356
/**
@@ -357,12 +359,12 @@ export function requireFleetViewer(req: RestRequest) {
357359
* if no.
358360
*/
359361
export function requireRole(
360-
req: RestRequest,
362+
cookies: Record<string, string>,
361363
resourceType: DbRoleAssignmentResourceType,
362364
resourceId: string,
363365
role: RoleKey
364366
) {
365-
const user = currentUser(req)
367+
const user = currentUser(cookies)
366368
// should it 404? I think the API is a mix
367369
if (!userHasRole(user, resourceType, resourceId, role)) throw 403
368370
}

libs/api/__generated__/Api.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/api/__generated__/http-client.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)