1+ import { IncomingMessage } from 'http'
2+
3+ import { createMock } from '@golevelup/ts-vitest'
4+ import { EntityManager } from '@mikro-orm/core'
15import { UnauthorizedException } from '@nestjs/common'
6+ import { JwtService } from '@nestjs/jwt'
27
8+ import { Agent } from 'common/agent'
39import { Role } from 'common/auth'
10+ import { User , Wallet } from 'common/entities'
11+ import { Logger } from 'common/logger'
412import { getWalletId } from 'utils/auth'
513
14+ import { AuthService } from '../auth.service'
15+
616describe ( 'getWalletId' , ( ) => {
717 test . each ( [
818 [ { role : Role . Admin , userId : '11' } , 'Administration_11' ] ,
@@ -29,3 +39,186 @@ describe('getWalletId', () => {
2939 expect ( ( ) => getWalletId ( params ) ) . toThrow ( UnauthorizedException )
3040 } )
3141} )
42+
43+ describe ( 'AuthService' , ( ) => {
44+ let service : AuthService
45+ let agent : Agent
46+ let jwtService : JwtService
47+ let em : EntityManager
48+ let logger : Logger
49+
50+ const makeUser = ( overrides : Partial < User > & { walletsContains ?: boolean } = { } ) : User => {
51+ const walletsContains = overrides . walletsContains ?? true
52+ return {
53+ id : overrides . id ?? 'user-1' ,
54+ wallets : {
55+ init : vi . fn ( ) . mockResolvedValue ( undefined ) ,
56+ contains : vi . fn ( ) . mockReturnValue ( walletsContains ) ,
57+ add : vi . fn ( ) ,
58+ } ,
59+ } as unknown as User
60+ }
61+
62+ const makeWallet = ( overrides : Partial < Wallet > = { } ) : Wallet =>
63+ ( {
64+ id : 'Issuer_11_in_Organization_7' ,
65+ tenantId : 'tenant-xyz' ,
66+ ...overrides ,
67+ } ) as Wallet
68+
69+ beforeEach ( ( ) => {
70+ agent = createMock < Agent > ( {
71+ modules : {
72+ tenants : {
73+ createTenant : vi . fn ( ) ,
74+ withTenantAgent : vi . fn ( ) . mockImplementation ( async ( _opts : unknown , cb : ( ta : unknown ) => Promise < void > ) => {
75+ await cb ( { modules : { anoncreds : { createLinkSecret : vi . fn ( ) } } } )
76+ } ) ,
77+ } ,
78+ } as any ,
79+ } )
80+ jwtService = createMock < JwtService > ( )
81+ em = createMock < EntityManager > ( )
82+ logger = createMock < Logger > ( )
83+ service = new AuthService ( agent , jwtService , em , logger )
84+ } )
85+
86+ describe ( 'validateRequestToken' , ( ) => {
87+ test ( 'throws when Authorization header is missing' , async ( ) => {
88+ const request = { headers : { } } as IncomingMessage
89+
90+ await expect ( service . validateRequestToken ( request ) ) . rejects . toThrow ( 'Authorization token is missing' )
91+ } )
92+
93+ test ( 'throws when scheme is not Bearer' , async ( ) => {
94+ const request = { headers : { authorization : 'Basic abc123' } } as IncomingMessage
95+
96+ await expect ( service . validateRequestToken ( request ) ) . rejects . toThrow ( 'Authorization token is missing' )
97+ } )
98+
99+ test ( 'verifies token and returns AuthInfo' , async ( ) => {
100+ const request = { headers : { authorization : 'Bearer my-jwt' } } as IncomingMessage
101+ const payload = { sub : '11' , org_id : '7' , name : 'test' , roles : [ Role . Issuer ] }
102+
103+ vi . mocked ( jwtService . verifyAsync ) . mockResolvedValue ( payload as any )
104+
105+ const user = makeUser ( { id : '11' } )
106+ const wallet = makeWallet ( )
107+ vi . mocked ( em . findOne ) . mockResolvedValueOnce ( user ) . mockResolvedValueOnce ( wallet )
108+
109+ const result = await service . validateRequestToken ( request )
110+
111+ expect ( jwtService . verifyAsync ) . toHaveBeenCalledWith ( 'my-jwt' )
112+ expect ( em . findOne ) . toHaveBeenNthCalledWith ( 1 , User , { id : '11' } )
113+ expect ( em . findOne ) . toHaveBeenNthCalledWith ( 2 , Wallet , { id : 'Issuer_11_in_Organization_7' } )
114+ expect ( result . userId ) . toBe ( '11' )
115+ expect ( result . role ) . toBe ( Role . Issuer )
116+ expect ( result . walletId ) . toBe ( 'Issuer_11_in_Organization_7' )
117+ expect ( result . tenantId ) . toBe ( 'tenant-xyz' )
118+ } )
119+ } )
120+
121+ describe ( 'validateTokenPayload' , ( ) => {
122+ test ( 'throws UnauthorizedException when payload has multiple roles' , async ( ) => {
123+ const payload = { sub : '11' , org_id : '7' , name : 'test' , roles : [ Role . Admin , Role . Issuer ] } as any
124+
125+ await expect ( service . validateTokenPayload ( payload ) ) . rejects . toThrow ( UnauthorizedException )
126+ } )
127+
128+ test ( 'throws UnauthorizedException when role is not a valid Role' , async ( ) => {
129+ const payload = { sub : '11' , org_id : '7' , name : 'test' , roles : [ 'Hacker' ] } as any
130+
131+ await expect ( service . validateTokenPayload ( payload ) ) . rejects . toThrow ( UnauthorizedException )
132+ } )
133+
134+ test ( 'returns AuthInfo when user and wallet exist and wallet already linked' , async ( ) => {
135+ const payload = { sub : '11' , org_id : '7' , name : 'Alice' , roles : [ Role . Issuer ] } as any
136+ const user = makeUser ( { id : '11' , walletsContains : true } )
137+ const wallet = makeWallet ( )
138+
139+ vi . mocked ( em . findOne ) . mockResolvedValueOnce ( user ) . mockResolvedValueOnce ( wallet )
140+
141+ const result = await service . validateTokenPayload ( payload )
142+
143+ expect ( em . findOne ) . toHaveBeenNthCalledWith ( 1 , User , { id : '11' } )
144+ expect ( em . findOne ) . toHaveBeenNthCalledWith ( 2 , Wallet , { id : 'Issuer_11_in_Organization_7' } )
145+ expect ( result ) . toEqual ( {
146+ userId : '11' ,
147+ user,
148+ userName : 'Alice' ,
149+ role : Role . Issuer ,
150+ orgId : '7' ,
151+ walletId : 'Issuer_11_in_Organization_7' ,
152+ tenantId : 'tenant-xyz' ,
153+ } )
154+ expect ( user . wallets . add ) . not . toHaveBeenCalled ( )
155+ expect ( em . flush ) . not . toHaveBeenCalled ( )
156+ } )
157+
158+ test ( 'links wallet to user when not already linked' , async ( ) => {
159+ const payload = { sub : '11' , org_id : '7' , name : 'Alice' , roles : [ Role . Issuer ] } as any
160+ const user = makeUser ( { id : '11' , walletsContains : false } )
161+ const wallet = makeWallet ( )
162+
163+ vi . mocked ( em . findOne ) . mockResolvedValueOnce ( user ) . mockResolvedValueOnce ( wallet )
164+
165+ await service . validateTokenPayload ( payload )
166+
167+ expect ( em . findOne ) . toHaveBeenNthCalledWith ( 1 , User , { id : '11' } )
168+ expect ( em . findOne ) . toHaveBeenNthCalledWith ( 2 , Wallet , { id : 'Issuer_11_in_Organization_7' } )
169+ expect ( user . wallets . add ) . toHaveBeenCalledWith ( wallet )
170+ expect ( em . flush ) . toHaveBeenCalled ( )
171+ } )
172+
173+ test ( 'creates user when not found' , async ( ) => {
174+ const payload = { sub : 'new-user' , org_id : '7' , name : 'Bob' , roles : [ Role . Issuer ] } as any
175+ const wallet = makeWallet ( )
176+
177+ vi . mocked ( em . findOne ) . mockResolvedValueOnce ( null ) . mockResolvedValueOnce ( wallet )
178+
179+ // The new User created inside the service has real Collection internals; replace findOne
180+ // behavior so the second path (wallets.init / contains / add) works via a post-create hook.
181+ vi . mocked ( em . persistAndFlush ) . mockImplementation ( ( entity : any ) => {
182+ if ( entity instanceof User ) {
183+ entity . wallets = {
184+ init : vi . fn ( ) . mockResolvedValue ( undefined ) ,
185+ contains : vi . fn ( ) . mockReturnValue ( true ) ,
186+ add : vi . fn ( ) ,
187+ } as any
188+ }
189+ return Promise . resolve ( )
190+ } )
191+
192+ const result = await service . validateTokenPayload ( payload )
193+
194+ expect ( em . findOne ) . toHaveBeenNthCalledWith ( 1 , User , { id : 'new-user' } )
195+ expect ( em . findOne ) . toHaveBeenNthCalledWith ( 2 , Wallet , { id : 'Issuer_new-user_in_Organization_7' } )
196+ expect ( em . persistAndFlush ) . toHaveBeenCalled ( )
197+ expect ( result . userId ) . toBe ( 'new-user' )
198+ } )
199+
200+ test ( 'creates wallet + tenant + link secret when wallet not found' , async ( ) => {
201+ const payload = { sub : '11' , org_id : '7' , name : 'Alice' , roles : [ Role . Issuer ] } as any
202+ const user = makeUser ( { id : '11' , walletsContains : true } )
203+
204+ vi . mocked ( em . findOne ) . mockResolvedValueOnce ( user ) . mockResolvedValueOnce ( null )
205+
206+ vi . mocked ( agent . modules . tenants . createTenant ) . mockResolvedValue ( { id : 'new-tenant-id' } as any )
207+
208+ const createLinkSecret = vi . fn ( )
209+ vi . mocked ( agent . modules . tenants . withTenantAgent ) . mockImplementation ( async ( _opts , cb ) => {
210+ await cb ( { modules : { anoncreds : { createLinkSecret } } } as any )
211+ } )
212+
213+ const result = await service . validateTokenPayload ( payload )
214+
215+ expect ( em . findOne ) . toHaveBeenNthCalledWith ( 1 , User , { id : '11' } )
216+ expect ( em . findOne ) . toHaveBeenNthCalledWith ( 2 , Wallet , { id : 'Issuer_11_in_Organization_7' } )
217+ expect ( agent . modules . tenants . createTenant ) . toHaveBeenCalledWith ( {
218+ config : { label : 'Issuer_11_in_Organization_7' } ,
219+ } )
220+ expect ( createLinkSecret ) . toHaveBeenCalled ( )
221+ expect ( result . tenantId ) . toBe ( 'new-tenant-id' )
222+ } )
223+ } )
224+ } )
0 commit comments