1- // Behavioural test for `createLoaderPATApiRoute` to confirm PAT-authenticated
1+ // Integration test for `createLoaderPATApiRoute` — confirms PAT-authenticated
22// requests stamp `userId` onto the tenant context (so Sentry events from
33// PAT routes get user-level attribution).
44//
5- // PAT auth normally hits the DB via `rbac.authenticatePat`. To keep this
6- // a unit test, we stub the two DB-touching dependencies — narrow enough
7- // that the test exercises the wrapping behaviour without bringing up a
8- // real database.
5+ // Runs against the local postgres the webapp test setup already targets
6+ // (`apps/webapp/.env` → `DATABASE_URL`). Seeds a real User + PersonalAccessToken
7+ // via Prisma, calls the real loader with the real bearer, and lets
8+ // `rbac.authenticatePat` validate against the DB end-to-end. No stubs.
9+ //
10+ // Cleans up the rows it creates so the test is repeatable.
911
10- import { describe , it , expect , vi } from "vitest" ;
12+ import { afterAll , describe , expect , it } from "vitest" ;
13+ import { prisma } from "../app/db.server" ;
14+ import { createPersonalAccessToken } from "../app/services/personalAccessToken.server" ;
15+ import { tenantContext } from "../app/services/tenantContext.server" ;
16+ import { createLoaderPATApiRoute } from "../app/services/routeBuilders/apiBuilder.server" ;
1117
12- vi . mock ( "~/services/rbac.server" , ( ) => ( {
13- rbac : {
14- authenticatePat : vi . fn ( async ( ) => ( {
15- ok : true ,
16- userId : "usr_test_42" ,
17- ability : { } ,
18- tokenId : "tok_1" ,
19- lastAccessedAt : new Date ( ) ,
20- } ) ) ,
21- } ,
22- } ) ) ;
18+ const cleanup : Array < ( ) => Promise < unknown > > = [ ] ;
2319
24- vi . mock ( "~/services/personalAccessToken.server" , async ( orig ) => {
25- const actual = ( await orig ( ) ) as Record < string , unknown > ;
26- return {
27- ...actual ,
28- updateLastAccessedAtIfStale : vi . fn ( async ( ) => undefined ) ,
29- } ;
20+ afterAll ( async ( ) => {
21+ for ( const fn of cleanup ) {
22+ await fn ( ) . catch ( ( ) => { } ) ;
23+ }
3024} ) ;
3125
32- import { tenantContext } from "../app/services/tenantContext.server" ;
33- import { createLoaderPATApiRoute } from "../app/services/routeBuilders/apiBuilder.server" ;
34-
3526describe ( "createLoaderPATApiRoute" , ( ) => {
36- it ( "enriches tenant context with `userId` from the PAT auth result" , async ( ) => {
37- let observedUserId : string | undefined ;
27+ it ( "enriches tenant context with the authenticated PAT's userId" , async ( ) => {
28+ const suffix = `${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ;
29+ const user = await prisma . user . create ( {
30+ data : {
31+ email : `pat-tenant-test-${ suffix } @test.local` ,
32+ authenticationMethod : "MAGIC_LINK" ,
33+ } ,
34+ } ) ;
35+ cleanup . push ( async ( ) => {
36+ await prisma . personalAccessToken . deleteMany ( { where : { userId : user . id } } ) ;
37+ await prisma . user . delete ( { where : { id : user . id } } ) ;
38+ } ) ;
3839
40+ const created = await createPersonalAccessToken ( {
41+ name : `pat-tenant-test-${ suffix } ` ,
42+ userId : user . id ,
43+ } ) ;
44+
45+ let observedUserId : string | undefined ;
3946 const loader = createLoaderPATApiRoute ( { } , async ( ) => {
4047 observedUserId = tenantContext . get ( ) ?. userId ;
4148 return new Response ( null , { status : 200 } ) ;
@@ -44,32 +51,46 @@ describe("createLoaderPATApiRoute", () => {
4451 await tenantContext . run ( { } , async ( ) => {
4552 await loader ( {
4653 request : new Request ( "http://localhost/api/test" , {
47- headers : { Authorization : " Bearer pat_irrelevant_for_this_test" } ,
54+ headers : { Authorization : ` Bearer ${ created . token } ` } ,
4855 } ) ,
4956 params : { } ,
5057 context : { } ,
5158 } ) ;
5259 } ) ;
5360
54- expect ( observedUserId ) . toBe ( "usr_test_42" ) ;
61+ expect ( observedUserId ) . toBe ( user . id ) ;
5562 } ) ;
5663
57- it ( "does not leak the enrich across requests once the scope ends" , async ( ) => {
58- const loader = createLoaderPATApiRoute ( { } , async ( ) => {
59- return new Response ( null , { status : 200 } ) ;
64+ it ( "does not leave the enrich behind once the request scope ends" , async ( ) => {
65+ const suffix = `${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ;
66+ const user = await prisma . user . create ( {
67+ data : {
68+ email : `pat-tenant-leak-${ suffix } @test.local` ,
69+ authenticationMethod : "MAGIC_LINK" ,
70+ } ,
6071 } ) ;
72+ cleanup . push ( async ( ) => {
73+ await prisma . personalAccessToken . deleteMany ( { where : { userId : user . id } } ) ;
74+ await prisma . user . delete ( { where : { id : user . id } } ) ;
75+ } ) ;
76+
77+ const created = await createPersonalAccessToken ( {
78+ name : `pat-tenant-leak-${ suffix } ` ,
79+ userId : user . id ,
80+ } ) ;
81+
82+ const loader = createLoaderPATApiRoute ( { } , async ( ) => new Response ( null , { status : 200 } ) ) ;
6183
6284 await tenantContext . run ( { } , async ( ) => {
6385 await loader ( {
6486 request : new Request ( "http://localhost/api/test" , {
65- headers : { Authorization : " Bearer pat_irrelevant_for_this_test" } ,
87+ headers : { Authorization : ` Bearer ${ created . token } ` } ,
6688 } ) ,
6789 params : { } ,
6890 context : { } ,
6991 } ) ;
7092 } ) ;
7193
72- // Outside the run() scope, the enrich is gone with the scope.
7394 expect ( tenantContext . get ( ) ) . toBeUndefined ( ) ;
7495 } ) ;
7596} ) ;
0 commit comments