Skip to content

Conversation

@ignaciosantise
Copy link
Collaborator

Summary

This PR implements Phase 2 of the testing plan - Critical Path Testing, building on top of the Phase 1 testing infrastructure.

New Test Files Added (136 total tests)

Category File Tests Description
Utilities utils/misc.test.ts 10 getDate(), getDeviceIdentifier()
Utilities utils/payment-errors.test.ts 6 getPaymentErrorMessage()
Stores __tests__/store/useLogsStore.test.ts 14 addLog, clearLogs, MAX_LOGS_COUNT limit
Stores __tests__/store/useSettingsStore.test.ts 34 Theme, variant, merchant ID/API key, PIN verification, lockout, biometrics
Services __tests__/services/client.test.ts 16 ApiClient GET/POST/PUT/DELETE, error handling, logging
Services __tests__/services/payment.test.ts 14 getApiHeaders, startPayment, getPaymentStatus
Services __tests__/services/hooks.test.tsx 12 useStartPayment, usePaymentStatus React Query hooks

Additional Changes

  • Fixed lint errors in app/payment-success.tsx (unused import, prettier formatting)
  • Added --forceExit to test script to handle React Query open handles
  • Suppressed React Query act() warnings in jest.setup.js
  • Set __DEV__ = false in tests to prevent console.log noise

Test plan

  • All 136 tests pass (npm test)
  • Lint passes (npm run lint)
  • Clean test output (no console spam)

- Install @testing-library/react-native and react-test-renderer
- Create jest.setup.js with comprehensive mocks for React Native and Expo modules
- Update jest.config.js with coverage settings and module mapping
- Add test utilities in __tests__/utils/:
  - test-helpers.tsx: Component rendering, API mocking, storage mocks
  - store-helpers.ts: Zustand store reset and setup helpers
  - test-helpers.test.ts: Tests for utility functions
- Update eslint.config.js to include Jest globals for test files
- Update AGENTS.md with lint and TypeScript checking instructions
- Fix React Query v5 compatibility (cacheTime -> gcTime)

All tests passing (18 tests)
- Fix crypto mock to produce deterministic but content-aware hashes
  (different inputs now produce different outputs for PIN tests)
- Add state isolation for MMKV mock (new storage per createMMKV call)
- Add state isolation for SecureStore mock (cleared between tests)
- Remove commented console mutation code
- Add documentation comment for node test environment choice
- Return cleanup function from setupTestMerchant for proper teardown
- Fix crypto mock to produce deterministic but content-aware hashes
  (different inputs now produce different outputs for PIN tests)
- Add state isolation for MMKV mock (new storage per createMMKV call)
- Add state isolation for SecureStore mock (cleared between tests)
- Remove commented console mutation code
- Add documentation comment for node test environment choice
- Return cleanup function from setupTestMerchant for proper teardown
Phase 2 tests include:
- Utility tests: misc.ts (getDate, getDeviceIdentifier), payment-errors.ts
- Store tests: useLogsStore.ts (addLog, clearLogs, MAX_LOGS_COUNT)
- Store tests: useSettingsStore.ts (all methods including PIN management)
- Service tests: client.ts (ApiClient class with error handling)
- Service tests: payment.ts (getApiHeaders, startPayment, getPaymentStatus)
- Service tests: hooks.ts (useStartPayment, usePaymentStatus React Query hooks)

Also merges phase-1 testing infrastructure setup.
- Remove unused getDate import from payment-success.tsx
- Fix prettier formatting for tokenDecimals ternary
- Set __DEV__ = false in jest.setup.js to suppress console.log during tests
- Suppress React Query act() warnings in jest.setup.js
- Add --forceExit to test script to handle React Query open handles
@vercel
Copy link

vercel bot commented Jan 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
react-native-examples Ready Ready Preview, Comment Jan 7, 2026 6:31pm

@ignaciosantise
Copy link
Collaborator Author

@claude review

@claude
Copy link

