diff options
| author | Pawel Zelawski <pawel.zelawski@outlook.com> | 2025-04-14 10:30:43 +0200 | 
|---|---|---|
| committer | Pawel Zelawski <pawel.zelawski@outlook.com> | 2025-04-14 10:30:43 +0200 | 
| commit | b354d96163e2ba2103f7d8b101dae547eb4747fa (patch) | |
| tree | a1be4510d0b82797a4ed465e534c15924d8d2082 /src/__tests__ | |
| parent | e5a32e3002dfd5c17c847013cd27092f96ac2fba (diff) | |
fix: Correct Bech32 address verification via dependency change
- Replaced faulty 'digibyte-message' dependency with 'bitcoinjs-message'.
- This resolves a critical bug where signatures from DigiByte Bech32 addresses (dgb1...) could not be verified due to issues in the old dependency chain.
- digiid-ts now correctly handles Legacy (D...), SegWit (S...), and Bech32 (dgb1...) address signature verification.
- Updated build configurations and addressed related linting issues revealed during testing.
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  | 
