Onboarding
This commit is contained in:
40
__tests__/server/authAccountDeletion.test.js
Normal file
40
__tests__/server/authAccountDeletion.test.js
Normal file
@@ -0,0 +1,40 @@
|
||||
jest.mock('../../server/lib/postgres', () => ({
|
||||
get: jest.fn(),
|
||||
run: jest.fn(),
|
||||
}));
|
||||
|
||||
const { get, run } = require('../../server/lib/postgres');
|
||||
const { deleteAccount, signUp } = require('../../server/lib/auth');
|
||||
|
||||
describe('server auth account deletion', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
get.mockResolvedValue(null);
|
||||
run.mockResolvedValue({ lastId: null, changes: 1, rows: [] });
|
||||
});
|
||||
|
||||
it('removes auth and billing rows so the same email can sign up again', async () => {
|
||||
const email = 'same@example.com';
|
||||
|
||||
await signUp({}, email, 'First User', 'password-1');
|
||||
await deleteAccount({}, 'usr_deleted');
|
||||
await signUp({}, email, 'Second User', 'password-2');
|
||||
|
||||
const authDeletes = run.mock.calls.filter(([, sql]) => (
|
||||
typeof sql === 'string' && sql.includes('DELETE FROM auth_users')
|
||||
));
|
||||
expect(authDeletes).toHaveLength(1);
|
||||
|
||||
const billingAccountDeletes = run.mock.calls.filter(([, sql]) => (
|
||||
typeof sql === 'string' && sql.includes('DELETE FROM billing_accounts')
|
||||
));
|
||||
expect(billingAccountDeletes).toHaveLength(1);
|
||||
|
||||
const signupChecks = get.mock.calls.filter(([, sql], params) => (
|
||||
typeof sql === 'string'
|
||||
&& sql.includes('SELECT id FROM auth_users WHERE LOWER(email)')
|
||||
&& params?.[0] === email
|
||||
));
|
||||
expect(signupChecks).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { mockBackendService } from '../../services/backend/mockBackendService';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { mockBackendService } from '../../services/backend/mockBackendService';
|
||||
import { openAiScanService } from '../../services/backend/openAiScanService';
|
||||
|
||||
jest.mock('@react-native-async-storage/async-storage', () => ({
|
||||
getItem: jest.fn(),
|
||||
@@ -11,7 +12,7 @@ const asyncStorageMemory: Record<string, string> = {};
|
||||
|
||||
const mockedAsyncStorage = AsyncStorage as jest.Mocked<typeof AsyncStorage>;
|
||||
|
||||
const runScan = async (userId: string, idempotencyKey: string) => {
|
||||
const runScan = async (userId: string, idempotencyKey: string) => {
|
||||
const settledPromise = mockBackendService.scanPlant({
|
||||
userId,
|
||||
idempotencyKey,
|
||||
@@ -26,7 +27,33 @@ const runScan = async (userId: string, idempotencyKey: string) => {
|
||||
const settled = await settledPromise;
|
||||
if (!settled.ok) throw settled.error;
|
||||
return settled.value;
|
||||
};
|
||||
};
|
||||
|
||||
const runHealthCheck = async (userId: string, idempotencyKey: string) => {
|
||||
const settledPromise = mockBackendService.healthCheck({
|
||||
userId,
|
||||
idempotencyKey,
|
||||
imageUri: `data:image/jpeg;base64,${idempotencyKey}`,
|
||||
language: 'en',
|
||||
plantContext: {
|
||||
name: 'Monstera',
|
||||
botanicalName: 'Monstera deliciosa',
|
||||
careInfo: {
|
||||
waterIntervalDays: 7,
|
||||
light: 'Bright indirect light',
|
||||
temp: '18-24C',
|
||||
},
|
||||
},
|
||||
}).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(() => {
|
||||
@@ -48,10 +75,11 @@ describe('mockBackendService billing simulation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
jest.restoreAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('keeps simulatePurchase idempotent for same idempotency key', async () => {
|
||||
const userId = 'test-user-idempotency';
|
||||
@@ -142,6 +170,40 @@ describe('mockBackendService billing simulation', () => {
|
||||
expect(second.billing.credits.available).toBe(first.billing.credits.available);
|
||||
});
|
||||
|
||||
it('charges one credit for a normal scan after a two-credit health check', async () => {
|
||||
const userId = 'test-user-health-then-scan-cost';
|
||||
jest.spyOn(openAiScanService, 'isConfigured').mockReturnValue(true);
|
||||
jest.spyOn(openAiScanService, 'analyzePlantHealth').mockResolvedValue({
|
||||
overallHealthScore: 72,
|
||||
status: 'watch',
|
||||
analysisSummary: 'Mild stress signs are visible.',
|
||||
likelyIssues: [
|
||||
{
|
||||
title: 'Watering stress',
|
||||
confidence: 0.62,
|
||||
details: 'The leaf texture suggests inconsistent watering.',
|
||||
},
|
||||
],
|
||||
actionsNow: ['Check soil moisture before watering.'],
|
||||
plan7Days: ['Take a comparison photo in one week.'],
|
||||
});
|
||||
|
||||
await mockBackendService.simulatePurchase({
|
||||
userId,
|
||||
idempotencyKey: 'sub-health-then-scan-cost',
|
||||
productId: 'monthly_pro',
|
||||
});
|
||||
|
||||
const healthCheck = await runHealthCheck(userId, 'health-cost-1');
|
||||
expect(healthCheck.creditsCharged).toBe(2);
|
||||
expect(healthCheck.billing.credits.usedThisCycle).toBe(2);
|
||||
|
||||
const scan = await runScan(userId, 'scan-after-health-cost-1');
|
||||
expect(scan.modelPath).toContain('mock-review');
|
||||
expect(scan.creditsCharged).toBe(1);
|
||||
expect(scan.billing.credits.usedThisCycle).toBe(3);
|
||||
});
|
||||
|
||||
it('blocks free users from real scans', async () => {
|
||||
const userId = 'test-user-credit-limit';
|
||||
let successfulScans = 0;
|
||||
|
||||
Reference in New Issue
Block a user