claude bot commented Jan 7, 2026

Claude finished @ignaciosantise's task in 4m 10s —— View job


Code Review Complete

Reviewed Phase 2 test implementation (136 tests across 7 files).

Todo:

  • Gather PR context and changes
  • Review test file implementations
  • Check test quality and patterns
  • Review configuration changes
  • Run automated security checks
  • Post comprehensive review

Issue 1: Missing timeout configuration for API client HTTP requests

ID: client-missing-timeout-7f3a
File: dapps/pos-app/tests/services/client.test.ts:1
Severity: HIGH
Category: reliability

Tests verify API client functionality but don't test timeout behavior. The underlying apiClient uses fetch() without explicit timeout configuration, which could lead to indefinitely hanging requests in production.

Recommendation: Add timeout configuration to the API client and add corresponding tests:

// In client.ts
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout || 30000);
const response = await fetch(url, { ...options, signal: controller.signal });
clearTimeout(timeoutId);

// In test file
it("should timeout long-running requests", async () => {
  jest.useFakeTimers();
  (global.fetch as jest.Mock).mockImplementation(() => 
    new Promise(resolve => setTimeout(resolve, 60000))
  );
  
  const promise = apiClient.get("/slow-endpoint");
  jest.advanceTimersByTime(31000);
  
  await expect(promise).rejects.toThrow(/timeout|aborted/i);
  jest.useRealTimers();
});

Issue 2: React Query hook tests don't verify polling/refetch behavior

ID: hooks-missing-polling-test-8c4d
File: dapps/pos-app/tests/services/hooks.test.tsx:187
Severity: MEDIUM
Category: test_coverage

The usePaymentStatus hook tests verify basic fetching but don't test the critical polling behavior when pollInMs is returned. Payment status polling is a core feature that should be thoroughly tested.

Recommendation: Add polling behavior tests:

it("should poll when status is not final and pollInMs is set", async () => {
  let callCount = 0;
  (getPaymentStatus as jest.Mock).mockImplementation(() => {
    callCount++;
    return Promise.resolve({
      status: callCount < 3 ? "processing" : "succeeded",
      isFinal: callCount >= 3,
      pollInMs: callCount < 3 ? 1000 : 0,
    });
  });

  const { result } = renderHook(() => usePaymentStatus("pay_123"), {
    wrapper: createWrapper(),
  });

  // Wait for multiple polls
  await waitFor(() => {
    expect(callCount).toBeGreaterThanOrEqual(3);
  }, { timeout: 10000 });
  
  expect(result.current.data?.status).toBe("succeeded");
});

Issue 3: PIN verification tests don't validate timing attack resistance

ID: settings-pin-timing-vuln-2a9f
File: dapps/pos-app/tests/store/useSettingsStore.test.ts:243
Severity: HIGH
Category: security

PIN verification tests validate correctness but don't verify constant-time comparison. The mock hash function in jest.setup.js:71 is deterministic but timing attacks could reveal PIN information through comparison timing differences.

Recommendation: Ensure PIN verification uses constant-time comparison in the implementation:

// In useSettingsStore.ts verifyPin method
// Instead of direct string comparison:
// if (hashedPin === storedHash) { ... }

// Use constant-time comparison:
import { timingSafeEqual } from 'crypto';
const inputBuffer = Buffer.from(hashedPin, 'utf-8');
const storedBuffer = Buffer.from(storedHash, 'utf-8');
if (inputBuffer.length === storedBuffer.length && 
    timingSafeEqual(inputBuffer, storedBuffer)) {
  // PIN valid
}

Issue 4: Test uses hardcoded test URLs instead of environment variables

ID: jest-hardcoded-test-urls-4b2c
File: dapps/pos-app/jest.setup.js:272
Severity: LOW
Category: code_quality

