Skip to content

Commit 6b63cb4

Browse files
Merge remote-tracking branch 'upstream/main' into fix/store-reducer-manager-type-casting
Signed-off-by: Aaryaa Newaskar <aryu.newaskar77@gmail.com> # Conflicts: # heka-identity-service/docs/setup.md
2 parents e8bfc2d + 3f9bb15 commit 6b63cb4

29 files changed

Lines changed: 6298 additions & 36 deletions

File tree

.github/workflows/heka-identity-service-verify.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ concurrency:
1313
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
1414
cancel-in-progress: true
1515

16+
permissions:
17+
checks: write
18+
pull-requests: write
19+
contents: write
20+
1621
jobs:
1722
verify:
1823
defaults:
@@ -77,4 +82,11 @@ jobs:
7782
EXPRESS_HOST: localhost
7883
FILE_STORAGE_FS_URL: http://localhost:3000
7984
APP_ENDPOINT: http://localhost:3000
80-
run: yarn test
85+
run: yarn test:coverage
86+
87+
- name: Report coverage
88+
uses: davelosert/vitest-coverage-report-action@15b5b41bb7d36796d89f4bf482b09529c53f3446 # v2.9.2
89+
with:
90+
working-directory: ./heka-identity-service
91+
json-summary-path: ./coverage/coverage-summary.json
92+
json-final-path: ./coverage/coverage-final.json

heka-auth-service/src/core/config/configs/jwt.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const jwtConfigDefaults = {
1515
secret: 'test',
1616
accessExpiry: 60 * 60, // 1h
1717
refreshExpiry: 86400, // 24h
18-
demoUserTokenExpiry: 1000 * 24 * 60 * 60 * 30 * 12, // ~1 years validity for Demo User
18+
demoUserTokenExpiry: 60 * 60 * 24 * 365, // ~1 year validity for Demo User
1919
}
2020

2121
export class JwtConfig {

heka-auth-service/src/oauth/oauth.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export class OAuthService {
165165

166166
return {
167167
token,
168-
expiresIn: this.configService.jwtConfig.accessExpiry,
168+
expiresIn: accessTokenExpiresIn,
169169
}
170170
}
171171

heka-identity-service/docs/setup.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ To run Heka Identity Service locally, follow these steps:
2222
```bash
2323
yarn install
2424
```
25-
2625
### Fixing node-gyp issues with Python 3.12
2726

2827
If you encounter `node-gyp` build failures with Python 3.12,
@@ -39,7 +38,6 @@ Downgrade or switch your active Python version to 3.11:
3938
```bash
4039
npm install --python=python3.11
4140
```
42-
4341
5. Configure persistent storage. You can find information on how to configure it in the [Persistence](#persistence)
4442
and [Migrations](#migrations) sections.
4543
6. Run the server as described in the [Run the app](#run-the-app)

heka-identity-service/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
"watch": "nodemon --config nodemon.json",
1212
"debug": "nodemon --config nodemon-debug.json",
1313
"test": "vitest run",
14+
"test:coverage": "vitest run --coverage",
1415
"test:watch": "vitest",
16+
"test:unit": "vitest run src/",
17+
"test:e2e": "vitest run test/",
1518
"check-types": "yarn check-types:src && yarn check-types:test",
1619
"check-types:src": "tsc -p tsconfig.src.json --noEmit",
1720
"check-types:test": "tsc -p tsconfig.test.json --noEmit",
@@ -104,6 +107,7 @@
104107
"@types/uuid": "^9.0.1",
105108
"@typescript-eslint/eslint-plugin": "^5.59.8",
106109
"@typescript-eslint/parser": "^5.59.8",
110+
"@vitest/coverage-v8": "^2.1.8",
107111
"did-resolver": "^4.1.0",
108112
"eslint": "^8.42.0",
109113
"eslint-config-prettier": "^8.8.0",

heka-identity-service/src/common/auth/__tests__/auth.service.test.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1+
import { IncomingMessage } from 'http'
2+
3+
import { createMock } from '@golevelup/ts-vitest'
4+
import { EntityManager } from '@mikro-orm/core'
15
import { UnauthorizedException } from '@nestjs/common'
6+
import { JwtService } from '@nestjs/jwt'
27

8+
import { Agent } from 'common/agent'
39
import { Role } from 'common/auth'
10+
import { User, Wallet } from 'common/entities'
11+
import { Logger } from 'common/logger'
412
import { getWalletId } from 'utils/auth'
513

14+
import { AuthService } from '../auth.service'
15+
616
describe('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

Comments
 (0)