Greenlens/__tests__/services/mockBackendService.test.ts

144 lines
4.4 KiB
TypeScript

import AsyncStorage from '@react-native-async-storage/async-storage';
import { mockBackendService } from '../../services/backend/mockBackendService';
jest.mock('@react-native-async-storage/async-storage', () => ({
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
}));
const asyncStorageMemory: Record<string, string> = {};
const mockedAsyncStorage = AsyncStorage as jest.Mocked<typeof AsyncStorage>;
const runScan = async (userId: string, idempotencyKey: string) => {
const settledPromise = mockBackendService.scanPlant({
userId,
idempotencyKey,
imageUri: `data:image/jpeg;base64,${idempotencyKey}`,
language: 'en',
}).then(
value => ({ ok: true as const, value }),
error => ({ ok: false as const, error }),
);
await Promise.resolve();
await jest.runAllTimersAsync();
const settled = await settledPromise;
if (!settled.ok) throw settled.error;
return settled.value;
};
describe('mockBackendService billing simulation', () => {
beforeEach(() => {
jest.useFakeTimers();
Object.keys(asyncStorageMemory).forEach((key) => {
delete asyncStorageMemory[key];
});
mockedAsyncStorage.getItem.mockImplementation(async (key: string) => {
return key in asyncStorageMemory ? asyncStorageMemory[key] : null;
});
mockedAsyncStorage.setItem.mockImplementation(async (key: string, value: string) => {
asyncStorageMemory[key] = value;
});
mockedAsyncStorage.removeItem.mockImplementation(async (key: string) => {
delete asyncStorageMemory[key];
});
});
afterEach(() => {
jest.useRealTimers();
jest.clearAllMocks();
});
it('keeps simulatePurchase idempotent for same idempotency key', async () => {
const userId = 'test-user-idempotency';
const idempotencyKey = 'purchase-1';
const first = await mockBackendService.simulatePurchase({
userId,
idempotencyKey,
productId: 'topup_small',
});
const second = await mockBackendService.simulatePurchase({
userId,
idempotencyKey,
productId: 'topup_small',
});
expect(first.billing.credits.topupBalance).toBe(25);
expect(second.billing.credits.topupBalance).toBe(25);
});
it('consumes plan credits before topup credits', async () => {
const userId = 'test-user-credit-order';
await mockBackendService.simulatePurchase({
userId,
idempotencyKey: 'topup-order-1',
productId: 'topup_small',
});
let lastScan = await runScan(userId, 'scan-order-0');
expect(lastScan.billing.credits.usedThisCycle).toBe(1);
expect(lastScan.billing.credits.topupBalance).toBe(25);
for (let i = 1; i <= 15; i += 1) {
lastScan = await runScan(userId, `scan-order-${i}`);
}
expect(lastScan.billing.credits.usedThisCycle).toBe(15);
expect(lastScan.billing.credits.topupBalance).toBe(24);
});
it('can deplete all available credits via webhook simulation', async () => {
const userId = 'test-user-deplete-credits';
await mockBackendService.simulatePurchase({
userId,
idempotencyKey: 'topup-deplete-1',
productId: 'topup_small',
});
const response = await mockBackendService.simulateWebhook({
userId,
idempotencyKey: 'webhook-deplete-1',
event: 'credits_depleted',
});
expect(response.billing.credits.available).toBe(0);
expect(response.billing.credits.topupBalance).toBe(0);
expect(response.billing.credits.usedThisCycle).toBe(response.billing.credits.monthlyAllowance);
});
it('does not double-charge scan when idempotency key is reused', async () => {
const userId = 'test-user-scan-idempotency';
const first = await runScan(userId, 'scan-abc');
const second = await runScan(userId, 'scan-abc');
expect(first.creditsCharged).toBe(1);
expect(second.creditsCharged).toBe(1);
expect(second.billing.credits.available).toBe(first.billing.credits.available);
});
it('enforces free monthly credit limit', async () => {
const userId = 'test-user-credit-limit';
let successfulScans = 0;
let errorCode: string | null = null;
for (let i = 0; i < 30; i += 1) {
try {
await runScan(userId, `scan-${i}`);
successfulScans += 1;
} catch (error) {
errorCode = (error as { code?: string }).code || null;
break;
}
}
expect(errorCode).toBe('INSUFFICIENT_CREDITS');
expect(successfulScans).toBeGreaterThanOrEqual(7);
expect(successfulScans).toBeLessThanOrEqual(15);
});
});