Skip to content

Commit

Permalink
chore: SSISDK-5 refactor and additional test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
sanderPostma committed Feb 12, 2025
1 parent 342f440 commit e1c8bc2
Show file tree
Hide file tree
Showing 11 changed files with 2,533 additions and 2,112 deletions.
46 changes: 8 additions & 38 deletions packages/client/lib/CredentialOfferClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,15 @@ import {
CredentialOfferRequestWithBaseUrl,
CredentialOfferV1_0_11,
CredentialOfferV1_0_13,
decodeJsonProperties,
determineSpecVersionFromURI,
getClientIdFromCredentialOfferPayload,
getURIComponentsAsArray,
OpenId4VCIVersion,
PRE_AUTH_CODE_LITERAL,
PRE_AUTH_GRANT_LITERAL,
toUniformCredentialOfferRequest
} from '@sphereon/oid4vci-common'
import Debug from 'debug'

import { LOG } from './types'
import { fetch } from 'cross-fetch'
import { isUrlEncoded } from './functions'
import { constructBaseResponse, handleCredentialOfferUri } from './functions'

const debug = Debug('sphereon:oid4vci:offer');

Expand Down Expand Up @@ -48,20 +43,7 @@ export class CredentialOfferClient {
};
} else {
if (uri.includes('credential_offer_uri')) {
const uriObj = getURIComponentsAsArray(uri) as unknown as Record<string, string> // FIXME
const credentialOfferUri = decodeURIComponent(uriObj['credential_offer_uri'])
const decodedUri = isUrlEncoded(credentialOfferUri) ? decodeURIComponent(credentialOfferUri) : credentialOfferUri
const response = await fetch(decodedUri)
if (!(response && response.status >= 200 && response.status < 400)) {
return Promise.reject(`the credential offer URI endpoint call was not successful. http code ${response.status} - reason ${response.statusText}`)
}

if (response.headers.get('Content-Type')?.startsWith('application/json') === false) {
return Promise.reject('the credential offer URI endpoint did not return content type application/json')
}
credentialOffer = {
credential_offer: decodeJsonProperties(await response.json())
} as CredentialOfferV1_0_11 | CredentialOfferV1_0_13
credentialOffer = await handleCredentialOfferUri(uri) as CredentialOfferV1_0_11 | CredentialOfferV1_0_13
} else {
credentialOffer = convertURIToJsonObject(uri, {
// It must have the '=' sign after credential_offer otherwise the uri will get split at openid_credential_offer
Expand All @@ -70,34 +52,22 @@ export class CredentialOfferClient {
}) as CredentialOfferV1_0_11 | CredentialOfferV1_0_13
}
if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) {
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri)
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri) // cannot be reached since convertURIToJsonObject will check the params

Check warning on line 55 in packages/client/lib/CredentialOfferClient.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClient.ts#L55

Added line #L55 was not covered by tests
}
}

const request = await toUniformCredentialOfferRequest(credentialOffer, {
...opts,
version,
});
const clientId = getClientIdFromCredentialOfferPayload(request.credential_offer);
const grants = request.credential_offer?.grants;
version
})

return {
scheme,
baseUrl,
...(clientId && { clientId }),
...request,
...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),
...(grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL] && {
preAuthorizedCode: grants[PRE_AUTH_GRANT_LITERAL][PRE_AUTH_CODE_LITERAL],
}),
...constructBaseResponse(request, scheme, baseUrl),
userPinRequired:
request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.user_pin_required ??
!!request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code ??
false,
...(request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code && {
txCode: request.credential_offer.grants[PRE_AUTH_GRANT_LITERAL].tx_code,
}),
};
false

Check warning on line 69 in packages/client/lib/CredentialOfferClient.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClient.ts#L69

Added line #L69 was not covered by tests
}
}

public static toURI(
Expand Down
2 changes: 1 addition & 1 deletion packages/client/lib/CredentialOfferClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class CredentialOfferClientV1_0_11 {
requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri='] : ['credential_offer='],
}) as CredentialOfferV1_0_11;
if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) {
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri);
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri); // cannot be reached since convertURIToJsonObject will check the params

Check warning on line 47 in packages/client/lib/CredentialOfferClientV1_0_11.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClientV1_0_11.ts#L47

Added line #L47 was not covered by tests
}
}

