From c40788f6fdb972223fae43863a9530fdb3176895 Mon Sep 17 00:00:00 2001 From: Markus Date: Wed, 8 Jan 2025 17:14:34 +0200 Subject: [PATCH 1/6] feat(events-locations): create endpoints for event locations --- packages/events/package.json | 2 + .../__snapshots__/locations.set.test.ts.snap | 17 ++++ .../events/src/router/locations.get.test.ts | 32 +++++++ .../events/src/router/locations.set.test.ts | 89 +++++++++++++++++++ .../src/router/middleware/middleware.ts | 56 ++++++++++++ packages/events/src/router/router.ts | 29 +++--- .../events/src/service/locations/locations.ts | 64 +++++++++++++ packages/events/src/storage/mongodb.ts | 1 + packages/events/src/tests/cert.key | 51 +++++++++++ packages/events/src/tests/generators.ts | 22 ++++- packages/events/src/tests/utils.ts | 21 ++++- 11 files changed, 368 insertions(+), 16 deletions(-) create mode 100644 packages/events/src/router/__snapshots__/locations.set.test.ts.snap create mode 100644 packages/events/src/router/locations.get.test.ts create mode 100644 packages/events/src/router/locations.set.test.ts create mode 100644 packages/events/src/router/middleware/middleware.ts create mode 100644 packages/events/src/service/locations/locations.ts create mode 100644 packages/events/src/tests/cert.key diff --git a/packages/events/package.json b/packages/events/package.json index 41f9cf8333e..19a13e5ccff 100644 --- a/packages/events/package.json +++ b/packages/events/package.json @@ -21,6 +21,7 @@ "@trpc/server": "^11.0.0-rc.532", "app-module-path": "^2.2.0", "envalid": "^8.0.0", + "jsonwebtoken": "^9.0.0", "mongodb": "6.9.0", "superjson": "1.9.0-0", "tsconfig-paths": "^3.13.0", @@ -30,6 +31,7 @@ "@testcontainers/elasticsearch": "^10.15.0", "@typescript-eslint/eslint-plugin": "^4.5.0", "@typescript-eslint/parser": "^4.5.0", + "@types/jsonwebtoken": "^9.0.0", "cross-env": "^7.0.0", "eslint": "^7.11.0", "eslint-config-prettier": "^9.0.0", diff --git a/packages/events/src/router/__snapshots__/locations.set.test.ts.snap b/packages/events/src/router/__snapshots__/locations.set.test.ts.snap new file mode 100644 index 00000000000..e396ef5537a --- /dev/null +++ b/packages/events/src/router/__snapshots__/locations.set.test.ts.snap @@ -0,0 +1,17 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Prevents sending empty payload 1`] = ` +[TRPCError: [ + { + "code": "too_small", + "minimum": 1, + "type": "array", + "inclusive": true, + "exact": false, + "message": "Array must contain at least 1 element(s)", + "path": [] + } +]] +`; + +exports[`prevents unauthorized access 1`] = `[TRPCError: UNAUTHORIZED]`; diff --git a/packages/events/src/router/locations.get.test.ts b/packages/events/src/router/locations.get.test.ts new file mode 100644 index 00000000000..0928c59a43b --- /dev/null +++ b/packages/events/src/router/locations.get.test.ts @@ -0,0 +1,32 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ +import { createTestClient } from '@events/tests/utils' +import { payloadGenerator } from '@events/tests/generators' +import { userScopes } from '@opencrvs/commons' + +const nationalSystemAdminClient = createTestClient([ + userScopes.nationalSystemAdmin +]) +const generator = payloadGenerator() + +test('Returns empty list when no locations are set', async () => { + const fetchedEvents = await nationalSystemAdminClient.locations.get() + + expect(fetchedEvents).toEqual([]) +}) + +test('Returns multiple locations', async () => { + await nationalSystemAdminClient.locations.set(generator.locations.set(5)) + + const events = await nationalSystemAdminClient.locations.get() + + expect(events).toHaveLength(5) +}) diff --git a/packages/events/src/router/locations.set.test.ts b/packages/events/src/router/locations.set.test.ts new file mode 100644 index 00000000000..8a82af5fb94 --- /dev/null +++ b/packages/events/src/router/locations.set.test.ts @@ -0,0 +1,89 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ +import { createTestClient } from '@events/tests/utils' +import { payloadGenerator } from '@events/tests/generators' +import { userScopes } from '@opencrvs/commons' + +const nationalSystemAdminClient = createTestClient([ + userScopes.nationalSystemAdmin +]) + +const registarClient = createTestClient() + +const generator = payloadGenerator() + +test('prevents unauthorized access', async () => { + await expect( + registarClient.locations.set([]) + ).rejects.toThrowErrorMatchingSnapshot() +}) + +test('Prevents sending empty payload', async () => { + await expect( + nationalSystemAdminClient.locations.set([]) + ).rejects.toThrowErrorMatchingSnapshot() +}) + +test('Creates single location', async () => { + const locationPayload = generator.locations.set([ + { id: '123-456-789', partOf: null, name: 'Location foobar' } + ]) + + await nationalSystemAdminClient.locations.set(locationPayload) + + const locations = await nationalSystemAdminClient.locations.get() + + expect(locations).toHaveLength(1) + expect(locations).toMatchObject(locationPayload) +}) + +test('Creates multiple locations', async () => { + const parentId = 'parent-id' + + await nationalSystemAdminClient.locations.set( + generator.locations.set([ + { id: 'parentId' }, + { partOf: parentId }, + { partOf: parentId }, + {} + ]) + ) + + const locations = await nationalSystemAdminClient.locations.get() + + expect(locations).toHaveLength(4) +}) + +test('Removes existing locations not in payload', async () => { + const initialPayload = generator.locations.set(5) + + await nationalSystemAdminClient.locations.set(initialPayload) + + const initialLocations = await nationalSystemAdminClient.locations.get() + expect(initialLocations).toHaveLength(initialPayload.length) + + const [removedLocation, ...remainingLocationsPayload] = initialPayload + + await nationalSystemAdminClient.locations.set(remainingLocationsPayload) + + const remainingLocationsAfterDeletion = + await nationalSystemAdminClient.locations.get() + + expect(remainingLocationsAfterDeletion).toHaveLength( + remainingLocationsPayload.length + ) + + expect( + remainingLocationsAfterDeletion.some( + (location) => location.id === removedLocation.id + ) + ).toBe(false) +}) diff --git a/packages/events/src/router/middleware/middleware.ts b/packages/events/src/router/middleware/middleware.ts new file mode 100644 index 00000000000..b796ae3dcf6 --- /dev/null +++ b/packages/events/src/router/middleware/middleware.ts @@ -0,0 +1,56 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ + +import { inScope, Scope, userScopes } from '@opencrvs/commons' +import { TRPCError, AnyTRPCMiddlewareFunction } from '@trpc/server' + +import { z } from 'zod' + +const ContextSchema = z.object({ + user: z.object({ + id: z.string(), + primaryOfficeId: z.string() + }), + token: z.string() +}) + +export type Context = z.infer + +/** + * TRPC Middleware options with correct context. + * Actual middleware type definition is only for internal use within TRPC. + */ +type MiddlewareOptions = Omit< + Parameters[0], + 'ctx' +> & { ctx: Context } + +/** + * + * @param scopes scopes that are allowed to access the resource + * @returns TRPC compatible middleware function + */ +const createScopeAuthMiddleware = + (scopes: Scope[]) => (opts: MiddlewareOptions) => { + if (inScope({ Authorization: `Bearer ${opts.ctx.token}` }, scopes)) { + return opts.next() + } + + throw new TRPCError({ code: 'UNAUTHORIZED' }) + } + +const isNationalSystemAdminUser = createScopeAuthMiddleware([ + userScopes.nationalSystemAdmin +]) + +export const middleware = { + isNationalSystemAdminUser +} diff --git a/packages/events/src/router/router.ts b/packages/events/src/router/router.ts index 93042988af6..7fa00e7f19f 100644 --- a/packages/events/src/router/router.ts +++ b/packages/events/src/router/router.ts @@ -33,14 +33,12 @@ import { import { EventConfig, getUUID } from '@opencrvs/commons' import { getIndexedEvents } from '@events/service/indexing/indexing' import { presignFilesInEvent } from '@events/service/files' - -const ContextSchema = z.object({ - user: z.object({ - id: z.string(), - primaryOfficeId: z.string() - }), - token: z.string() -}) +import { + getLocations, + Location, + setLocations +} from '@events/service/locations/locations' +import { Context, middleware } from './middleware/middleware' const validateEventType = ({ eventTypes, @@ -59,8 +57,6 @@ const validateEventType = ({ } } -type Context = z.infer - export const t = initTRPC.context().create({ transformer: superjson }) @@ -164,8 +160,15 @@ export const appRouter = router({ }) }), events: router({ - get: publicProcedure.output(z.array(EventIndex)).query(async () => { - return getIndexedEvents() - }) + get: publicProcedure.output(z.array(EventIndex)).query(getIndexedEvents) + }), + locations: router({ + set: publicProcedure + .use(middleware.isNationalSystemAdminUser) + .input(z.array(Location).min(1)) + .mutation(async (options) => { + await setLocations(options.input) + }), + get: publicProcedure.output(z.array(Location)).query(getLocations) }) }) diff --git a/packages/events/src/service/locations/locations.ts b/packages/events/src/service/locations/locations.ts new file mode 100644 index 00000000000..eb8fceb72f0 --- /dev/null +++ b/packages/events/src/service/locations/locations.ts @@ -0,0 +1,64 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ + +import { z } from 'zod' +import * as _ from 'lodash' +import { getClient } from '@events/storage' + +export const Location = z.object({ + id: z.string(), + name: z.string(), + partOf: z.string().nullable() +}) + +export type Location = z.infer + +/** + * Sets incoming locations in the database for events. Should be only run as part of the initial seeding. + * Clears all existing locations that are not in the incoming locations. + * + * @TODO: Consider removing the conditional logic after setting up dev environments for all devs. + * In production it is run only once, and without transactions it is possible that the locations are not set correctly. + * + * @param incomingLocations - Locations to be set + */ +export async function setLocations(incomingLocations: Array) { + const db = await getClient() + const currentLocations = await db.collection('locations').find().toArray() + + const [locationsToKeep, locationsToRemove] = _.partition( + currentLocations, + (location) => + incomingLocations.some( + (incomingLocation) => incomingLocation.id === location.id + ) + ) + + const [, newLocations] = _.partition(incomingLocations, (location) => + locationsToKeep.some((l) => l.id === location.id) + ) + + if (locationsToRemove.length > 0) { + await db + .collection('locations') + .deleteMany({ id: { $in: locationsToRemove.map((l) => l.id) } }) + } + + if (newLocations.length > 0) { + await db.collection('locations').insertMany(newLocations) + } +} + +export const getLocations = async () => { + const { db } = await getClient() + + return db.collection('locations').find().toArray() +} diff --git a/packages/events/src/storage/mongodb.ts b/packages/events/src/storage/mongodb.ts index 8704b24df87..e2cad2c9ccd 100644 --- a/packages/events/src/storage/mongodb.ts +++ b/packages/events/src/storage/mongodb.ts @@ -17,6 +17,7 @@ const client = new MongoClient(url) export async function getClient() { await client.connect() + const db = client.db('events') return db } diff --git a/packages/events/src/tests/cert.key b/packages/events/src/tests/cert.key new file mode 100644 index 00000000000..791dd777536 --- /dev/null +++ b/packages/events/src/tests/cert.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/packages/events/src/tests/generators.ts b/packages/events/src/tests/generators.ts index 14a357cdb3b..8668889bebb 100644 --- a/packages/events/src/tests/generators.ts +++ b/packages/events/src/tests/generators.ts @@ -16,6 +16,7 @@ import { getUUID, ActionType } from '@opencrvs/commons' +import { Location } from '@events/service/locations/locations' /** * @returns a payload generator for creating events and actions with sensible defaults. @@ -53,5 +54,24 @@ export function payloadGenerator() { } } - return { event } + const locations = { + /** Create test data by providing count or desired locations */ + set: (input: Array> | number) => { + if (typeof input === 'number') { + return Array.from({ length: input }).map((_, i) => ({ + id: getUUID(), + name: `Location name ${i}`, + partOf: null + })) + } + + return input.map((location, i) => ({ + id: location.id ?? getUUID(), + name: location.name ?? `Location name ${i}`, + partOf: null + })) + } + } + + return { event, locations } } diff --git a/packages/events/src/tests/utils.ts b/packages/events/src/tests/utils.ts index 44cf4451097..bc240a737b3 100644 --- a/packages/events/src/tests/utils.ts +++ b/packages/events/src/tests/utils.ts @@ -10,14 +10,31 @@ */ import { appRouter, t } from '@events/router' +import * as jwt from 'jsonwebtoken' +import { readFileSync } from 'fs' +import { join } from 'path' +import { Scope, userScopes } from '@opencrvs/commons' const { createCallerFactory } = t -export function createTestClient() { +export function createTestClient(scopes?: Scope[]) { const createCaller = createCallerFactory(appRouter) + const token = createTestToken(scopes) + const caller = createCaller({ user: { id: '1', primaryOfficeId: '123' }, - token: 'NOT_A_REAL_TOKEN' + token }) return caller } + +const createTestToken = (scopes?: Scope[]) => + jwt.sign( + { scope: scopes ?? [userScopes.register, userScopes.declare] }, + readFileSync(join(__dirname, './cert.key')), + { + algorithm: 'RS256', + issuer: 'opencrvs:auth-service', + audience: 'opencrvs:events-user' + } + ) From 18a82f504cc6755407072a899af85317f8abcafd Mon Sep 17 00:00:00 2001 From: Markus Date: Wed, 8 Jan 2025 17:36:32 +0200 Subject: [PATCH 2/6] feat: setup data seeding for events --- packages/data-seeder/src/index.ts | 12 ++++-- packages/data-seeder/src/locations.ts | 37 +++++++++++++++++++ .../__snapshots__/locations.set.test.ts.snap | 2 +- .../events/src/router/locations.set.test.ts | 12 ++++-- .../src/router/middleware/middleware.ts | 11 +++++- .../events/src/service/locations/locations.ts | 2 +- packages/events/src/tests/utils.ts | 4 +- 7 files changed, 69 insertions(+), 11 deletions(-) diff --git a/packages/data-seeder/src/index.ts b/packages/data-seeder/src/index.ts index 279e2fc15c1..e5aa49989a2 100644 --- a/packages/data-seeder/src/index.ts +++ b/packages/data-seeder/src/index.ts @@ -10,7 +10,7 @@ */ import { env } from './environment' import fetch from 'node-fetch' -import { seedLocations } from './locations' +import { seedLocations, seedLocationsForV2Events } from './locations' import { seedRoles } from './roles' import { seedUsers } from './users' import { parseGQLResponse, raise } from './utils' @@ -33,7 +33,9 @@ async function getToken(): Promise { if (!res.ok) { raise('Could not login as the super user') } + const body = await res.json() + console.log('body.token', body.token) return body.token } @@ -85,6 +87,7 @@ async function deactivateSuperuser(token: string) { } }) }) + parseGQLResponse(await res.json()) } @@ -92,9 +95,12 @@ async function main() { const token = await getToken() console.log('Seeding roles') const roleIdMap = await seedRoles(token) - console.log('Seeding locations') + console.log('Seeding locations for v1 system') await seedLocations(token) - console.log('Seeding users') + + console.log('Seeding locations for v2 system (events)') + await seedLocationsForV2Events(token) + await seedUsers(token, roleIdMap) await deactivateSuperuser(token) } diff --git a/packages/data-seeder/src/locations.ts b/packages/data-seeder/src/locations.ts index 98a4237cbb9..c552e7ed3e0 100644 --- a/packages/data-seeder/src/locations.ts +++ b/packages/data-seeder/src/locations.ts @@ -220,6 +220,7 @@ export async function seedLocations(token: string) { ) ) ).flat() + const savedLocationsSet = new Set(savedLocations) const locations = (await getLocations()).filter((location) => { return !savedLocationsSet.has(location.id) @@ -252,3 +253,39 @@ export async function seedLocations(token: string) { } }) } + +function updateLocationPartOf(partOf: string) { + const locationPrefix = 'Location/' + + const parent = partOf.replace(locationPrefix, '') + + if (parent === '0') { + return null + } + + return parent +} + +export async function seedLocationsForV2Events(token: string) { + const locations = await getLocations() + + const simplifiedLocations = locations.map((location) => ({ + id: location.id, + name: location.name, + partOf: updateLocationPartOf(location.partOf) + })) + + // NOTE: TRPC expects certain format, which may seem unconventional. + const res = await fetch(`${env.GATEWAY_HOST}/events/locations.set`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ json: simplifiedLocations }) + }) + + if (!res.ok) { + raise(await res.json()) + } +} diff --git a/packages/events/src/router/__snapshots__/locations.set.test.ts.snap b/packages/events/src/router/__snapshots__/locations.set.test.ts.snap index e396ef5537a..0d5bacfde2e 100644 --- a/packages/events/src/router/__snapshots__/locations.set.test.ts.snap +++ b/packages/events/src/router/__snapshots__/locations.set.test.ts.snap @@ -14,4 +14,4 @@ exports[`Prevents sending empty payload 1`] = ` ]] `; -exports[`prevents unauthorized access 1`] = `[TRPCError: UNAUTHORIZED]`; +exports[`prevents unauthorized access from registrar 1`] = `[TRPCError: UNAUTHORIZED]`; diff --git a/packages/events/src/router/locations.set.test.ts b/packages/events/src/router/locations.set.test.ts index 8a82af5fb94..eb84d6a4eda 100644 --- a/packages/events/src/router/locations.set.test.ts +++ b/packages/events/src/router/locations.set.test.ts @@ -16,16 +16,22 @@ const nationalSystemAdminClient = createTestClient([ userScopes.nationalSystemAdmin ]) -const registarClient = createTestClient() +const registrarClient = createTestClient() const generator = payloadGenerator() -test('prevents unauthorized access', async () => { +test('prevents unauthorized access from registrar', async () => { await expect( - registarClient.locations.set([]) + registrarClient.locations.set([]) ).rejects.toThrowErrorMatchingSnapshot() }) +test('Allows national system admin to set locations', async () => { + await expect( + nationalSystemAdminClient.locations.set(generator.locations.set(1)) + ).resolves.toEqual(undefined) +}) + test('Prevents sending empty payload', async () => { await expect( nationalSystemAdminClient.locations.set([]) diff --git a/packages/events/src/router/middleware/middleware.ts b/packages/events/src/router/middleware/middleware.ts index b796ae3dcf6..4bd53834fc7 100644 --- a/packages/events/src/router/middleware/middleware.ts +++ b/packages/events/src/router/middleware/middleware.ts @@ -33,6 +33,15 @@ type MiddlewareOptions = Omit< 'ctx' > & { ctx: Context } +/** + * Depending on how the API is called, there might or might not be Bearer keyword in the header. + * To allow for usage with both direct HTTP calls and TRPC, ensure it's present to be able to use shared scope auth functions. + */ +const setBearerForToken = (token: string) => { + const bearer = 'Bearer' + + return token.startsWith(bearer) ? token : `${bearer} ${token}` +} /** * * @param scopes scopes that are allowed to access the resource @@ -40,7 +49,7 @@ type MiddlewareOptions = Omit< */ const createScopeAuthMiddleware = (scopes: Scope[]) => (opts: MiddlewareOptions) => { - if (inScope({ Authorization: `Bearer ${opts.ctx.token}` }, scopes)) { + if (inScope({ Authorization: setBearerForToken(opts.ctx.token) }, scopes)) { return opts.next() } diff --git a/packages/events/src/service/locations/locations.ts b/packages/events/src/service/locations/locations.ts index eb8fceb72f0..37fbf077e87 100644 --- a/packages/events/src/service/locations/locations.ts +++ b/packages/events/src/service/locations/locations.ts @@ -58,7 +58,7 @@ export async function setLocations(incomingLocations: Array) { } export const getLocations = async () => { - const { db } = await getClient() + const db = await getClient() return db.collection('locations').find().toArray() } diff --git a/packages/events/src/tests/utils.ts b/packages/events/src/tests/utils.ts index bc240a737b3..4b1a4f52b4c 100644 --- a/packages/events/src/tests/utils.ts +++ b/packages/events/src/tests/utils.ts @@ -13,7 +13,7 @@ import { appRouter, t } from '@events/router' import * as jwt from 'jsonwebtoken' import { readFileSync } from 'fs' import { join } from 'path' -import { Scope, userScopes } from '@opencrvs/commons' +import { Scope, userRoleScopes } from '@opencrvs/commons' const { createCallerFactory } = t @@ -30,7 +30,7 @@ export function createTestClient(scopes?: Scope[]) { const createTestToken = (scopes?: Scope[]) => jwt.sign( - { scope: scopes ?? [userScopes.register, userScopes.declare] }, + { scope: scopes ?? userRoleScopes.REGISTRATION_AGENT }, readFileSync(join(__dirname, './cert.key')), { algorithm: 'RS256', From 59fc990a078f6a3b63239ba965a2de7f286f7839 Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 9 Jan 2025 09:50:43 +0200 Subject: [PATCH 3/6] chore: clean up comments and logs --- packages/data-seeder/src/index.ts | 2 -- .../events/src/router/locations.get.test.ts | 12 ++++++++++ .../events/src/router/locations.set.test.ts | 22 +++++++++---------- .../src/router/middleware/middleware.ts | 1 - 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/data-seeder/src/index.ts b/packages/data-seeder/src/index.ts index e5aa49989a2..85932f82060 100644 --- a/packages/data-seeder/src/index.ts +++ b/packages/data-seeder/src/index.ts @@ -33,9 +33,7 @@ async function getToken(): Promise { if (!res.ok) { raise('Could not login as the super user') } - const body = await res.json() - console.log('body.token', body.token) return body.token } diff --git a/packages/events/src/router/locations.get.test.ts b/packages/events/src/router/locations.get.test.ts index 0928c59a43b..415b7de3184 100644 --- a/packages/events/src/router/locations.get.test.ts +++ b/packages/events/src/router/locations.get.test.ts @@ -22,6 +22,18 @@ test('Returns empty list when no locations are set', async () => { expect(fetchedEvents).toEqual([]) }) +test('Returns single location in right format', async () => { + const setLocationPayload = [ + { id: '123-456-789', partOf: null, name: 'Location foobar' } + ] + + await nationalSystemAdminClient.locations.set(setLocationPayload) + + const locations = await nationalSystemAdminClient.locations.get() + + expect(locations).toHaveLength(1) + expect(locations).toMatchObject(setLocationPayload) +}) test('Returns multiple locations', async () => { await nationalSystemAdminClient.locations.set(generator.locations.set(5)) diff --git a/packages/events/src/router/locations.set.test.ts b/packages/events/src/router/locations.set.test.ts index eb84d6a4eda..68ec05b3c97 100644 --- a/packages/events/src/router/locations.set.test.ts +++ b/packages/events/src/router/locations.set.test.ts @@ -39,9 +39,9 @@ test('Prevents sending empty payload', async () => { }) test('Creates single location', async () => { - const locationPayload = generator.locations.set([ + const locationPayload = [ { id: '123-456-789', partOf: null, name: 'Location foobar' } - ]) + ] await nationalSystemAdminClient.locations.set(locationPayload) @@ -54,18 +54,18 @@ test('Creates single location', async () => { test('Creates multiple locations', async () => { const parentId = 'parent-id' - await nationalSystemAdminClient.locations.set( - generator.locations.set([ - { id: 'parentId' }, - { partOf: parentId }, - { partOf: parentId }, - {} - ]) - ) + const locationPayload = generator.locations.set([ + { id: 'parentId' }, + { partOf: parentId }, + { partOf: parentId }, + {} + ]) + + await nationalSystemAdminClient.locations.set(locationPayload) const locations = await nationalSystemAdminClient.locations.get() - expect(locations).toHaveLength(4) + expect(locations).toEqual(locationPayload) }) test('Removes existing locations not in payload', async () => { diff --git a/packages/events/src/router/middleware/middleware.ts b/packages/events/src/router/middleware/middleware.ts index 4bd53834fc7..1fa15efea01 100644 --- a/packages/events/src/router/middleware/middleware.ts +++ b/packages/events/src/router/middleware/middleware.ts @@ -43,7 +43,6 @@ const setBearerForToken = (token: string) => { return token.startsWith(bearer) ? token : `${bearer} ${token}` } /** - * * @param scopes scopes that are allowed to access the resource * @returns TRPC compatible middleware function */ From 4402efc3d1ce4383c0caf4c6d963980355522747 Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 9 Jan 2025 09:56:42 +0200 Subject: [PATCH 4/6] fix: bring back missing log --- packages/data-seeder/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/data-seeder/src/index.ts b/packages/data-seeder/src/index.ts index 85932f82060..aaf07bd820f 100644 --- a/packages/data-seeder/src/index.ts +++ b/packages/data-seeder/src/index.ts @@ -99,6 +99,7 @@ async function main() { console.log('Seeding locations for v2 system (events)') await seedLocationsForV2Events(token) + console.log('Seeding users') await seedUsers(token, roleIdMap) await deactivateSuperuser(token) } From 292c173d2940254723884c60126c44f575db426e Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 9 Jan 2025 10:40:10 +0200 Subject: [PATCH 5/6] fix: use correct variable name --- packages/events/src/router/locations.get.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/events/src/router/locations.get.test.ts b/packages/events/src/router/locations.get.test.ts index 415b7de3184..b64b5936a21 100644 --- a/packages/events/src/router/locations.get.test.ts +++ b/packages/events/src/router/locations.get.test.ts @@ -18,10 +18,11 @@ const nationalSystemAdminClient = createTestClient([ const generator = payloadGenerator() test('Returns empty list when no locations are set', async () => { - const fetchedEvents = await nationalSystemAdminClient.locations.get() + const locations = await nationalSystemAdminClient.locations.get() - expect(fetchedEvents).toEqual([]) + expect(locations).toEqual([]) }) + test('Returns single location in right format', async () => { const setLocationPayload = [ { id: '123-456-789', partOf: null, name: 'Location foobar' } @@ -38,7 +39,7 @@ test('Returns single location in right format', async () => { test('Returns multiple locations', async () => { await nationalSystemAdminClient.locations.set(generator.locations.set(5)) - const events = await nationalSystemAdminClient.locations.get() + const locations = await nationalSystemAdminClient.locations.get() - expect(events).toHaveLength(5) + expect(locations).toHaveLength(5) }) From 4395e13093fb2d6f548e4d5c6c8ef58736da942f Mon Sep 17 00:00:00 2001 From: Riku Rouvila Date: Thu, 9 Jan 2025 22:39:03 +0900 Subject: [PATCH 6/6] Fix flaky unit test (#8309) --- packages/client/src/views/RegisterForm/DeclarationForm.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/views/RegisterForm/DeclarationForm.test.tsx b/packages/client/src/views/RegisterForm/DeclarationForm.test.tsx index 3530900d407..a6cc77a1b51 100644 --- a/packages/client/src/views/RegisterForm/DeclarationForm.test.tsx +++ b/packages/client/src/views/RegisterForm/DeclarationForm.test.tsx @@ -123,7 +123,7 @@ describe('when user starts a new declaration', () => { */ store.dispatch(storeDeclaration(draft)) - router.navigate( + await router.navigate( formatUrl(DRAFT_BIRTH_PARENT_FORM, { declarationId: draft.id.toString() })