diff options
Diffstat (limited to 'src/__tests__')
-rw-r--r-- | src/__tests__/digiid.test.ts | 145 |
1 files changed, 80 insertions, 65 deletions
diff --git a/src/__tests__/digiid.test.ts b/src/__tests__/digiid.test.ts index d3c29b3..ee01637 100644 --- a/src/__tests__/digiid.test.ts +++ b/src/__tests__/digiid.test.ts @@ -1,15 +1,18 @@ -import { describe, it, expect, vi, afterAll, beforeEach, afterEach } from 'vitest'; -import { generateDigiIDUri, verifyDigiIDCallback, DigiIDError, _internalVerifySignature } from '../index'; // Import from index -import * as crypto from 'crypto'; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +// Import necessary functions and types from the main export +import { DigiIDError, generateDigiIDUri, verifyDigiIDCallback } from '../index'; +// Import Buffer constructor +import { Buffer } from 'buffer'; -// Perform dynamic import at top level +// Perform dynamic import at top level to access internal function for spying const digiidModule = await import('../digiid'); -// Mock the crypto.randomBytes for predictable nonce generation in tests +// Mock crypto.randomBytes for predictable nonce generation in tests vi.mock('crypto', () => ({ - randomBytes: vi.fn((size: number): Buffer => { - // Return a buffer of predictable bytes (e.g., all zeros or a sequence) - return Buffer.alloc(size, 'a'); // Creates a buffer like <Buffer 61 61 61 ...> + // Mock implementation - ignore size parameter as we return a fixed value + randomBytes: vi.fn((/* size: number */): Buffer => { + // Return a fixed Buffer matching the expectedDefaultNonce below + return Buffer.from('61616161616161616161616161616161', 'hex'); }), })); @@ -17,7 +20,8 @@ describe('generateDigiIDUri', () => { const defaultOptions = { callbackUrl: 'https://example.com/callback', }; - const expectedDefaultNonce = '61616161616161616161616161616161'; // 16 bytes of 'a' (0x61) hex encoded + // Matches the fixed buffer returned by the mock (16 bytes of 'a' / 0x61) + const expectedDefaultNonce = '61616161616161616161616161616161'; it('should generate a valid DigiID URI with default nonce and secure flag', () => { const uri = generateDigiIDUri(defaultOptions); @@ -41,7 +45,10 @@ describe('generateDigiIDUri', () => { }); it('should throw error if callbackUrl is missing', () => { + // Use 'as any' for testing invalid input where required properties are missing + // eslint-disable-next-line @typescript-eslint/no-explicit-any expect(() => generateDigiIDUri({} as any)).toThrow(DigiIDError); + // eslint-disable-next-line @typescript-eslint/no-explicit-any expect(() => generateDigiIDUri({} as any)).toThrow('Callback URL is required.'); }); @@ -63,37 +70,41 @@ describe('generateDigiIDUri', () => { 'Callback URL must use https protocol unless unsecure flag is set to true.' ); }); - - // Restore mocks after tests - afterAll(() => { - vi.restoreAllMocks(); - }); }); // --- Verification Tests --- // -// Spy on the internal signature verification helper -let signatureVerifySpy; // Let TypeScript infer the type +// Spy on the internal signature verification helper from the dynamically imported module +// Let assignment define the type implicitly - no explicit type annotation +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let signatureVerifySpy: any; beforeEach(async () => { // Recreate the spy before each test in this suite - // Ensure we spy on the dynamically imported module + // Target the internal function using the dynamic import signatureVerifySpy = vi.spyOn(digiidModule, '_internalVerifySignature'); + // Default mock implementation for most tests + signatureVerifySpy.mockResolvedValue(true); }); afterEach(() => { // Restore the spy after each test - signatureVerifySpy?.mockRestore(); // Add optional chaining in case setup fails + signatureVerifySpy?.mockRestore(); +}); + +// Restore all mocks after the entire suite is done +afterAll(() => { + vi.restoreAllMocks(); }); describe('verifyDigiIDCallback', () => { // Use a syntactically valid Legacy address format - const defaultAddress = 'D7dVskXFpH8gTgZG9sN3d6dPUbJtZfJ2Vc'; - const defaultNonce = '61616161616161616161616161616161'; // Correct nonce from crypto mock + const defaultAddress = 'D7dVskXFpH8gTgZG9sN3d6dPUbJtZfJ2Vc'; + const defaultNonce = '61616161616161616161616161616161'; // Matches the mock const defaultCallback = 'https://example.com/callback'; const defaultUri = `digiid://example.com/callback?x=${defaultNonce}&u=0`; // Use a syntactically valid Base64 string placeholder - const defaultSignature = 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiYw=='; + const defaultSignature = 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiYw=='; const defaultCallbackData = { address: defaultAddress, @@ -106,22 +117,15 @@ describe('verifyDigiIDCallback', () => { expectedNonce: defaultNonce, }; - beforeEach(() => { - // Reset mocks before each inner test - // Note: The spy is already created in the outer beforeEach - signatureVerifySpy.mockClear(); - // Default to valid signature unless overridden in a specific test - signatureVerifySpy.mockResolvedValue(true); - }); - - // Skip tests that depend on mocking the internal helper's outcome reliably - it.skip('should resolve successfully with valid data and signature', async () => { + // Test valid case (signature verification mocked to true) + it('should resolve successfully with valid data and signature (mocked)', async () => { const result = await verifyDigiIDCallback(defaultCallbackData, defaultVerifyOptions); expect(result).toEqual({ isValid: true, address: defaultAddress, nonce: defaultNonce, }); + // Check if the internal verification function was called correctly expect(signatureVerifySpy).toHaveBeenCalledWith(defaultUri, defaultAddress, defaultSignature); expect(signatureVerifySpy).toHaveBeenCalledOnce(); }); @@ -131,6 +135,7 @@ describe('verifyDigiIDCallback', () => { await expect(verifyDigiIDCallback(data, defaultVerifyOptions)).rejects.toThrow( 'Missing required callback data: address, uri, or signature.' ); + expect(signatureVerifySpy).not.toHaveBeenCalled(); }); it('should throw if required callback data is missing (uri)', async () => { @@ -138,41 +143,47 @@ describe('verifyDigiIDCallback', () => { await expect(verifyDigiIDCallback(data, defaultVerifyOptions)).rejects.toThrow( 'Missing required callback data: address, uri, or signature.' ); + expect(signatureVerifySpy).not.toHaveBeenCalled(); }); - + it('should throw if required callback data is missing (signature)', async () => { const data = { ...defaultCallbackData, signature: '' }; await expect(verifyDigiIDCallback(data, defaultVerifyOptions)).rejects.toThrow( 'Missing required callback data: address, uri, or signature.' ); + expect(signatureVerifySpy).not.toHaveBeenCalled(); }); it('should throw for invalid URI format in callback data', async () => { const data = { ...defaultCallbackData, uri: 'invalid-uri-format' }; await expect(verifyDigiIDCallback(data, defaultVerifyOptions)).rejects.toThrow( /^Invalid URI received in callback:/); + expect(signatureVerifySpy).not.toHaveBeenCalled(); }); it('should throw if URI is missing nonce (x)', async () => { const uriWithoutNonce = `digiid://example.com/callback?u=0`; const data = { ...defaultCallbackData, uri: uriWithoutNonce }; await expect(verifyDigiIDCallback(data, defaultVerifyOptions)).rejects.toThrow( - 'URI missing nonce (x) or unsecure (u) parameter.' - ); + 'URI missing nonce (x) or unsecure (u) parameter.' + ); + expect(signatureVerifySpy).not.toHaveBeenCalled(); }); it('should throw if URI is missing unsecure (u)', async () => { const uriWithoutUnsecure = `digiid://example.com/callback?x=${defaultNonce}`; const data = { ...defaultCallbackData, uri: uriWithoutUnsecure }; await expect(verifyDigiIDCallback(data, defaultVerifyOptions)).rejects.toThrow( - 'URI missing nonce (x) or unsecure (u) parameter.' - ); + 'URI missing nonce (x) or unsecure (u) parameter.' + ); + expect(signatureVerifySpy).not.toHaveBeenCalled(); }); it('should throw for invalid expectedCallbackUrl format', async () => { const options = { ...defaultVerifyOptions, expectedCallbackUrl: 'invalid-url' }; await expect(verifyDigiIDCallback(defaultCallbackData, options)).rejects.toThrow( /^Invalid expectedCallbackUrl provided:/); + expect(signatureVerifySpy).not.toHaveBeenCalled(); }); it('should throw if callback URL domain/path mismatch', async () => { @@ -180,31 +191,33 @@ describe('verifyDigiIDCallback', () => { await expect(verifyDigiIDCallback(defaultCallbackData, options)).rejects.toThrow( 'Callback URL mismatch: URI contained "example.com/callback", expected "different.com/callback"' ); + expect(signatureVerifySpy).not.toHaveBeenCalled(); }); - it('should throw if URI indicates unsecure (u=1) but expected URL is https', async () => { + it('should throw if URI indicates unsecure (u=1) but expected URL is https', async () => { const unsecureUri = `digiid://example.com/callback?x=${defaultNonce}&u=1`; const data = { ...defaultCallbackData, uri: unsecureUri }; // Expected URL is still https await expect(verifyDigiIDCallback(data, defaultVerifyOptions)).rejects.toThrow( 'URI indicates unsecure (u=1), but expectedCallbackUrl is not http.' ); + expect(signatureVerifySpy).not.toHaveBeenCalled(); }); it('should throw if URI indicates secure (u=0) but expected URL is http', async () => { - const options = { ...defaultVerifyOptions, expectedCallbackUrl: 'http://example.com/callback' }; + const options = { ...defaultVerifyOptions, expectedCallbackUrl: 'http://example.com/callback' }; // URI is secure (u=0), but expected is http await expect(verifyDigiIDCallback(defaultCallbackData, options)).rejects.toThrow( 'URI indicates secure (u=0), but expectedCallbackUrl is not https.' ); + expect(signatureVerifySpy).not.toHaveBeenCalled(); }); - // Skip test - it.skip('should succeed if URI indicates unsecure (u=1) and expected URL is http', async () => { + it('should succeed if URI indicates unsecure (u=1) and expected URL is http', async () => { const unsecureUri = `digiid://example.com/callback?x=${defaultNonce}&u=1`; const data = { ...defaultCallbackData, uri: unsecureUri }; const options = { ...defaultVerifyOptions, expectedCallbackUrl: 'http://example.com/callback' }; - + const result = await verifyDigiIDCallback(data, options); expect(result.isValid).toBe(true); expect(signatureVerifySpy).toHaveBeenCalledOnce(); @@ -215,45 +228,47 @@ describe('verifyDigiIDCallback', () => { await expect(verifyDigiIDCallback(defaultCallbackData, options)).rejects.toThrow( `Nonce mismatch: URI contained "${defaultNonce}", expected "different-nonce". Possible replay attack.` ); + expect(signatureVerifySpy).not.toHaveBeenCalled(); }); - // Skip test - it.skip('should not throw if nonce matches when expectedNonce is provided', async () => { - const options = { ...defaultVerifyOptions, expectedNonce: defaultNonce }; // Explicitly matching - const result = await verifyDigiIDCallback(defaultCallbackData, options); - expect(result.isValid).toBe(true); - expect(signatureVerifySpy).toHaveBeenCalledOnce(); + it('should not throw if nonce matches when expectedNonce is provided', async () => { + const options = { ...defaultVerifyOptions, expectedNonce: defaultNonce }; // Explicitly matching + const result = await verifyDigiIDCallback(defaultCallbackData, options); + expect(result.isValid).toBe(true); + expect(signatureVerifySpy).toHaveBeenCalledOnce(); }); - // Skip test - it.skip('should not check nonce if expectedNonce is not provided', async () => { - const options = { expectedCallbackUrl: defaultCallback }; // No expectedNonce - const result = await verifyDigiIDCallback(defaultCallbackData, options); - expect(result.isValid).toBe(true); - expect(signatureVerifySpy).toHaveBeenCalledOnce(); + it('should not check nonce if expectedNonce is not provided', async () => { + const options = { expectedCallbackUrl: defaultCallback }; // No expectedNonce + const result = await verifyDigiIDCallback(defaultCallbackData, options); + expect(result.isValid).toBe(true); + expect(signatureVerifySpy).toHaveBeenCalledOnce(); }); - // Skip test - it.skip('should throw if signature verification fails (mocked)', async () => { - signatureVerifySpy.mockResolvedValue(false); // Simulate invalid signature + it('should throw "Invalid signature." if internal verification returns false', async () => { + signatureVerifySpy.mockResolvedValue(false); // Simulate invalid signature from internal check await expect(verifyDigiIDCallback(defaultCallbackData, defaultVerifyOptions)).rejects.toThrow( 'Invalid signature.' ); expect(signatureVerifySpy).toHaveBeenCalledOnce(); }); - // Skip test - it.skip('should throw if signature verification library throws (mocked)', async () => { - const verifyError = new DigiIDError('Signature verification failed: Malformed signature'); - signatureVerifySpy.mockRejectedValue(verifyError); // Simulate library error re-thrown by helper + it('should re-throw DigiIDError from internal verification', async () => { + const internalError = new DigiIDError('Internal checksum failed'); + signatureVerifySpy.mockRejectedValue(internalError); // Simulate internal lib throwing specific error await expect(verifyDigiIDCallback(defaultCallbackData, defaultVerifyOptions)).rejects.toThrow( - verifyError // Expect the exact error instance to be re-thrown + internalError // Expect the exact error instance to be re-thrown ); - // Check message explicitly as well + expect(signatureVerifySpy).toHaveBeenCalledOnce(); + }); + + it('should wrap unexpected errors from internal verification', async () => { + const unexpectedError = new Error('Unexpected network issue'); + signatureVerifySpy.mockRejectedValue(unexpectedError); // Simulate unexpected error await expect(verifyDigiIDCallback(defaultCallbackData, defaultVerifyOptions)).rejects.toThrow( - 'Signature verification failed: Malformed signature' + 'Unexpected error during signature verification: Unexpected network issue' ); - expect(signatureVerifySpy).toHaveBeenCalledTimes(2); // Called twice due to expect().toThrow needing to run the function again + expect(signatureVerifySpy).toHaveBeenCalledOnce(); }); - }); +// Ensure no trailing characters or unclosed comments below this line |