The jest.setup.js file sets test environment variables with hardcoded test URLs (https://api.test.example.com). While these are test URLs, using domain patterns consistent with the project or more obviously fake domains would be clearer.

Recommendation: Use RFC 2606 reserved domains for test URLs:

// Replace:
process.env.EXPO_PUBLIC_API_URL = 
  process.env.EXPO_PUBLIC_API_URL || "https://api.test.example.com";

// With:
process.env.EXPO_PUBLIC_API_URL = 
  process.env.EXPO_PUBLIC_API_URL || "https://api.example.test";
// or
process.env.EXPO_PUBLIC_API_URL = 
  process.env.EXPO_PUBLIC_API_URL || "https://api.test.invalid";

Note: .test and .invalid are IANA reserved TLDs per RFC 6761.


Issue 5: Store tests don't verify persistence/hydration behavior

ID: store-missing-persist-test-9d1e
File: dapps/pos-app/tests/store/useSettingsStore.test.ts:16
Severity: MEDIUM
Category: test_coverage

Tests verify store state management but don't test persistence mechanisms. The stores use zustand with persistence (likely MMKV), but tests don't verify that state properly persists and hydrates across store recreation.

Recommendation: Add hydration tests:

it("should persist and hydrate state across store instances", async () => {
  // Set some state
  useSettingsStore.getState().setThemeMode("dark");
  useSettingsStore.getState().setMerchantId("merchant-123");
  
  // Simulate app restart - get current persisted state
  const persistedState = { ...useSettingsStore.getState() };
  
  // Reset store and verify hydration
  useSettingsStore.getState().setHasHydrated(false);
  
  // Trigger hydration (implementation-specific)
  // Then verify state matches
  expect(useSettingsStore.getState().themeMode).toBe("dark");
  expect(useSettingsStore.getState().merchantId).toBe("merchant-123");
});

Issue 6: Missing edge case tests for payment amount handling

ID: payment-amount-edge-cases-3c7b
File: dapps/pos-app/tests/services/payment.test.ts:117
Severity: MEDIUM
Category: test_coverage

Payment service tests verify happy paths but don't test edge cases for amount handling: zero amounts, negative amounts, very large amounts, non-numeric values, or decimal precision issues.

Recommendation: Add edge case tests:

describe("payment amount edge cases", () => {
  beforeEach(async () => {
    await setupTestMerchant("merchant-123", "api-key-456");
  });

  it("should reject zero amount", async () => {
    await expect(startPayment({
      referenceId: "ref-123",
      amount: { value: "0", unit: "cents" }
    })).rejects.toThrow(/amount.*zero/i);
  });

  it("should reject negative amount", async () => {
    await expect(startPayment({
      referenceId: "ref-123",
      amount: { value: "-100", unit: "cents" }
    })).rejects.toThrow(/amount.*negative/i);
  });

  it("should handle maximum safe integer amount", async () => {
    const maxSafe = Number.MAX_SAFE_INTEGER.toString();
    // Test implementation
  });
});

Automated Checks

All external URLs detected are test URLs (api.test.example.com, gateway.test.example.com) or documentation references (reactnative.dev, sentry.io, base64.guru, solscan.io, rpc.walletconnect.org). No unapproved external domain issues.


Summary

Test implementation is comprehensive with good coverage of critical paths. Main concerns:

  1. HIGH: Missing timeout configuration for HTTP requests (production reliability risk)
  2. HIGH: PIN verification may be vulnerable to timing attacks (security risk)
  3. MEDIUM: Incomplete test coverage for polling behavior, persistence, and edge cases

Recommended to address HIGH severity issues before merging, MEDIUM severity issues can be tracked for follow-up.

--- | Branch: feature/testing-phase-2

expect(result.current.data).toBeUndefined();
});

