|
| 1 | +import { deriveStealthAddress } from '../../shared/src/crypto/stealth-derive'; |
| 2 | + |
| 3 | +// ── deriveStealthAddress unit tests ────────────────────────────────────────── |
| 4 | + |
| 5 | +describe('deriveStealthAddress', () => { |
| 6 | + const META = 'test-meta-address'; |
| 7 | + const SUB_ID = 'sub-abc-123'; |
| 8 | + |
| 9 | + it('returns a 64-char hex string', () => { |
| 10 | + expect(deriveStealthAddress(META, SUB_ID, 0)).toMatch(/^[0-9a-f]{64}$/); |
| 11 | + }); |
| 12 | + |
| 13 | + it('is deterministic', () => { |
| 14 | + expect(deriveStealthAddress(META, SUB_ID, 0)).toBe(deriveStealthAddress(META, SUB_ID, 0)); |
| 15 | + }); |
| 16 | + |
| 17 | + it('different indices produce different addresses', () => { |
| 18 | + expect(deriveStealthAddress(META, SUB_ID, 0)).not.toBe(deriveStealthAddress(META, SUB_ID, 1)); |
| 19 | + }); |
| 20 | + |
| 21 | + it('different subscription IDs produce different addresses', () => { |
| 22 | + expect(deriveStealthAddress(META, 'sub-111', 0)).not.toBe(deriveStealthAddress(META, 'sub-222', 0)); |
| 23 | + }); |
| 24 | + |
| 25 | + it('different meta-addresses produce different addresses', () => { |
| 26 | + expect(deriveStealthAddress('meta-A', SUB_ID, 0)).not.toBe(deriveStealthAddress('meta-B', SUB_ID, 0)); |
| 27 | + }); |
| 28 | + |
| 29 | + it('100 consecutive indices are all unique (no collisions)', () => { |
| 30 | + const addrs = Array.from({ length: 100 }, (_, i) => deriveStealthAddress(META, SUB_ID, i)); |
| 31 | + expect(new Set(addrs).size).toBe(100); |
| 32 | + }); |
| 33 | + |
| 34 | + it('throws RangeError for negative index', () => { |
| 35 | + expect(() => deriveStealthAddress(META, SUB_ID, -1)).toThrow(RangeError); |
| 36 | + }); |
| 37 | + |
| 38 | + it('throws RangeError for non-integer index', () => { |
| 39 | + expect(() => deriveStealthAddress(META, SUB_ID, 1.5)).toThrow(RangeError); |
| 40 | + }); |
| 41 | +}); |
| 42 | + |
| 43 | +// ── Stealth index assignment in subscription-service tests ─────────────────── |
| 44 | + |
| 45 | +jest.mock('../src/config/database', () => ({ supabase: { from: jest.fn() } })); |
| 46 | +jest.mock('../src/config/logger', () => ({ |
| 47 | + default: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() }, |
| 48 | + __esModule: true, |
| 49 | +})); |
| 50 | +jest.mock('../src/services/blockchain-service', () => ({ |
| 51 | + blockchainService: { syncSubscription: jest.fn() }, |
| 52 | +})); |
| 53 | +jest.mock('../src/services/analytics-service', () => ({ |
| 54 | + analyticsService: { checkBudgetThreshold: jest.fn() }, |
| 55 | +})); |
| 56 | +jest.mock('../src/services/referral-service', () => ({ |
| 57 | + referralService: { markConverted: jest.fn() }, |
| 58 | +})); |
| 59 | +jest.mock('../src/services/renewal-cooldown-service', () => ({ |
| 60 | + renewalCooldownService: { checkCooldown: jest.fn(), recordRenewalAttempt: jest.fn() }, |
| 61 | +})); |
| 62 | +jest.mock('../src/utils/transaction'); |
| 63 | + |
| 64 | +import { subscriptionService } from '../src/services/subscription-service'; |
| 65 | +import { DatabaseTransaction } from '../src/utils/transaction'; |
| 66 | +import { blockchainService } from '../src/services/blockchain-service'; |
| 67 | +import { analyticsService } from '../src/services/analytics-service'; |
| 68 | +import { referralService } from '../src/services/referral-service'; |
| 69 | + |
| 70 | +describe('SubscriptionService — stealth index assignment', () => { |
| 71 | + const USER_ID = 'user-xyz'; |
| 72 | + const BASE = { name: 'Netflix', price: 15.99, billing_cycle: 'monthly' as const }; |
| 73 | + |
| 74 | + beforeEach(() => { |
| 75 | + // resetMocks:true wipes implementations — restore each test |
| 76 | + (analyticsService.checkBudgetThreshold as jest.Mock).mockResolvedValue(undefined); |
| 77 | + (referralService.markConverted as jest.Mock).mockResolvedValue(undefined); |
| 78 | + (blockchainService.syncSubscription as jest.Mock).mockResolvedValue({ success: true }); |
| 79 | + delete process.env.STEALTH_META_ADDRESS; |
| 80 | + }); |
| 81 | + |
| 82 | + function setupTransaction( |
| 83 | + indexRow: { stealth_index: number } | null, |
| 84 | + insertedSub: Record<string, unknown>, |
| 85 | + ) { |
| 86 | + (DatabaseTransaction.execute as jest.Mock).mockImplementation( |
| 87 | + async (fn: (c: any) => Promise<any>) => { |
| 88 | + let call = 0; |
| 89 | + return fn({ |
| 90 | + from: () => { |
| 91 | + call++; |
| 92 | + if (call === 1) { |
| 93 | + return { |
| 94 | + select: () => ({ eq: () => ({ order: () => ({ limit: () => ({ single: () => Promise.resolve({ data: indexRow, error: null }) }) }) }) }), |
| 95 | + }; |
| 96 | + } |
| 97 | + if (call === 2) { |
| 98 | + return { |
| 99 | + insert: () => ({ select: () => ({ single: () => Promise.resolve({ data: insertedSub, error: null }) }) }), |
| 100 | + }; |
| 101 | + } |
| 102 | + // call === 3: UPDATE stealth_address |
| 103 | + return { update: () => ({ eq: () => Promise.resolve({ error: null }) }) }; |
| 104 | + }, |
| 105 | + }); |
| 106 | + }, |
| 107 | + ); |
| 108 | + } |
| 109 | + |
| 110 | + it('assigns stealth_index 0 when no subscriptions exist', async () => { |
| 111 | + setupTransaction(null, { id: 's1', user_id: USER_ID, name: 'Netflix', stealth_index: 0, stealth_address: null }); |
| 112 | + const { subscription } = await subscriptionService.createSubscription(USER_ID, BASE); |
| 113 | + expect(subscription.stealth_index).toBe(0); |
| 114 | + }); |
| 115 | + |
| 116 | + it('assigns stealth_index = max + 1', async () => { |
| 117 | + setupTransaction({ stealth_index: 2 }, { id: 's2', user_id: USER_ID, name: 'Netflix', stealth_index: 3, stealth_address: null }); |
| 118 | + const { subscription } = await subscriptionService.createSubscription(USER_ID, BASE); |
| 119 | + expect(subscription.stealth_index).toBe(3); |
| 120 | + }); |
| 121 | + |
| 122 | + it('derives and persists stealth_address when STEALTH_META_ADDRESS is set', async () => { |
| 123 | + process.env.STEALTH_META_ADDRESS = 'secret-meta'; |
| 124 | + setupTransaction(null, { id: 'sub-stealth', user_id: USER_ID, name: 'Netflix', stealth_index: 0, stealth_address: null }); |
| 125 | + const { subscription } = await subscriptionService.createSubscription(USER_ID, BASE); |
| 126 | + expect(subscription.stealth_address).toBe(deriveStealthAddress('secret-meta', 'sub-stealth', 0)); |
| 127 | + }); |
| 128 | +}); |
0 commit comments