From 27824601b3483b4f282495c351f2ff8e7e2a922c Mon Sep 17 00:00:00 2001 From: rtavernaea Date: Mon, 5 Jan 2026 14:46:17 -0600 Subject: [PATCH 1/5] copy changes --- .../helpers/session/session-factory.ts | 2 + .../integration/tests/job-templates.spec.ts | 20 +++++- app/api/src/app/app.module.ts | 7 +++ app/api/src/app/routes.ts | 5 ++ .../src/auth/helpers/authorize.decorator.ts | 6 ++ app/api/src/auth/login/authorized.guard.ts | 62 +++++++++++++++++++ .../auth/login/identity-provider.service.ts | 1 + .../job-templates/job-templates.controller.ts | 7 ++- app/api/src/partners/partners.controller.ts | 13 ++++ app/api/src/partners/partners.module.ts | 9 +++ app/models/src/dtos/privileges.ts | 4 ++ app/models/src/dtos/role-privileges.ts | 13 ++++ app/models/src/dtos/session-data.dto.ts | 10 ++- .../src/interfaces/session.interface.ts | 2 + 14 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 app/api/src/auth/helpers/authorize.decorator.ts create mode 100644 app/api/src/auth/login/authorized.guard.ts create mode 100644 app/api/src/partners/partners.controller.ts create mode 100644 app/api/src/partners/partners.module.ts create mode 100644 app/models/src/dtos/privileges.ts create mode 100644 app/models/src/dtos/role-privileges.ts diff --git a/app/api/integration/helpers/session/session-factory.ts b/app/api/integration/helpers/session/session-factory.ts index 92cb267..93a8ddc 100644 --- a/app/api/integration/helpers/session/session-factory.ts +++ b/app/api/integration/helpers/session/session-factory.ts @@ -19,6 +19,8 @@ export const sessionData = ( tenant: tenant as Tenant, idpSessionId: `${user.idpId}-${user.id}-${tenant.code}`, idToken: `id_token_${user.idpId}-${user.id}-${tenant.code}`, + //Todo: update this when writing role tests + roles: [], }, }, }; diff --git a/app/api/integration/tests/job-templates.spec.ts b/app/api/integration/tests/job-templates.spec.ts index ae595d2..ad714d6 100644 --- a/app/api/integration/tests/job-templates.spec.ts +++ b/app/api/integration/tests/job-templates.spec.ts @@ -18,6 +18,7 @@ import { seedContext, } from '../factories/partner-user-tenant'; import { partnerA, partnerX } from '../fixtures/context-fixtures/partner-fixtures'; +import { SessionData } from 'express-session'; describe('GET /job-templates', () => { const endpoint = '/job-templates/assessments'; @@ -34,7 +35,7 @@ describe('GET /job-templates', () => { const sessM = sessionCookie('job-templates-spec-m'); let contextM: Awaited>; - beforeAll(async () => { + beforeEach(async () => { contextM = await seedContext(makePartnerUserTenantContext('m')); jest.spyOn(EarthbeamBundlesService.prototype, 'getBundles').mockResolvedValue(allBundles); @@ -43,7 +44,7 @@ describe('GET /job-templates', () => { await sessionStore.set(sessM.sid, sessionData(contextM.user, contextM.tenant)); }); - afterAll(async () => { + afterEach(async () => { await sessionStore.destroy(sessA.sid); await sessionStore.destroy(sessX.sid); await sessionStore.destroy(sessM.sid); @@ -102,5 +103,20 @@ describe('GET /job-templates', () => { expect(resA.status).toBe(200); expect(resA.body).toHaveLength(partnerABundles.length); }); + it('should reject requests from user without the PartnerAdmin role', async () => { + const resA = await request(app.getHttpServer()) + .post('/partners/assessments/test') + .set('Cookie', [sessA.cookie]); + expect(resA.status).toBe(403); + }); + it('allow partner admins to call this route', async () => { + const session = await sessionStore.get(sessA.sid); + session?.passport?.user.roles.push('PartnerAdmin'); + await sessionStore.set(sessA.sid, session as SessionData); + const resA = await request(app.getHttpServer()) + .post('/partners/assessments/test') + .set('Cookie', [sessA.cookie]); + expect(resA.status).toBe(201); + }); }); }); diff --git a/app/api/src/app/app.module.ts b/app/api/src/app/app.module.ts index 69c54bd..207e2d3 100644 --- a/app/api/src/app/app.module.ts +++ b/app/api/src/app/app.module.ts @@ -13,6 +13,8 @@ import { JobTemplatesModule } from '../job-templates/job-templates.module'; import { JobsModule } from '../jobs/jobs.module'; import { EarthbeamApiModule } from '../earthbeam/api/earthbeam-api.module'; import { EarthbeamApiAuthModule } from '../earthbeam/api/auth/earthbeam-api-auth.module'; +import { AuthorizedGuard } from '../auth/login/authorized.guard'; +import { PartnersModule } from '../partners/partners.module'; const resourceModules = [ UsersModule, @@ -22,6 +24,7 @@ const resourceModules = [ SchoolYearsModule, EarthbeamApiModule, EarthbeamApiAuthModule, + PartnersModule, ]; @Module({ @@ -33,6 +36,10 @@ const resourceModules = [ provide: APP_GUARD, useClass: AuthenticatedGuard, }, + { + provide: APP_GUARD, + useClass: AuthorizedGuard, + }, ], }) export class AppModule {} diff --git a/app/api/src/app/routes.ts b/app/api/src/app/routes.ts index e2730ff..0ae0357 100644 --- a/app/api/src/app/routes.ts +++ b/app/api/src/app/routes.ts @@ -10,6 +10,7 @@ import { EARTHBEAM_AUTH_BASE_ROUTE, EARTHBEAM_API_BASE_ROUTE, } from '../earthbeam/api/earthbeam-api.endpoints'; +import { PartnersModule } from '../partners/partners.module'; export const routes: Routes = [ { @@ -32,6 +33,10 @@ export const routes: Routes = [ path: 'school-years', module: SchoolYearsModule, }, + { + path: 'partners', + module: PartnersModule, + }, { path: EARTHBEAM_API_BASE_ROUTE, module: EarthbeamApiModule, diff --git a/app/api/src/auth/helpers/authorize.decorator.ts b/app/api/src/auth/helpers/authorize.decorator.ts new file mode 100644 index 0000000..d2497b0 --- /dev/null +++ b/app/api/src/auth/helpers/authorize.decorator.ts @@ -0,0 +1,6 @@ +import { SetMetadata } from '@nestjs/common'; +import { PrivilegeKey } from 'models/src/dtos/privileges'; + +export const AUTHORIZE_KEY = 'authorize_rule'; + +export const Authorize = (privilege: PrivilegeKey | null) => SetMetadata(AUTHORIZE_KEY, privilege); diff --git a/app/api/src/auth/login/authorized.guard.ts b/app/api/src/auth/login/authorized.guard.ts new file mode 100644 index 0000000..24e011d --- /dev/null +++ b/app/api/src/auth/login/authorized.guard.ts @@ -0,0 +1,62 @@ +import { GetSessionDataDto } from '@edanalytics/models'; +import { + CanActivate, + ExecutionContext, + HttpException, + Inject, + Injectable, + Logger, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { PrivilegeKey } from 'models/src/dtos/privileges'; +import { AUTHORIZE_KEY } from '../helpers/authorize.decorator'; +import { IS_PUBLIC_KEY } from './public.decorator'; +import { rolePrivileges } from 'models/src/dtos/role-privileges'; +import { Request } from 'express'; +import { plainToInstance } from 'class-transformer'; +import { AuthService } from '../auth.service'; +@Injectable() +export class AuthorizedGuard implements CanActivate { + constructor( + private reflector: Reflector, + @Inject(AuthService) private readonly authService: AuthService + ) {} + async canActivate(context: ExecutionContext) { + const privilege = this.reflector.getAllAndOverride(AUTHORIZE_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (privilege === undefined) { + return true; + } + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) { + return true; + } + const request: Request = context.switchToHttp().getRequest(); + if (request.isAuthenticated()) { + const user = request['user']; + const userDto = plainToInstance(GetSessionDataDto, user); + try { + if (privilege === null) { + Logger.verbose('Authorization explicitly skipped for route' + request.url); + return true; + } else { + if (!userDto.privileges.has(privilege)) { + return false; + } + } + } catch (authorizationSystemError) { + Logger.log(authorizationSystemError); + return false; + } + + return true; + } else { + return false; + } + } +} diff --git a/app/api/src/auth/login/identity-provider.service.ts b/app/api/src/auth/login/identity-provider.service.ts index c0a8e8f..33fd51f 100644 --- a/app/api/src/auth/login/identity-provider.service.ts +++ b/app/api/src/auth/login/identity-provider.service.ts @@ -231,6 +231,7 @@ export class IdentityProviderService implements OnApplicationBootstrap { tenant, idpSessionId: (claims.sid as string) ?? null, // used to look up session for OIDC backchannel logout idToken: tokenset.id_token ?? null, + roles: [], }); } catch (err) { // Log error so we can troubleshoot config but pass null to `done` diff --git a/app/api/src/job-templates/job-templates.controller.ts b/app/api/src/job-templates/job-templates.controller.ts index 94adfc3..ff45ff9 100644 --- a/app/api/src/job-templates/job-templates.controller.ts +++ b/app/api/src/job-templates/job-templates.controller.ts @@ -1,10 +1,11 @@ -import { Controller, Get, Inject, Param, ParseEnumPipe } from '@nestjs/common'; +import { Controller, Get, Inject, Param, ParseEnumPipe, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { EarthmoverBundleTypes, toGetJobTemplateDto } from '@edanalytics/models'; import { EarthbeamBundlesService } from '../earthbeam/earthbeam-bundles.service'; import { Tenant } from '../auth/helpers/tenant.decorator'; import type { PrismaClient, Tenant as TTenant } from '@prisma/client'; import { PRISMA_APP_USER } from '../database'; +import { Authorize } from '../auth/helpers/authorize.decorator'; @Controller() @ApiTags('JobTemplates') @@ -30,4 +31,8 @@ export class JobsTemplatesController { const allowedBundles = bundles.filter((bundle) => allowedKeys.includes(bundle.path)); // path is the only unique identifier for a bundle currently and should be stable. Bundle repo might someday be enhanced with IDs, but not there yet return toGetJobTemplateDto(allowedBundles); } + //TODO: move to partner controller + @Authorize('partner-earthmover-bundle.create') + @Post(':type/:bundleKey') + async enableBundle() {} } diff --git a/app/api/src/partners/partners.controller.ts b/app/api/src/partners/partners.controller.ts new file mode 100644 index 0000000..29b3a49 --- /dev/null +++ b/app/api/src/partners/partners.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Authorize } from '../auth/helpers/authorize.decorator'; + +@Controller() +@ApiTags('Partners') +export class PartnersController { + constructor() {} + + @Authorize('partner-earthmover-bundle.create') + @Post(':type/:bundleKey') + async enableBundle() {} +} diff --git a/app/api/src/partners/partners.module.ts b/app/api/src/partners/partners.module.ts new file mode 100644 index 0000000..253c6c8 --- /dev/null +++ b/app/api/src/partners/partners.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { PartnersController } from './partners.controller'; + +@Module({ + imports: [], + providers: [], + controllers: [PartnersController], +}) +export class PartnersModule {} diff --git a/app/models/src/dtos/privileges.ts b/app/models/src/dtos/privileges.ts new file mode 100644 index 0000000..f94ec35 --- /dev/null +++ b/app/models/src/dtos/privileges.ts @@ -0,0 +1,4 @@ +export type PrivilegeKey = + | 'partner-earthmover-bundle.read' + | 'partner-earthmover-bundle.create' + | 'partner-earthmover-bundle.delete'; diff --git a/app/models/src/dtos/role-privileges.ts b/app/models/src/dtos/role-privileges.ts new file mode 100644 index 0000000..8a86d5d --- /dev/null +++ b/app/models/src/dtos/role-privileges.ts @@ -0,0 +1,13 @@ +import { PrivilegeKey } from './privileges'; + +export type AppRoles = 'PartnerAdmin'; + +export const rolePrivileges: Record> = Object.freeze({ + PartnerAdmin: Object.freeze( + new Set([ + 'partner-earthmover-bundle.read', + 'partner-earthmover-bundle.create', + 'partner-earthmover-bundle.delete', + ]) + ), +}); diff --git a/app/models/src/dtos/session-data.dto.ts b/app/models/src/dtos/session-data.dto.ts index 9655930..0628c4b 100644 --- a/app/models/src/dtos/session-data.dto.ts +++ b/app/models/src/dtos/session-data.dto.ts @@ -1,3 +1,5 @@ +import { PrivilegeKey } from './privileges'; +import { AppRoles, rolePrivileges } from './role-privileges'; import { GetUserDto, toGetUserDto } from './user.dto'; import { IPassportSession } from '../interfaces'; import { Expose, Transform, Type } from 'class-transformer'; @@ -13,8 +15,14 @@ export class GetSessionDataDto { @Expose() @Type(() => GetTenantDto) tenant: Tenant; + @Expose() + roles: AppRoles[]; + get privileges() { + return new Set( + ...this.roles.flatMap((role) => (role in rolePrivileges ? rolePrivileges[role] : [])) + ); + } } - export const toGetSessionDataDto = makeSerializerCustomType( GetSessionDataDto ); diff --git a/app/models/src/interfaces/session.interface.ts b/app/models/src/interfaces/session.interface.ts index 201c6ec..ea6b997 100644 --- a/app/models/src/interfaces/session.interface.ts +++ b/app/models/src/interfaces/session.interface.ts @@ -1,4 +1,5 @@ import { Tenant, User } from '@prisma/client'; +import { AppRoles } from '../dtos/role-privileges'; /** * This interface describes the full session object stored in the DB @@ -21,4 +22,5 @@ export interface IPassportSession { tenant: Tenant; idpSessionId: string | null; idToken: string | null; + roles: AppRoles[]; } From af7cbb6c743bb77d8b1527350bcf61653fbb1ea5 Mon Sep 17 00:00:00 2001 From: rtavernaea Date: Tue, 13 Jan 2026 14:39:15 -0600 Subject: [PATCH 2/5] pr comments --- .../integration/tests/job-templates.spec.ts | 16 ------- app/api/integration/tests/partners.spec.ts | 47 +++++++++++++++++++ .../job-templates/job-templates.controller.ts | 8 +--- app/models/src/dtos/session-data.dto.ts | 5 +- 4 files changed, 51 insertions(+), 25 deletions(-) create mode 100644 app/api/integration/tests/partners.spec.ts diff --git a/app/api/integration/tests/job-templates.spec.ts b/app/api/integration/tests/job-templates.spec.ts index ad714d6..dda6a7e 100644 --- a/app/api/integration/tests/job-templates.spec.ts +++ b/app/api/integration/tests/job-templates.spec.ts @@ -18,7 +18,6 @@ import { seedContext, } from '../factories/partner-user-tenant'; import { partnerA, partnerX } from '../fixtures/context-fixtures/partner-fixtures'; -import { SessionData } from 'express-session'; describe('GET /job-templates', () => { const endpoint = '/job-templates/assessments'; @@ -103,20 +102,5 @@ describe('GET /job-templates', () => { expect(resA.status).toBe(200); expect(resA.body).toHaveLength(partnerABundles.length); }); - it('should reject requests from user without the PartnerAdmin role', async () => { - const resA = await request(app.getHttpServer()) - .post('/partners/assessments/test') - .set('Cookie', [sessA.cookie]); - expect(resA.status).toBe(403); - }); - it('allow partner admins to call this route', async () => { - const session = await sessionStore.get(sessA.sid); - session?.passport?.user.roles.push('PartnerAdmin'); - await sessionStore.set(sessA.sid, session as SessionData); - const resA = await request(app.getHttpServer()) - .post('/partners/assessments/test') - .set('Cookie', [sessA.cookie]); - expect(resA.status).toBe(201); - }); }); }); diff --git a/app/api/integration/tests/partners.spec.ts b/app/api/integration/tests/partners.spec.ts new file mode 100644 index 0000000..63ab648 --- /dev/null +++ b/app/api/integration/tests/partners.spec.ts @@ -0,0 +1,47 @@ +import { sessionData } from '../helpers/session/session-factory'; +import { userA } from '../fixtures/user-fixtures'; +import { tenantA } from '../fixtures/context-fixtures/tenant-fixtures'; +import { allBundles } from '../fixtures/em-bundle-fixtures'; +import { sessionCookie } from '../helpers/session/session-cookie'; +import sessionStore from '../helpers/session/session-store'; +import request from 'supertest'; +import { EarthbeamBundlesService } from 'api/src/earthbeam/earthbeam-bundles.service'; +import { SessionData } from 'express-session'; + +describe('POST /partners', () => { + const endpoint = '/partners/assessments/test'; + it('should reject unauthenticated requests', async () => { + const res = await request(app.getHttpServer()).post(endpoint); + expect(res.status).toBe(401); + }); + + describe('authenticated requests', () => { + const sessA = sessionCookie('partners-spec-a'); + + beforeEach(async () => { + jest.spyOn(EarthbeamBundlesService.prototype, 'getBundles').mockResolvedValue(allBundles); + await sessionStore.set(sessA.sid, sessionData(userA, tenantA)); + }); + + afterEach(async () => { + await sessionStore.destroy(sessA.sid); + await jest.restoreAllMocks(); + }); + + it('should reject requests from user without the PartnerAdmin role', async () => { + const resA = await request(app.getHttpServer()) + .post('/partners/assessments/test') + .set('Cookie', [sessA.cookie]); + expect(resA.status).toBe(403); + }); + it('allow partner admins to call this route', async () => { + const session = await sessionStore.get(sessA.sid); + session?.passport?.user.roles.push('PartnerAdmin'); + await sessionStore.set(sessA.sid, session as SessionData); + const resA = await request(app.getHttpServer()) + .post('/partners/assessments/test') + .set('Cookie', [sessA.cookie]); + expect(resA.status).toBe(201); + }); + }); +}); diff --git a/app/api/src/job-templates/job-templates.controller.ts b/app/api/src/job-templates/job-templates.controller.ts index ff45ff9..16fa6f7 100644 --- a/app/api/src/job-templates/job-templates.controller.ts +++ b/app/api/src/job-templates/job-templates.controller.ts @@ -1,12 +1,10 @@ -import { Controller, Get, Inject, Param, ParseEnumPipe, Post } from '@nestjs/common'; +import { Controller, Get, Inject, Param, ParseEnumPipe } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { EarthmoverBundleTypes, toGetJobTemplateDto } from '@edanalytics/models'; import { EarthbeamBundlesService } from '../earthbeam/earthbeam-bundles.service'; import { Tenant } from '../auth/helpers/tenant.decorator'; import type { PrismaClient, Tenant as TTenant } from '@prisma/client'; import { PRISMA_APP_USER } from '../database'; -import { Authorize } from '../auth/helpers/authorize.decorator'; - @Controller() @ApiTags('JobTemplates') export class JobsTemplatesController { @@ -31,8 +29,4 @@ export class JobsTemplatesController { const allowedBundles = bundles.filter((bundle) => allowedKeys.includes(bundle.path)); // path is the only unique identifier for a bundle currently and should be stable. Bundle repo might someday be enhanced with IDs, but not there yet return toGetJobTemplateDto(allowedBundles); } - //TODO: move to partner controller - @Authorize('partner-earthmover-bundle.create') - @Post(':type/:bundleKey') - async enableBundle() {} } diff --git a/app/models/src/dtos/session-data.dto.ts b/app/models/src/dtos/session-data.dto.ts index 0628c4b..76b0be8 100644 --- a/app/models/src/dtos/session-data.dto.ts +++ b/app/models/src/dtos/session-data.dto.ts @@ -16,10 +16,11 @@ export class GetSessionDataDto { @Type(() => GetTenantDto) tenant: Tenant; @Expose() - roles: AppRoles[]; + roles?: AppRoles[]; get privileges() { + if (!this.roles) return new Set(); return new Set( - ...this.roles.flatMap((role) => (role in rolePrivileges ? rolePrivileges[role] : [])) + this.roles.flatMap((role) => (role in rolePrivileges ? Array.from(rolePrivileges[role]) : [])) ); } } From a2e8f444ee12503e8fa76d52908f91d9bb20a432 Mon Sep 17 00:00:00 2001 From: rtavernaea Date: Tue, 13 Jan 2026 14:51:20 -0600 Subject: [PATCH 3/5] refactor auth guard --- app/api/src/auth/login/authorized.guard.ts | 47 +++++++++------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/app/api/src/auth/login/authorized.guard.ts b/app/api/src/auth/login/authorized.guard.ts index 24e011d..ff4a6fd 100644 --- a/app/api/src/auth/login/authorized.guard.ts +++ b/app/api/src/auth/login/authorized.guard.ts @@ -11,7 +11,6 @@ import { Reflector } from '@nestjs/core'; import { PrivilegeKey } from 'models/src/dtos/privileges'; import { AUTHORIZE_KEY } from '../helpers/authorize.decorator'; import { IS_PUBLIC_KEY } from './public.decorator'; -import { rolePrivileges } from 'models/src/dtos/role-privileges'; import { Request } from 'express'; import { plainToInstance } from 'class-transformer'; import { AuthService } from '../auth.service'; @@ -21,41 +20,35 @@ export class AuthorizedGuard implements CanActivate { private reflector: Reflector, @Inject(AuthService) private readonly authService: AuthService ) {} - async canActivate(context: ExecutionContext) { - const privilege = this.reflector.getAllAndOverride(AUTHORIZE_KEY, [ + + async canActivate(context: ExecutionContext): Promise { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ context.getHandler(), context.getClass(), ]); - if (privilege === undefined) { - return true; - } - const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + + if (isPublic) return true; + + const privilege = this.reflector.getAllAndOverride(AUTHORIZE_KEY, [ context.getHandler(), context.getClass(), ]); - if (isPublic) { - return true; - } + + if (privilege === undefined) return true; + const request: Request = context.switchToHttp().getRequest(); - if (request.isAuthenticated()) { - const user = request['user']; - const userDto = plainToInstance(GetSessionDataDto, user); - try { - if (privilege === null) { - Logger.verbose('Authorization explicitly skipped for route' + request.url); - return true; - } else { - if (!userDto.privileges.has(privilege)) { - return false; - } - } - } catch (authorizationSystemError) { - Logger.log(authorizationSystemError); - return false; + + if (!request.isAuthenticated()) return false; + try { + if (privilege === null) { + Logger.verbose('Authorization explicitly skipped for route ' + request.url); + return true; } - return true; - } else { + const userDto = plainToInstance(GetSessionDataDto, request['user']); + return userDto.privileges.has(privilege); + } catch (err) { + Logger.log(err); return false; } } From d3829c74d10a76a4a3dcdf88c99f04ed49ef9452 Mon Sep 17 00:00:00 2001 From: rtavernaea Date: Tue, 13 Jan 2026 14:53:41 -0600 Subject: [PATCH 4/5] move try catch --- app/api/src/auth/login/authorized.guard.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/api/src/auth/login/authorized.guard.ts b/app/api/src/auth/login/authorized.guard.ts index ff4a6fd..1f041df 100644 --- a/app/api/src/auth/login/authorized.guard.ts +++ b/app/api/src/auth/login/authorized.guard.ts @@ -37,14 +37,13 @@ export class AuthorizedGuard implements CanActivate { if (privilege === undefined) return true; const request: Request = context.switchToHttp().getRequest(); + if (privilege === null) { + Logger.verbose('Authorization explicitly skipped for route ' + request.url); + return true; + } if (!request.isAuthenticated()) return false; try { - if (privilege === null) { - Logger.verbose('Authorization explicitly skipped for route ' + request.url); - return true; - } - const userDto = plainToInstance(GetSessionDataDto, request['user']); return userDto.privileges.has(privilege); } catch (err) { From 22a047343abd77bcbc05a2b45afac44c9c195803 Mon Sep 17 00:00:00 2001 From: rtavernaea Date: Wed, 14 Jan 2026 11:47:44 -0600 Subject: [PATCH 5/5] dont use restoreallmocks --- app/api/integration/tests/partners.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/api/integration/tests/partners.spec.ts b/app/api/integration/tests/partners.spec.ts index 63ab648..8cd6244 100644 --- a/app/api/integration/tests/partners.spec.ts +++ b/app/api/integration/tests/partners.spec.ts @@ -17,6 +17,9 @@ describe('POST /partners', () => { describe('authenticated requests', () => { const sessA = sessionCookie('partners-spec-a'); + let mockGetBundles = jest + .spyOn(EarthbeamBundlesService.prototype, 'getBundles') + .mockResolvedValue(allBundles); beforeEach(async () => { jest.spyOn(EarthbeamBundlesService.prototype, 'getBundles').mockResolvedValue(allBundles); @@ -25,7 +28,7 @@ describe('POST /partners', () => { afterEach(async () => { await sessionStore.destroy(sessA.sid); - await jest.restoreAllMocks(); + mockGetBundles.mockRestore(); }); it('should reject requests from user without the PartnerAdmin role', async () => {