it("should start fetching when paymentId is provided and enabled", async () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Auto Review Issue: React Query hook tests don't verify polling/refetch behavior

Severity: MEDIUM
Category: test_coverage
Tool: Claude Auto Review

Recommendation: Add polling behavior tests:

it("should poll when status is not final and pollInMs is set", async () => {
  let callCount = 0;
  (getPaymentStatus as jest.Mock).mockImplementation(() => {
    callCount++;
    return Promise.resolve({
      status: callCount < 3 ? "processing" : "succeeded",
      isFinal: callCount >= 3,
      pollInMs: callCount < 3 ? 1000 : 0,
    });
  });

  const { result } = renderHook(() => usePaymentStatus("pay_123"), {
    wrapper: createWrapper(),
  });

  // Wait for multiple polls
  await waitFor(() => {
    expect(callCount).toBeGreaterThanOrEqual(3);
  }, { timeout: 10000 });
  
  expect(result.current.data?.status).toBe("succeeded");
});

expect(useSettingsStore.getState().pinLockoutUntil).toBeNull();
});
});

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Auto Review Issue: PIN verification tests don't validate timing attack resistance

Severity: HIGH
Category: security
Tool: Claude Auto Review

Recommendation: Ensure PIN verification uses constant-time comparison in the implementation:

// In useSettingsStore.ts verifyPin method
// Instead of direct string comparison:
// if (hashedPin === storedHash) { ... }

// Use constant-time comparison:
import { timingSafeEqual } from 'crypto';
const inputBuffer = Buffer.from(hashedPin, 'utf-8');
const storedBuffer = Buffer.from(storedHash, 'utf-8');
if (inputBuffer.length === storedBuffer.length && 
    timingSafeEqual(inputBuffer, storedBuffer)) {
  // PIN valid
}

}
});

describe("initial state", () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Auto Review Issue: Store tests don't verify persistence/hydration behavior

Severity: MEDIUM
Category: test_coverage
Tool: Claude Auto Review

Recommendation: Add hydration tests:

it("should persist and hydrate state across store instances", async () => {
  // Set some state
  useSettingsStore.getState().setThemeMode("dark");
  useSettingsStore.getState().setMerchantId("merchant-123");
  
  // Simulate app restart - get current persisted state
  const persistedState = { ...useSettingsStore.getState() };
  
  // Reset store and verify hydration
  useSettingsStore.getState().setHasHydrated(false);
  
  // Trigger hydration (implementation-specific)
  // Then verify state matches
  expect(useSettingsStore.getState().themeMode).toBe("dark");
  expect(useSettingsStore.getState().merchantId).toBe("merchant-123");
});

});
});