Expand Down
55 changes: 13 additions & 42 deletions packages/client/lib/CredentialOfferClientV1_0_13.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,30 @@ import {
convertURIToJsonObject,
CredentialOffer,
CredentialOfferRequestWithBaseUrl,
CredentialOfferV1_0_11,
CredentialOfferV1_0_13,
decodeJsonProperties,
determineSpecVersionFromURI,
getClientIdFromCredentialOfferPayload,
getURIComponentsAsArray,
OpenId4VCIVersion,
PRE_AUTH_CODE_LITERAL,
PRE_AUTH_GRANT_LITERAL,
toUniformCredentialOfferRequest
} from '@sphereon/oid4vci-common'
import Debug from 'debug'
import { fetch } from 'cross-fetch'
import { isUrlEncoded } from './functions'
import { constructBaseResponse, handleCredentialOfferUri } from './functions'

const debug = Debug('sphereon:oid4vci:offer');

export class CredentialOfferClientV1_0_13 {
public static async fromURI(uri: string, opts?: { resolve?: boolean }): Promise<CredentialOfferRequestWithBaseUrl> {
debug(`Credential Offer URI: ${uri}`);
debug(`Credential Offer URI: ${uri}`)

Check warning on line 19 in packages/client/lib/CredentialOfferClientV1_0_13.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClientV1_0_13.ts#L19

Added line #L19 was not covered by tests
if (!uri.includes('?') || !uri.includes('://')) {
debug(`Invalid Credential Offer URI: ${uri}`);
throw Error(`Invalid Credential Offer Request`);
debug(`Invalid Credential Offer URI: ${uri}`)
throw Error(`Invalid Credential Offer Request`)

Check warning on line 22 in packages/client/lib/CredentialOfferClientV1_0_13.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClientV1_0_13.ts#L21-L22

Added lines #L21 - L22 were not covered by tests
}
const scheme = uri.split('://')[0];
const baseUrl = uri.split('?')[0];
const scheme = uri.split('://')[0]
const baseUrl = uri.split('?')[0]
const version = determineSpecVersionFromURI(uri)

Check warning on line 26 in packages/client/lib/CredentialOfferClientV1_0_13.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClientV1_0_13.ts#L24-L26

Added lines #L24 - L26 were not covered by tests
let credentialOffer: CredentialOffer
if (uri.includes('credential_offer_uri')) { // FIXME deduplicate
const uriObj = getURIComponentsAsArray(uri) as unknown as Record<string, string> // FIXME
const credentialOfferUri = decodeURIComponent(uriObj['credential_offer_uri'])
const decodedUri = isUrlEncoded(credentialOfferUri) ? decodeURIComponent(credentialOfferUri) : credentialOfferUri
const response = await fetch(decodedUri)
if (!(response && response.status >= 200 && response.status < 400)) {
return Promise.reject(`the credential offer URI endpoint call was not successful. http code ${response.status} - reason ${response.statusText}`)
}

if (response.headers.get('Content-Type')?.startsWith('application/json') === false) {
return Promise.reject('the credential offer URI endpoint did not return content type application/json')
}
credentialOffer = decodeJsonProperties(await response.json()) as CredentialOfferV1_0_11 | CredentialOfferV1_0_13
credentialOffer = await handleCredentialOfferUri(uri) as CredentialOfferV1_0_13
} else {
credentialOffer = convertURIToJsonObject(uri, {

Check warning on line 31 in packages/client/lib/CredentialOfferClientV1_0_13.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClientV1_0_13.ts#L29-L31

Added lines #L29 - L31 were not covered by tests
// It must have the '=' sign after credential_offer otherwise the uri will get split at openid_credential_offer
Expand All @@ -54,30 +37,18 @@ export class CredentialOfferClientV1_0_13 {
}) as CredentialOfferV1_0_13
}
if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) {
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri)
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri) // cannot be reached since convertURIToJsonObject will check the params

Check warning on line 40 in packages/client/lib/CredentialOfferClientV1_0_13.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClientV1_0_13.ts#L40

Added line #L40 was not covered by tests
}

const request = await toUniformCredentialOfferRequest(credentialOffer, {
...opts,
version,
});
const clientId = getClientIdFromCredentialOfferPayload(request.credential_offer);
const grants = request.credential_offer?.grants;
version
})

return {
scheme,
baseUrl,
...(clientId && { clientId }),
...request,
...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),
...(grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL] && {
preAuthorizedCode: grants[PRE_AUTH_GRANT_LITERAL][PRE_AUTH_CODE_LITERAL],
}),
userPinRequired: !!request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code ?? false,
...(request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code && {
txCode: request.credential_offer.grants[PRE_AUTH_GRANT_LITERAL].tx_code,
}),
};
...constructBaseResponse(request, scheme, baseUrl),
userPinRequired: !!request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code ?? false
}
}

public static toURI(
Expand Down
59 changes: 59 additions & 0 deletions packages/client/lib/__tests__/CredentialRequestClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,62 @@ describe('Credential Request Client with different issuers ', () => {
});
});
});

