Skip to content

Commit a9428ed

Browse files
authored
chore: OpenID module cleanup (#1812)
Signed-off-by: Mostafa Gamal <moscd3@gmail.com>
1 parent fda1a41 commit a9428ed

36 files changed

Lines changed: 2994 additions & 537 deletions

.changeset/ripe-sloths-bet.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@bifold/core': patch
3+
---
4+
5+
Cleanup OpenID module
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import React, { PropsWithChildren } from 'react'
2+
import { renderHook, waitFor } from '@testing-library/react-native'
3+
import {
4+
ClaimFormat,
5+
MdocRecord,
6+
SdJwtVcRecord,
7+
W3cCredentialRecord,
8+
} from '@credo-ts/core'
9+
10+
import {
11+
OpenIDCredentialRecordProvider,
12+
useOpenIDCredentials,
13+
} from '../../../src/modules/openid/context/OpenIDCredentialRecordProvider'
14+
import { OpenIDCredentialType } from '../../../src/modules/openid/types'
15+
import { useAppAgent } from '../../../src/utils/agent'
16+
import { TOKENS, useServices } from '../../../src/container-api'
17+
import {
18+
deleteOpenIDCredential,
19+
findOpenIDCredentialById,
20+
getOpenIDCredentialById,
21+
storeOpenIDCredential,
22+
} from '../../../src/modules/openid/credentialRecord'
23+
import { getCredentialForDisplay } from '../../../src/modules/openid/display'
24+
import { buildFieldsFromW3cCredsCredential } from '../../../src/utils/oca'
25+
import { recordsAddedByType, recordsRemovedByType } from '@bifold/react-hooks/build/recordUtils'
26+
27+
jest.mock('../../../src/utils/agent', () => ({
28+
useAppAgent: jest.fn(),
29+
}))
30+
31+
jest.mock('../../../src/container-api', () => ({
32+
TOKENS: {
33+
UTIL_LOGGER: 'UTIL_LOGGER',
34+
UTIL_OCA_RESOLVER: 'UTIL_OCA_RESOLVER',
35+
},
36+
useServices: jest.fn(),
37+
}))
38+
39+
jest.mock('../../../src/modules/openid/credentialRecord', () => ({
40+
getOpenIDCredentialById: jest.fn(),
41+
findOpenIDCredentialById: jest.fn(),
42+
storeOpenIDCredential: jest.fn(),
43+
deleteOpenIDCredential: jest.fn(),
44+
}))
45+
46+
jest.mock('../../../src/modules/openid/display', () => ({
47+
getCredentialForDisplay: jest.fn(),
48+
}))
49+
50+
jest.mock('../../../src/utils/oca', () => ({
51+
buildFieldsFromW3cCredsCredential: jest.fn(),
52+
}))
53+
54+
jest.mock('@bifold/react-hooks/build/recordUtils', () => ({
55+
recordsAddedByType: jest.fn(),
56+
recordsRemovedByType: jest.fn(),
57+
}))
58+
59+
jest.mock('react-i18next', () => ({
60+
useTranslation: () => ({
61+
i18n: { language: 'en' },
62+
}),
63+
}))
64+
65+
const createRecord = <T extends object>(prototype: T, values: Record<string, unknown> = {}) =>
66+
Object.assign(Object.create(prototype), values)
67+
68+
const mockUseAppAgent = useAppAgent as jest.Mock
69+
const mockUseServices = useServices as jest.Mock
70+
const mockGetOpenIDCredentialById = getOpenIDCredentialById as jest.Mock
71+
const mockFindOpenIDCredentialById = findOpenIDCredentialById as jest.Mock
72+
const mockStoreOpenIDCredential = storeOpenIDCredential as jest.Mock
73+
const mockDeleteOpenIDCredential = deleteOpenIDCredential as jest.Mock
74+
const mockGetCredentialForDisplay = getCredentialForDisplay as jest.Mock
75+
const mockBuildFieldsFromW3cCredsCredential = buildFieldsFromW3cCredsCredential as jest.Mock
76+
const mockRecordsAddedByType = recordsAddedByType as jest.Mock
77+
const mockRecordsRemovedByType = recordsRemovedByType as jest.Mock
78+
79+
describe('OpenIDCredentialRecordProvider', () => {
80+
const logger = {
81+
error: jest.fn(),
82+
}
83+
84+
const bundleResolver = {
85+
resolveAllBundles: jest.fn(),
86+
}
87+
88+
const createAgentMock = ({
89+
w3cRecords = [],
90+
sdJwtRecords = [],
91+
}: {
92+
w3cRecords?: W3cCredentialRecord[]
93+
sdJwtRecords?: SdJwtVcRecord[]
94+
} = {}) => ({
95+
w3cCredentials: {
96+
getAll: jest.fn().mockResolvedValue(w3cRecords),
97+
},
98+
sdJwtVc: {
99+
getAll: jest.fn().mockResolvedValue(sdJwtRecords),
100+
},
101+
events: {
102+
observable: {},
103+
},
104+
})
105+
106+
const wrapper = ({ children }: PropsWithChildren) => (
107+
<OpenIDCredentialRecordProvider>{children}</OpenIDCredentialRecordProvider>
108+
)
109+
110+
beforeEach(() => {
111+
jest.clearAllMocks()
112+
113+
mockUseServices.mockImplementation((tokens) => {
114+
expect(tokens).toEqual([TOKENS.UTIL_LOGGER, TOKENS.UTIL_OCA_RESOLVER])
115+
return [logger, bundleResolver]
116+
})
117+
118+
const createSubscription = () => ({
119+
unsubscribe: jest.fn(),
120+
})
121+
122+
mockRecordsAddedByType.mockReturnValue({
123+
subscribe: jest.fn(() => createSubscription()),
124+
})
125+
mockRecordsRemovedByType.mockReturnValue({
126+
subscribe: jest.fn(() => createSubscription()),
127+
})
128+
})
129+
130+
test('loads and filters W3C and SD-JWT records into provider state', async () => {
131+
const jwtW3cRecord = createRecord(W3cCredentialRecord.prototype, {
132+
id: 'w3c-jwt',
133+
getTags: jest.fn().mockReturnValue({ claimFormat: ClaimFormat.JwtVc }),
134+
})
135+
const ldpW3cRecord = createRecord(W3cCredentialRecord.prototype, {
136+
id: 'w3c-ldp',
137+
getTags: jest.fn().mockReturnValue({ claimFormat: ClaimFormat.LdpVc }),
138+
})
139+
const sdJwtRecord = createRecord(SdJwtVcRecord.prototype, {
140+
id: 'sd-jwt',
141+
compactSdJwtVc: 'compact-token',
142+
})
143+
const nonSdJwtRecord = {
144+
id: 'not-sd-jwt',
145+
} as SdJwtVcRecord
146+
147+
mockUseAppAgent.mockReturnValue({
148+
agent: createAgentMock({
149+
w3cRecords: [jwtW3cRecord, ldpW3cRecord],
150+
sdJwtRecords: [sdJwtRecord, nonSdJwtRecord],
151+
}),
152+
})
153+
154+
const { result } = renderHook(() => useOpenIDCredentials(), { wrapper })
155+
156+
await waitFor(() => {
157+
expect(result.current.openIdState.isLoading).toBe(false)
158+
expect(result.current.openIdState.w3cCredentialRecords.map((record) => record.id)).toEqual(['w3c-jwt'])
159+
expect(result.current.openIdState.sdJwtVcRecords.map((record) => record.id)).toEqual(['sd-jwt'])
160+
})
161+
162+
expect(result.current.openIdState.mdocVcRecords).toEqual([])
163+
expect(result.current.openIdState.openIDCredentialRecords).toEqual([])
164+
})
165+
166+
test('delegates credential lookup and storage helpers to credentialRecord utilities', async () => {
167+
const agent = createAgentMock()
168+
const w3cRecord = createRecord(W3cCredentialRecord.prototype, { id: 'w3c-id' })
169+
const sdJwtRecord = createRecord(SdJwtVcRecord.prototype, { id: 'sd-jwt-id' })
170+
const mdocRecord = createRecord(MdocRecord.prototype, { id: 'mdoc-id' })
171+
172+
mockUseAppAgent.mockReturnValue({ agent })
173+
mockGetOpenIDCredentialById.mockImplementation((_agent, type, id) => {
174+
if (type === OpenIDCredentialType.W3cCredential && id === 'w3c-id') return Promise.resolve(w3cRecord)
175+
if (type === OpenIDCredentialType.SdJwtVc && id === 'sd-jwt-id') return Promise.resolve(sdJwtRecord)
176+
if (type === OpenIDCredentialType.Mdoc && id === 'mdoc-id') return Promise.resolve(mdocRecord)
177+
return Promise.resolve(undefined)
178+
})
179+
mockFindOpenIDCredentialById.mockResolvedValue(sdJwtRecord)
180+
181+
const { result } = renderHook(() => useOpenIDCredentials(), { wrapper })
182+
183+
await waitFor(() => {
184+
expect(result.current.openIdState.isLoading).toBe(false)
185+
})
186+
187+
await expect(result.current.getW3CCredentialById('w3c-id')).resolves.toBe(w3cRecord)
188+
await expect(result.current.getSdJwtCredentialById('sd-jwt-id')).resolves.toBe(sdJwtRecord)
189+
await expect(result.current.getMdocCredentialById('mdoc-id')).resolves.toBe(mdocRecord)
190+
await expect(result.current.getCredentialById('w3c-id', OpenIDCredentialType.W3cCredential)).resolves.toBe(w3cRecord)
191+
await expect(result.current.getCredentialById('sd-jwt-id')).resolves.toBe(sdJwtRecord)
192+
193+
await result.current.storeCredential(w3cRecord)
194+
await result.current.removeCredential(sdJwtRecord, OpenIDCredentialType.SdJwtVc)
195+
196+
expect(mockGetOpenIDCredentialById).toHaveBeenCalledWith(agent, OpenIDCredentialType.W3cCredential, 'w3c-id')
197+
expect(mockGetOpenIDCredentialById).toHaveBeenCalledWith(agent, OpenIDCredentialType.SdJwtVc, 'sd-jwt-id')
198+
expect(mockGetOpenIDCredentialById).toHaveBeenCalledWith(agent, OpenIDCredentialType.Mdoc, 'mdoc-id')
199+
expect(mockFindOpenIDCredentialById).toHaveBeenCalledWith(agent, 'sd-jwt-id')
200+
expect(mockStoreOpenIDCredential).toHaveBeenCalledWith(agent, w3cRecord)
201+
expect(mockDeleteOpenIDCredential).toHaveBeenCalledWith(agent, sdJwtRecord)
202+
})
203+
204+
test('resolveBundleForCredential merges display branding with resolved OCA bundle', async () => {
205+
const agent = createAgentMock()
206+
const w3cRecord = createRecord(W3cCredentialRecord.prototype, { id: 'cred-1' })
207+
const display = {
208+
id: 'display-id',
209+
display: {
210+
issuer: { name: 'Issuer Inc.' },
211+
name: 'OpenID Credential',
212+
backgroundColor: '#0a7f3f',
213+
backgroundImage: { uri: 'https://example.com/background.png' },
214+
logo: { uri: 'https://example.com/logo.png' },
215+
},
216+
}
217+
const fields = [{ name: 'given_name', value: 'Ada' }]
218+
const resolvedBundle = {
219+
presentationFields: [{ label: 'Given Name' }],
220+
brandingOverlay: { type: 'original-branding' },
221+
}
222+
223+
mockUseAppAgent.mockReturnValue({ agent })
224+
mockGetCredentialForDisplay.mockReturnValue(display)
225+
mockBuildFieldsFromW3cCredsCredential.mockReturnValue(fields)
226+
bundleResolver.resolveAllBundles.mockResolvedValue(resolvedBundle)
227+
228+
const { result } = renderHook(() => useOpenIDCredentials(), { wrapper })
229+
230+
await waitFor(() => {
231+
expect(result.current.openIdState.isLoading).toBe(false)
232+
})
233+
234+
const bundle = await result.current.resolveBundleForCredential(w3cRecord)
235+
236+
expect(mockGetCredentialForDisplay).toHaveBeenCalledWith(w3cRecord)
237+
expect(mockBuildFieldsFromW3cCredsCredential).toHaveBeenCalledWith(display)
238+
expect(bundleResolver.resolveAllBundles).toHaveBeenCalledWith({
239+
identifiers: {
240+
schemaId: '',
241+
credentialDefinitionId: 'display-id',
242+
},
243+
meta: {
244+
alias: 'Issuer Inc.',
245+
credConnectionId: undefined,
246+
credName: 'OpenID Credential',
247+
},
248+
attributes: fields,
249+
language: 'en',
250+
})
251+
252+
expect(bundle.presentationFields).toEqual(resolvedBundle.presentationFields)
253+
expect(bundle.brandingOverlay).toBeDefined()
254+
expect(bundle.brandingOverlay?.primaryBackgroundColor).toBe('#0a7f3f')
255+
expect(bundle.brandingOverlay?.backgroundImage).toBe('https://example.com/background.png')
256+
expect(bundle.brandingOverlay?.logo).toBe('https://example.com/logo.png')
257+
})
258+
})

0 commit comments

Comments
 (0)