describe("startPayment", () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Auto Review Issue: Missing edge case tests for payment amount handling

Severity: MEDIUM
Category: test_coverage
Tool: Claude Auto Review

Recommendation: Add edge case tests:

describe("payment amount edge cases", () => {
  beforeEach(async () => {
    await setupTestMerchant("merchant-123", "api-key-456");
  });

  it("should reject zero amount", async () => {
    await expect(startPayment({
      referenceId: "ref-123",
      amount: { value: "0", unit: "cents" }
    })).rejects.toThrow(/amount.*zero/i);
  });

  it("should reject negative amount", async () => {
    await expect(startPayment({
      referenceId: "ref-123",
      amount: { value: "-100", unit: "cents" }
    })).rejects.toThrow(/amount.*negative/i);
  });

  it("should handle maximum safe integer amount", async () => {
    const maxSafe = Number.MAX_SAFE_INTEGER.toString();
    // Test implementation
  });
});

Issue 1: Add timeout configuration to API client
- Added AbortController-based timeout (default 30s) to all HTTP requests
- Added timeout error handling with TIMEOUT error code
- Added tests for timeout behavior

Issue 2: Add polling behavior tests for usePaymentStatus hook
- Added tests verifying polling stops on terminal states (succeeded/failed)
- Added test for onTerminalState callback during polling

Issue 3: Add timing-safe PIN comparison (security)
- Implemented timingSafeEqual() function using constant-time XOR comparison
- Updated verifyPin() to use timing-safe comparison
- Added test for timing-safe PIN verification

Issue 5: Add store persistence/hydration tests
- Added hydration state tracking tests for both stores
- Added tests for state preservation through hydration
- Added tests for PIN and biometric state persistence
@ignaciosantise
Copy link
Collaborator Author

@claude review

@claude
Copy link

claude bot commented Jan 7, 2026

Claude finished @ignaciosantise's task in 2m 18s —— View job


🔄 Incremental Review: Phase 2 Test Implementation

Reviewed latest commits addressing previous review feedback.

Todo:

  • Gather PR context and analyze changed files
  • Review previous automated review comments for issue IDs
  • Verify if previously reported issues still exist
  • Analyze test implementations for new issues
  • Run automated security checks
  • Post comprehensive review findings

Previously Reported Issues - Status Update

✅ Resolved Issues

1. client-missing-timeout-7f3a (HIGH) - API timeout configuration

  • Status: RESOLVED
  • Implementation at dapps/pos-app/services/client.ts:14-44 now includes DEFAULT_TIMEOUT_MS (30s) and AbortController for proper timeout handling
  • Tests verify timeout behavior at client.test.ts:236-284

2. hooks-missing-polling-test-8c4d (MEDIUM) - Missing React Query polling tests

  • Status: RESOLVED
  • Comprehensive polling tests added at hooks.test.tsx:278-402
  • Tests verify polling stops at terminal states and callback invocation

3. settings-pin-timing-vuln-2a9f (HIGH) - PIN timing attack vulnerability

  • Status: RESOLVED
  • Timing-safe comparison implemented at useSettingsStore.ts:21-39
  • Test coverage at useSettingsStore.test.ts:329-355

⚠️ Still Open Issues

4. jest-hardcoded-test-urls-4b2c (LOW) - Test environment configuration

  • ID: jest-hardcoded-test-urls-4b2c
  • Status: STILL EXISTS at jest.setup.js:272-277
  • Uses test.example.com instead of RFC 6761 reserved domains

5. payment-amount-edge-cases-3c7b (MEDIUM) - Missing payment validation tests

  • ID: payment-amount-edge-cases-3c7b
  • Status: NOT ADDRESSED
  • No tests for zero/negative amounts, overflow, or decimal precision

New Issues Found

Issue 1: React Query cache not isolated between hook tests

ID: hooks-query-cache-leak-9a3f
File: dapps/pos-app/tests/services/hooks.test.tsx:26
Severity: MEDIUM
Category: test_quality

QueryClient instances created in createWrapper() are cleared in afterEach, but tests create new clients per render without ensuring previous queries are fully settled. This can cause test pollution.

Recommendation: Add explicit cleanup:

afterEach(async () => {
  await clearTestMerchant();
  if (queryClient) {
    queryClient.clear();
    await queryClient.cancelQueries(); // Add this
  }
});

Issue 2: Store hydration tests don't verify MMKV persistence layer

ID: store-mmkv-persist-gap-7d2e
File: dapps/pos-app/tests/store/useSettingsStore.test.ts:502
Severity: LOW
Category: test_coverage

Tests at lines 502-569 verify state changes persist within the same store instance, but don't test the MMKV storage layer integration. The mock at jest.setup.js:112-136 creates isolated Map instances per createMMKV() call, so cross-instance persistence is untested.

Recommendation: Test store recreation scenario:

it("should persist state across store recreation", async () => {
  // Set state
  useSettingsStore.getState().setMerchantId("merchant-test");
  
  // Get underlying MMKV storage
  const mmkvStorage = useSettingsStore.persist.getOptions().storage;
  const persistedData = mmkvStorage.getString?.("settings");
  
  // Reset store state (simulating app restart)
  useSettingsStore.setState({ merchantId: null });
  
  // Manually rehydrate from persisted data
  // Verify merchantId restored to "merchant-test"
});

Issue 3: Test environment variables could expose real endpoints

ID: jest-env-var-override-risk-4c2b
File: dapps/pos-app/jest.setup.js:272
Severity: LOW
Category: security

Lines 272-277 use process.env.VAR || "default" pattern. If developer accidentally sets real production URLs in environment, tests would hit real endpoints.

Recommendation: Use explicit test-only values:

// Force test URLs regardless of environment
process.env.EXPO_PUBLIC_API_URL = "https://api.test.invalid";
process.env.EXPO_PUBLIC_GATEWAY_URL = "https://gateway.test.invalid";

Issue 4: Mock fetch doesn't validate request body structure

ID: client-test-missing-body-validation-8e1f
File: dapps/pos-app/tests/services/client.test.ts:84
Severity: LOW
Category: test_quality

Tests verify JSON.stringify(requestBody) is called but don't validate the serialized body matches expected API contract. Malformed objects could pass tests but fail in production.

Recommendation: Add body validation:

it("should serialize request body correctly", async () => {
  const requestBody = { amount: "10.00", currency: "USD" };
  
  (global.fetch as jest.Mock).mockImplementation((url, options) => {
    const body = JSON.parse(options.body);
    expect(body).toEqual(requestBody);
    expect(body.amount).toBe("10.00"); // Verify string type preserved
    return Promise.resolve({
      ok: true,
      status: 200,
      json: jest.fn().mockResolvedValue({ paymentId: "pay_123" })
    });
  });
  
  await apiClient.post("/payments", requestBody);
});

Automated Checks

External Domain URLs: ✅ No unapproved external domains detected. All URLs in tests use test.example.com, test.invalid, or documentation sites (reactnative.dev, etc.).

Static Resource Cache-Control: N/A - No static resource cache headers in test files.

GitHub Actions Workflow Security: N/A - No workflow files modified.

WalletConnect Pay Architecture: N/A - This is a POS app test suite, not WalletConnect Pay service code.


Summary

Resolved since last review: 3 HIGH/MEDIUM issues fixed (timeout, polling tests, timing-safe PIN comparison)

Remaining issues: 2 previously reported (test URLs, payment edge cases) + 4 new findings

Recommendation: Address payment-amount-edge-cases-3c7b (MEDIUM) to prevent production issues with invalid payment amounts. Other findings are test quality improvements that can be addressed in follow-up.


| Branch: feature/testing-phase-2

@ignaciosantise ignaciosantise changed the title feat: Add Phase 2 tests - Critical path testing feat: Add critical path testing (Phase 2) Jan 7, 2026
}));