describe('Credential Offer Client error handling', () => {
beforeEach(() => {
nock.cleanAll()
})

afterEach(() => {
nock.cleanAll()
})

it('should handle non-200 response from credential offer URI endpoint', async () => {
const IRR_URI = 'openid-credential-offer://?credential_offer_uri=https%3A%2F%2Ftest.example.com%2Foffer'

nock('https://test.example.com')
.get('/offer')
.reply(404, 'Not Found')

await expect(CredentialOfferClient.fromURI(IRR_URI)).rejects.toMatch(
/the credential offer URI endpoint call was not successful. http code 404 - reason Not Found/
)
})

it('should handle invalid content type from credential offer URI endpoint', async () => {
const IRR_URI = 'openid-credential-offer://?credential_offer_uri=https%3A%2F%2Ftest.example.com%2Foffer'

nock('https://test.example.com')
.get('/offer')
.reply(200, 'plain text response', { 'Content-Type': 'text/plain' })

await expect(CredentialOfferClient.fromURI(IRR_URI)).rejects.toMatch(
'the credential offer URI endpoint did not return content type application/json'
)
})

it('should handle missing required credential offer properties', async () => {
const IRR_URI = 'openid-credential-offer://?invalid_param=test'

await expect(CredentialOfferClient.fromURI(IRR_URI)).rejects.toThrow('Wrong parameters provided')
})

it('should handle credential offer URI with credential_offer param', async () => {
const IRR_URI = 'openid-credential-offer://?credential_offer=%7B%22test%22%3A%22value%22%7D'

const client = await CredentialOfferClient.fromURI(IRR_URI)
expect(client.credential_offer).toBeDefined()
})

it('should handle URL encoded credential offer URI properly', async () => {
const encodedUri = 'https%3A%2F%2Ftest.example.com%2Foffer'
const IRR_URI = `openid-credential-offer://?credential_offer_uri=${encodedUri}`

nock('https://test.example.com')
.get('/offer')
.reply(200, { test: 'value' }, { 'Content-Type': 'application/json' })

const client = await CredentialOfferClient.fromURI(IRR_URI)
expect(client.credential_offer).toBeDefined()
})
})
52 changes: 52 additions & 0 deletions packages/client/lib/functions/CredentialOfferCommons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
decodeJsonProperties,
getClientIdFromCredentialOfferPayload,
getURIComponentsAsArray,
PRE_AUTH_CODE_LITERAL,
PRE_AUTH_GRANT_LITERAL,
UniformCredentialOfferRequest
} from '@sphereon/oid4vci-common'
import { fetch } from 'cross-fetch'

export function isUriEncoded(str: string): boolean {
const pattern = /%[0-9A-F]{2}/i
return pattern.test(str)
}

export async function handleCredentialOfferUri(uri: string) {
const uriObj = getURIComponentsAsArray(uri) as unknown as Record<string, string>
const credentialOfferUri = decodeURIComponent(uriObj['credential_offer_uri'])
const decodedUri = isUriEncoded(credentialOfferUri) ? decodeURIComponent(credentialOfferUri) : credentialOfferUri
const response = await fetch(decodedUri)

if (!(response && response.status >= 200 && response.status < 400)) {
return Promise.reject(`the credential offer URI endpoint call was not successful. http code ${response.status} - reason ${response.statusText}`)
}

if (response.headers.get('Content-Type')?.startsWith('application/json') === false) {
return Promise.reject('the credential offer URI endpoint did not return content type application/json')
}

return {
credential_offer: decodeJsonProperties(await response.json())
}
}

export function constructBaseResponse(request: UniformCredentialOfferRequest, scheme: string, baseUrl: string) {
const clientId = getClientIdFromCredentialOfferPayload(request.credential_offer)
const grants = request.credential_offer?.grants

return {
scheme,
baseUrl,
...(clientId && { clientId }),
...request,
...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),
...(grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL] && {
preAuthorizedCode: grants[PRE_AUTH_GRANT_LITERAL][PRE_AUTH_CODE_LITERAL]
}),
...(request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code && {
txCode: request.credential_offer.grants[PRE_AUTH_GRANT_LITERAL].tx_code
})
}
}
5 changes: 0 additions & 5 deletions packages/client/lib/functions/UrlUtil.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/client/lib/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ export * from './AuthorizationUtil';
export * from './notifications';
export * from './OpenIDUtils';
export * from './AccessTokenUtil';
export * from './UrlUtil';
export * from './CredentialOfferCommons';
Loading

0 comments on commit e1c8bc2

Please sign in to comment.