Initial commit for Greenlens
This commit is contained in:
143
__tests__/services/mockBackendService.test.ts
Normal file
143
__tests__/services/mockBackendService.test.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user