describe("Payment Hooks", () => {
let queryClient: QueryClient;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Auto Review Issue: React Query cache not isolated between hook tests

Severity: MEDIUM
Category: test_quality
Tool: Claude Auto Review

Recommendation: Add explicit cleanup:

afterEach(async () => {
  await clearTestMerchant();
  if (queryClient) {
    queryClient.clear();
    await queryClient.cancelQueries(); // Add this
  }
});

});
});

describe("persistence and hydration", () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Auto Review Issue: Store hydration tests don't verify MMKV persistence layer

Severity: LOW
Category: test_coverage
Tool: Claude Auto Review

Recommendation: Test store recreation scenario:

it("should persist state across store recreation", async () => {
  // Set state
  useSettingsStore.getState().setMerchantId("merchant-test");
  
  // Get underlying MMKV storage
  const mmkvStorage = useSettingsStore.persist.getOptions().storage;
  const persistedData = mmkvStorage.getString?.("settings");
  
  // Reset store state (simulating app restart)
  useSettingsStore.setState({ merchantId: null });
  
  // Manually rehydrate from persisted data
  // Verify merchantId restored to "merchant-test"
});

});

describe("POST requests", () => {
it("should make a POST request with JSON body", async () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Auto Review Issue: Mock fetch doesn't validate request body structure

Severity: LOW
Category: test_quality
Tool: Claude Auto Review

Recommendation: Add body validation:

it("should serialize request body correctly", async () => {
  const requestBody = { amount: "10.00", currency: "USD" };
  
  (global.fetch as jest.Mock).mockImplementation((url, options) => {
    const body = JSON.parse(options.body);
    expect(body).toEqual(requestBody);
    expect(body.amount).toBe("10.00"); // Verify string type preserved
    return Promise.resolve({
      ok: true,
      status: 200,
      json: jest.fn().mockResolvedValue({ paymentId: "pay_123" })
    });
  });
  
  await apiClient.post("/payments", requestBody);
});

Issue 1: React Query cache isolation
- Added cancelQueries() to afterEach cleanup in hooks tests

Issue 2: MMKV persistence layer verification
- Added tests for persist configuration (name, version, storage)
- Added tests for persist API availability (rehydrate, hasHydrated)

Issue 3: Test environment variable security
- Changed from fallback pattern to forced test-only URLs
- Prevents accidental real endpoint calls during tests

Issue 4: Request body validation
- Added test verifying JSON serialization preserves types
- Tests string, number, boolean, and nested object types
Base automatically changed from feature/testing-phase-1 to main January 7, 2026 18:05
- Configure notifyManager to use synchronous scheduler in tests
- Add removeQueries() to afterEach cleanup
- Add staleTime: 0 to test QueryClient config
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements Phase 2 of the testing plan, adding 136 comprehensive tests for critical paths across utilities, stores, and services. The PR also introduces timing-safe PIN comparison for enhanced security and timeout handling for API requests.

Key Changes:

  • Added 136 unit tests covering utilities (date formatting, device ID, error messages), stores (logs, settings with PIN/biometric management), and services (API client, payment operations, React Query hooks)
  • Implemented timing-safe string comparison function to prevent timing attacks on PIN verification
  • Added configurable timeout handling with AbortController for API requests

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
utils/payment-errors.test.ts Tests for payment error message mapping
utils/misc.test.ts Tests for date formatting and device identifier utilities
__tests__/store/useLogsStore.test.ts Tests for logs store including MAX_LOGS_COUNT limit behavior
__tests__/store/useSettingsStore.test.ts Comprehensive tests for settings store covering theme, variant, merchant config, PIN verification, lockout, and biometrics
__tests__/services/client.test.ts Tests for ApiClient HTTP methods, error handling, timeout, and logging
__tests__/services/payment.test.ts Tests for payment service functions including API headers and payment operations
__tests__/services/hooks.test.tsx Tests for React Query hooks including polling behavior and terminal states
store/useSettingsStore.ts Added timing-safe comparison function for PIN verification security
services/client.ts Added timeout support with AbortController for API requests
app/payment-success.tsx Fixed lint issues (removed unused import, formatting)
package.json Added --forceExit flag to test script for React Query cleanup
jest.setup.js Configured test environment (disabled DEV, suppressed React Query warnings)
.gitignore Added TESTING_PLAN.md to ignored files

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +266 to +283
describe("timeout handling", () => {
it("should use AbortController signal for fetch", async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
status: 200,
json: jest.fn().mockResolvedValue({ success: true }),
});

await apiClient.get("/test");

// Verify fetch was called with an AbortSignal
expect(global.fetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
signal: expect.any(AbortSignal),
}),
);
});
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on lines 272-276 states "Verify fetch was called with an AbortSignal" but the test doesn't actually verify that the AbortSignal is functioning correctly - it only checks that an AbortSignal object was passed. The timeout functionality (setting up the timeout, clearing it properly) is not actually tested here.

Consider adding a more comprehensive test that actually triggers the timeout by making fetch delay longer than the timeout value, or update the comment to reflect that this test only verifies the signal is passed, not that the timeout mechanism works correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +38
// Length check - but still perform full comparison to maintain constant time
const lengthMatch = a.length === b.length;

// Use the longer string length to ensure we always do the same amount of work
const maxLength = Math.max(a.length, b.length);

let result = 0;
for (let i = 0; i < maxLength; i++) {
// Use charCodeAt with fallback to 0 for out-of-bounds
const charA = i < a.length ? a.charCodeAt(i) : 0;
const charB = i < b.length ? b.charCodeAt(i) : 0;
// XOR accumulates any differences
result |= charA ^ charB;
}

// Both length must match AND all characters must be equal (result === 0)
return lengthMatch && result === 0;
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timing-safe comparison implementation has a security vulnerability. While the function attempts to maintain constant time by always iterating through maxLength, it still leaks information through the early length check assignment. The lengthMatch variable is computed before the constant-time comparison, and modern compilers or JIT optimizations could potentially reorder these operations. Additionally, using charCodeAt with fallback to 0 for out-of-bounds access still reveals length information through the actual character comparisons performed.

A more secure approach would be to ensure both strings are padded to the same length before comparison, or use a cryptographic library's timing-safe comparison function if available.

Suggested change
// Length check - but still perform full comparison to maintain constant time
const lengthMatch = a.length === b.length;
// Use the longer string length to ensure we always do the same amount of work
const maxLength = Math.max(a.length, b.length);
let result = 0;
for (let i = 0; i < maxLength; i++) {
// Use charCodeAt with fallback to 0 for out-of-bounds
const charA = i < a.length ? a.charCodeAt(i) : 0;
const charB = i < b.length ? b.charCodeAt(i) : 0;
// XOR accumulates any differences
result |= charA ^ charB;
}
// Both length must match AND all characters must be equal (result === 0)
return lengthMatch && result === 0;
// Normalize both strings to the same length to avoid length-based branching
const maxLength = Math.max(a.length, b.length);
const paddedA = a.padEnd(maxLength, "\0");
const paddedB = b.padEnd(maxLength, "\0");
let result = 0;
for (let i = 0; i < maxLength; i++) {
// XOR accumulates any differences
result |= paddedA.charCodeAt(i) ^ paddedB.charCodeAt(i);
}
// All characters must be equal (result === 0) for strings to be considered equal
return result === 0;

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +21
callback();
});

// Suppress React Query's act() warnings in tests
// These warnings come from React Query's internal state management and are expected
const originalConsoleError = console.error;
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suppressing React Query's act() warnings by intercepting console.error is not recommended. These warnings exist to help identify improper async handling in tests. While React Query may generate some expected warnings, blindly suppressing them could hide real issues in component tests where state updates aren't properly wrapped in act().

A better approach would be to properly structure tests to avoid these warnings, or use more targeted suppression that only filters specific React Query warnings while allowing other important console.error messages through.

Suggested change
callback();
});
// Suppress React Query's act() warnings in tests
// These warnings come from React Query's internal state management and are expected
const originalConsoleError = console.error;
const [message] = args;
const isActWarning =
typeof message === "string" && message.includes("not wrapped in act(...)");
const isFromReactQuery = args.some(
(arg) =>
typeof arg === "string" &&
(arg.includes("react-query") || arg.includes("@tanstack/react-query"))
);
if (isActWarning && isFromReactQuery) {
// Ignore known React Query act() warnings
return;
}

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +47

it("returns default message when called without arguments", () => {
const result = getPaymentErrorMessage();
expect(result).toBe(
"The payment couldn't be completed due to an error. Please try again or use a different payment method.",
);
});
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test on line 42-47 calls getPaymentErrorMessage() without any arguments, but the function signature shows the parameter as optional (errorStatus?: string). TypeScript should allow this, but it's testing the same code path as the "undefined" test on lines 35-40. This test is redundant and should be removed to reduce duplication and improve test clarity.

Suggested change
it("returns default message when called without arguments", () => {
const result = getPaymentErrorMessage();
expect(result).toBe(
"The payment couldn't be completed due to an error. Please try again or use a different payment method.",
);
});

Copilot uses AI. Check for mistakes.
@ignaciosantise ignaciosantise merged commit 0e785cf into main Jan 7, 2026
6 checks passed
@ignaciosantise ignaciosantise deleted the feature/testing-phase-2 branch January 7, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants