test(desktop): Phase 3 store unit tests — 112 new tests for 5 stores
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- saasStore: login/logout/register, TOTP setup/verify/disable, billing (plans/subscription/payment), templates, connection mode, config sync - workflowStore: CRUD, trigger, cancel, loadRuns, client injection - offlineStore: queue message, update/remove, reconnect backoff, getters - handStore: loadHands, getHandDetails, trigger/approve/cancel, triggers CRUD, approvals, autonomy blocking - streamStore: chatMode switching, getChatModeConfig, suggestions, setIsLoading, cancelStream, searchSkills, initStreamListener All 173 tests pass (61 existing + 112 new).
This commit is contained in:
586
desktop/tests/store/saasStore.test.ts
Normal file
586
desktop/tests/store/saasStore.test.ts
Normal file
@@ -0,0 +1,586 @@
|
||||
/**
|
||||
* SaaS Store Tests
|
||||
*
|
||||
* Tests for SaaS login/logout, connection mode, billing,
|
||||
* TOTP, and session management.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { localStorageMock } from '../setup';
|
||||
|
||||
// ── Mock saas-client (use vi.hoisted to ensure availability before hoisted vi.mock) ──
|
||||
const {
|
||||
mockSetBaseUrl, mockSetToken, mockLogin, mockRegister, mockMe,
|
||||
mockRestoreFromCookie, mockListModels, mockRegisterDevice, mockDeviceHeartbeat,
|
||||
mockSetupTotp, mockVerifyTotp, mockDisableTotp,
|
||||
mockListPlans, mockGetSubscription, mockCreatePayment, mockGetPaymentStatus,
|
||||
mockFetchAvailableTemplates, mockGetAssignedTemplate, mockAssignTemplate, mockUnassignTemplate,
|
||||
mockPullConfig, mockSyncConfig, mockComputeConfigDiff, mockIsAuthenticated,
|
||||
} = vi.hoisted(() => ({
|
||||
mockSetBaseUrl: vi.fn(),
|
||||
mockSetToken: vi.fn(),
|
||||
mockLogin: vi.fn(),
|
||||
mockRegister: vi.fn(),
|
||||
mockMe: vi.fn(),
|
||||
mockRestoreFromCookie: vi.fn(),
|
||||
mockListModels: vi.fn(),
|
||||
mockRegisterDevice: vi.fn(),
|
||||
mockDeviceHeartbeat: vi.fn(),
|
||||
mockSetupTotp: vi.fn(),
|
||||
mockVerifyTotp: vi.fn(),
|
||||
mockDisableTotp: vi.fn(),
|
||||
mockListPlans: vi.fn(),
|
||||
mockGetSubscription: vi.fn(),
|
||||
mockCreatePayment: vi.fn(),
|
||||
mockGetPaymentStatus: vi.fn(),
|
||||
mockFetchAvailableTemplates: vi.fn(),
|
||||
mockGetAssignedTemplate: vi.fn(),
|
||||
mockAssignTemplate: vi.fn(),
|
||||
mockUnassignTemplate: vi.fn(),
|
||||
mockPullConfig: vi.fn(),
|
||||
mockSyncConfig: vi.fn(),
|
||||
mockComputeConfigDiff: vi.fn(),
|
||||
mockIsAuthenticated: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/lib/saas-client', () => ({
|
||||
saasClient: {
|
||||
setBaseUrl: mockSetBaseUrl,
|
||||
setToken: mockSetToken,
|
||||
login: mockLogin,
|
||||
register: mockRegister,
|
||||
me: mockMe,
|
||||
restoreFromCookie: mockRestoreFromCookie,
|
||||
listModels: mockListModels,
|
||||
registerDevice: mockRegisterDevice,
|
||||
deviceHeartbeat: mockDeviceHeartbeat,
|
||||
setupTotp: mockSetupTotp,
|
||||
verifyTotp: mockVerifyTotp,
|
||||
disableTotp: mockDisableTotp,
|
||||
listPlans: mockListPlans,
|
||||
getSubscription: mockGetSubscription,
|
||||
createPayment: mockCreatePayment,
|
||||
getPaymentStatus: mockGetPaymentStatus,
|
||||
fetchAvailableTemplates: mockFetchAvailableTemplates,
|
||||
getAssignedTemplate: mockGetAssignedTemplate,
|
||||
assignTemplate: mockAssignTemplate,
|
||||
unassignTemplate: mockUnassignTemplate,
|
||||
pullConfig: mockPullConfig,
|
||||
syncConfig: mockSyncConfig,
|
||||
computeConfigDiff: mockComputeConfigDiff,
|
||||
isAuthenticated: mockIsAuthenticated,
|
||||
},
|
||||
SaaSApiError: class SaaSApiError extends Error {
|
||||
code: string;
|
||||
status: number;
|
||||
constructor(msg: string, code: string, status: number) {
|
||||
super(msg);
|
||||
this.code = code;
|
||||
this.status = status;
|
||||
}
|
||||
},
|
||||
loadSaaSSession: vi.fn(async () => null),
|
||||
loadSaaSSessionSync: vi.fn(() => null),
|
||||
saveSaaSSession: vi.fn(async () => {}),
|
||||
clearSaaSSession: vi.fn(async () => {}),
|
||||
saveConnectionMode: vi.fn(),
|
||||
loadConnectionMode: vi.fn(() => 'tauri'),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/lib/telemetry-collector', () => ({
|
||||
initTelemetryCollector: vi.fn(),
|
||||
stopTelemetryCollector: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/lib/llm-service', () => ({
|
||||
startPromptOTASync: vi.fn(),
|
||||
stopPromptOTASync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/lib/logger', () => ({
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Import after mocks
|
||||
import { useSaaSStore, type ConnectionMode } from '../../src/store/saasStore';
|
||||
|
||||
const ACCOUNT = {
|
||||
id: 'acc-1',
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
role: 'user',
|
||||
totp_enabled: false,
|
||||
created_at: '2026-01-01',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorageMock.clear();
|
||||
|
||||
useSaaSStore.setState({
|
||||
isLoggedIn: false,
|
||||
account: null,
|
||||
saasUrl: 'http://127.0.0.1:8080',
|
||||
authToken: null,
|
||||
connectionMode: 'tauri',
|
||||
availableModels: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
totpRequired: false,
|
||||
totpSetupData: null,
|
||||
saasReachable: true,
|
||||
availableTemplates: [],
|
||||
assignedTemplate: null,
|
||||
_consecutiveFailures: 0,
|
||||
_heartbeatTimer: undefined,
|
||||
_healthCheckTimer: undefined,
|
||||
plans: [],
|
||||
subscription: null,
|
||||
billingLoading: false,
|
||||
billingError: null,
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Initial State
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('saasStore — initial state', () => {
|
||||
it('should start not logged in', () => {
|
||||
expect(useSaaSStore.getState().isLoggedIn).toBe(false);
|
||||
});
|
||||
|
||||
it('should have no account', () => {
|
||||
expect(useSaaSStore.getState().account).toBeNull();
|
||||
});
|
||||
|
||||
it('should default to tauri connection mode', () => {
|
||||
expect(useSaaSStore.getState().connectionMode).toBe('tauri');
|
||||
});
|
||||
|
||||
it('should have empty models', () => {
|
||||
expect(useSaaSStore.getState().availableModels).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not be loading', () => {
|
||||
expect(useSaaSStore.getState().isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// login
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('saasStore — login', () => {
|
||||
it('should login successfully', async () => {
|
||||
mockLogin.mockResolvedValue({
|
||||
token: 'test-token',
|
||||
account: ACCOUNT,
|
||||
});
|
||||
mockRegisterDevice.mockResolvedValue({});
|
||||
mockListModels.mockResolvedValue([]);
|
||||
mockFetchAvailableTemplates.mockResolvedValue([]);
|
||||
mockGetAssignedTemplate.mockResolvedValue(null);
|
||||
mockListPlans.mockResolvedValue([]);
|
||||
mockGetSubscription.mockResolvedValue(null);
|
||||
mockPullConfig.mockResolvedValue({ configs: [], pulled_at: '2026-01-01' });
|
||||
|
||||
await useSaaSStore.getState().login('http://localhost:8080', 'testuser', 'password');
|
||||
|
||||
const state = useSaaSStore.getState();
|
||||
expect(state.isLoggedIn).toBe(true);
|
||||
expect(state.account).toEqual(ACCOUNT);
|
||||
expect(state.connectionMode).toBe('saas');
|
||||
expect(state.isLoading).toBe(false);
|
||||
expect(state.error).toBeNull();
|
||||
expect(mockSetBaseUrl).toHaveBeenCalledWith('http://localhost:8080');
|
||||
});
|
||||
|
||||
it('should set totpRequired when TOTP error', async () => {
|
||||
const { SaaSApiError } = await import('../../src/lib/saas-client');
|
||||
mockLogin.mockRejectedValue(new SaaSApiError('TOTP required', 'TOTP_ERROR', 400));
|
||||
|
||||
await useSaaSStore.getState().login('http://localhost:8080', 'testuser', 'password');
|
||||
|
||||
expect(useSaaSStore.getState().totpRequired).toBe(true);
|
||||
expect(useSaaSStore.getState().isLoggedIn).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate empty username', async () => {
|
||||
await expect(
|
||||
useSaaSStore.getState().login('http://localhost:8080', ' ', 'password')
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should validate empty password', async () => {
|
||||
await expect(
|
||||
useSaaSStore.getState().login('http://localhost:8080', 'testuser', '')
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle login error', async () => {
|
||||
mockLogin.mockRejectedValue(new Error('Invalid credentials'));
|
||||
|
||||
await expect(
|
||||
useSaaSStore.getState().login('http://localhost:8080', 'testuser', 'wrong')
|
||||
).rejects.toThrow('Invalid credentials');
|
||||
|
||||
expect(useSaaSStore.getState().isLoggedIn).toBe(false);
|
||||
expect(useSaaSStore.getState().error).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should detect connection refused errors', async () => {
|
||||
mockLogin.mockRejectedValue(new Error('Failed to fetch'));
|
||||
|
||||
await expect(
|
||||
useSaaSStore.getState().login('http://localhost:8080', 'testuser', 'password')
|
||||
).rejects.toThrow();
|
||||
|
||||
expect(useSaaSStore.getState().error).toContain('无法连接');
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// loginWithTotp
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('saasStore — loginWithTotp', () => {
|
||||
it('should login with TOTP code', async () => {
|
||||
mockLogin.mockResolvedValue({
|
||||
token: 'test-token',
|
||||
account: { ...ACCOUNT, totp_enabled: true },
|
||||
});
|
||||
mockRegisterDevice.mockResolvedValue({});
|
||||
mockListModels.mockResolvedValue([]);
|
||||
|
||||
await useSaaSStore.getState().loginWithTotp('http://localhost:8080', 'testuser', 'password', '123456');
|
||||
|
||||
expect(useSaaSStore.getState().isLoggedIn).toBe(true);
|
||||
expect(useSaaSStore.getState().totpRequired).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle TOTP login error', async () => {
|
||||
mockLogin.mockRejectedValue(new Error('Invalid TOTP code'));
|
||||
|
||||
await expect(
|
||||
useSaaSStore.getState().loginWithTotp('http://localhost:8080', 'testuser', 'password', '000000')
|
||||
).rejects.toThrow('Invalid TOTP code');
|
||||
|
||||
expect(useSaaSStore.getState().isLoggedIn).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// register
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('saasStore — register', () => {
|
||||
it('should register and auto-login', async () => {
|
||||
mockRegister.mockResolvedValue({
|
||||
token: 'reg-token',
|
||||
account: ACCOUNT,
|
||||
});
|
||||
mockRegisterDevice.mockResolvedValue({});
|
||||
mockListModels.mockResolvedValue([]);
|
||||
|
||||
await useSaaSStore.getState().register('http://localhost:8080', 'newuser', 'new@example.com', 'password');
|
||||
|
||||
expect(useSaaSStore.getState().isLoggedIn).toBe(true);
|
||||
expect(useSaaSStore.getState().connectionMode).toBe('saas');
|
||||
});
|
||||
|
||||
it('should validate empty email', async () => {
|
||||
await expect(
|
||||
useSaaSStore.getState().register('http://localhost:8080', 'user', '', 'password')
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// logout
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('saasStore — logout', () => {
|
||||
it('should clear all state on logout', async () => {
|
||||
useSaaSStore.setState({
|
||||
isLoggedIn: true,
|
||||
account: ACCOUNT,
|
||||
connectionMode: 'saas',
|
||||
availableModels: [{ id: 'm1', name: 'test', provider: 'p' }] as any,
|
||||
plans: [{ id: 'p1', name: 'Pro' }] as any,
|
||||
subscription: { id: 's1', plan_id: 'p1' } as any,
|
||||
});
|
||||
|
||||
await useSaaSStore.getState().logout();
|
||||
|
||||
const state = useSaaSStore.getState();
|
||||
expect(state.isLoggedIn).toBe(false);
|
||||
expect(state.account).toBeNull();
|
||||
expect(state.authToken).toBeNull();
|
||||
expect(state.connectionMode).toBe('tauri');
|
||||
expect(state.availableModels).toEqual([]);
|
||||
expect(state.plans).toEqual([]);
|
||||
expect(state.subscription).toBeNull();
|
||||
expect(mockSetToken).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// setConnectionMode
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('saasStore — setConnectionMode', () => {
|
||||
it('should switch to gateway mode', () => {
|
||||
useSaaSStore.getState().setConnectionMode('gateway');
|
||||
expect(useSaaSStore.getState().connectionMode).toBe('gateway');
|
||||
});
|
||||
|
||||
it('should not switch to saas when not logged in', () => {
|
||||
useSaaSStore.setState({ isLoggedIn: false });
|
||||
useSaaSStore.getState().setConnectionMode('saas');
|
||||
expect(useSaaSStore.getState().connectionMode).toBe('tauri');
|
||||
});
|
||||
|
||||
it('should allow saas mode when logged in', () => {
|
||||
useSaaSStore.setState({ isLoggedIn: true, account: ACCOUNT });
|
||||
useSaaSStore.getState().setConnectionMode('saas');
|
||||
expect(useSaaSStore.getState().connectionMode).toBe('saas');
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// fetchAvailableModels
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('saasStore — fetchAvailableModels', () => {
|
||||
it('should fetch models when logged in', async () => {
|
||||
useSaaSStore.setState({ isLoggedIn: true, saasUrl: 'http://localhost:8080' });
|
||||
mockListModels.mockResolvedValue([
|
||||
{ id: 'glm-4', name: 'GLM-4', provider: 'zhipu' },
|
||||
{ id: 'gpt-4', name: 'GPT-4', provider: 'openai' },
|
||||
]);
|
||||
|
||||
await useSaaSStore.getState().fetchAvailableModels();
|
||||
|
||||
expect(useSaaSStore.getState().availableModels).toHaveLength(2);
|
||||
expect(useSaaSStore.getState().availableModels[0].id).toBe('glm-4');
|
||||
});
|
||||
|
||||
it('should clear models when not logged in', async () => {
|
||||
useSaaSStore.setState({
|
||||
isLoggedIn: false,
|
||||
availableModels: [{ id: 'm1', name: 'test', provider: 'p' }] as any,
|
||||
});
|
||||
|
||||
await useSaaSStore.getState().fetchAvailableModels();
|
||||
|
||||
expect(useSaaSStore.getState().availableModels).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle fetch error gracefully', async () => {
|
||||
useSaaSStore.setState({ isLoggedIn: true, saasUrl: 'http://localhost:8080' });
|
||||
mockListModels.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await useSaaSStore.getState().fetchAvailableModels();
|
||||
|
||||
expect(useSaaSStore.getState().availableModels).toEqual([]);
|
||||
// Should not set global error
|
||||
expect(useSaaSStore.getState().error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// TOTP
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('saasStore — TOTP', () => {
|
||||
it('should setup TOTP', async () => {
|
||||
const setupData = { secret: 'JBSWY3DPEHPK3PXP', qr_code_url: 'otpauth://...' };
|
||||
mockSetupTotp.mockResolvedValue(setupData);
|
||||
|
||||
const result = await useSaaSStore.getState().setupTotp();
|
||||
|
||||
expect(result).toEqual(setupData);
|
||||
expect(useSaaSStore.getState().totpSetupData).toEqual(setupData);
|
||||
expect(useSaaSStore.getState().isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should verify TOTP', async () => {
|
||||
useSaaSStore.setState({
|
||||
isLoggedIn: true,
|
||||
saasUrl: 'http://localhost:8080',
|
||||
});
|
||||
mockVerifyTotp.mockResolvedValue({});
|
||||
mockMe.mockResolvedValue({ ...ACCOUNT, totp_enabled: true });
|
||||
|
||||
await useSaaSStore.getState().verifyTotp('123456');
|
||||
|
||||
expect(useSaaSStore.getState().totpSetupData).toBeNull();
|
||||
expect(useSaaSStore.getState().account?.totp_enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should disable TOTP', async () => {
|
||||
useSaaSStore.setState({ isLoggedIn: true, saasUrl: 'http://localhost:8080' });
|
||||
mockDisableTotp.mockResolvedValue({});
|
||||
mockMe.mockResolvedValue({ ...ACCOUNT, totp_enabled: false });
|
||||
|
||||
await useSaaSStore.getState().disableTotp('password123');
|
||||
|
||||
expect(useSaaSStore.getState().account?.totp_enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle TOTP setup error', async () => {
|
||||
mockSetupTotp.mockRejectedValue(new Error('Setup failed'));
|
||||
|
||||
await expect(
|
||||
useSaaSStore.getState().setupTotp()
|
||||
).rejects.toThrow('Setup failed');
|
||||
|
||||
expect(useSaaSStore.getState().isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('cancelTotpSetup should clear setup data', () => {
|
||||
useSaaSStore.setState({ totpSetupData: { secret: 'abc', qr_code_url: 'url' } as any });
|
||||
useSaaSStore.getState().cancelTotpSetup();
|
||||
expect(useSaaSStore.getState().totpSetupData).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Billing
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('saasStore — billing', () => {
|
||||
it('should fetch billing overview', async () => {
|
||||
useSaaSStore.setState({ isLoggedIn: true, saasUrl: 'http://localhost:8080' });
|
||||
mockListPlans.mockResolvedValue([{ id: 'p1', name: 'Pro', price: 99 }]);
|
||||
mockGetSubscription.mockResolvedValue({ id: 's1', plan_id: 'p1', status: 'active' });
|
||||
|
||||
await useSaaSStore.getState().fetchBillingOverview();
|
||||
|
||||
const state = useSaaSStore.getState();
|
||||
expect(state.plans).toHaveLength(1);
|
||||
expect(state.subscription).not.toBeNull();
|
||||
expect(state.billingLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle billing fetch error', async () => {
|
||||
useSaaSStore.setState({ isLoggedIn: true, saasUrl: 'http://localhost:8080' });
|
||||
mockListPlans.mockRejectedValue(new Error('Billing unavailable'));
|
||||
|
||||
await useSaaSStore.getState().fetchBillingOverview();
|
||||
|
||||
expect(useSaaSStore.getState().billingLoading).toBe(false);
|
||||
expect(useSaaSStore.getState().billingError).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should create payment', async () => {
|
||||
useSaaSStore.setState({ isLoggedIn: true, saasUrl: 'http://localhost:8080' });
|
||||
const paymentResult = { payment_id: 'pay-1', status: 'pending', payment_url: 'https://pay.example.com' };
|
||||
mockCreatePayment.mockResolvedValue(paymentResult);
|
||||
|
||||
const result = await useSaaSStore.getState().createPayment('plan-1', 'alipay');
|
||||
|
||||
expect(result).toEqual(paymentResult);
|
||||
expect(mockCreatePayment).toHaveBeenCalledWith({
|
||||
plan_id: 'plan-1',
|
||||
payment_method: 'alipay',
|
||||
});
|
||||
});
|
||||
|
||||
it('should poll payment status and refresh on success', async () => {
|
||||
useSaaSStore.setState({ isLoggedIn: true, saasUrl: 'http://localhost:8080' });
|
||||
mockGetPaymentStatus.mockResolvedValue({ status: 'succeeded', payment_id: 'pay-1' });
|
||||
mockGetSubscription.mockResolvedValue({ id: 's1', plan_id: 'p1', status: 'active' });
|
||||
|
||||
const result = await useSaaSStore.getState().pollPaymentStatus('pay-1');
|
||||
|
||||
expect(result!.status).toBe('succeeded');
|
||||
expect(mockGetSubscription).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle payment creation error', async () => {
|
||||
useSaaSStore.setState({ isLoggedIn: true, saasUrl: 'http://localhost:8080' });
|
||||
mockCreatePayment.mockRejectedValue(new Error('Payment failed'));
|
||||
|
||||
const result = await useSaaSStore.getState().createPayment('plan-1', 'wechat');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(useSaaSStore.getState().billingError).toBeTruthy();
|
||||
});
|
||||
|
||||
it('clearBillingError should clear error', () => {
|
||||
useSaaSStore.setState({ billingError: 'Test error' });
|
||||
useSaaSStore.getState().clearBillingError();
|
||||
expect(useSaaSStore.getState().billingError).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Templates
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('saasStore — templates', () => {
|
||||
it('should fetch available templates', async () => {
|
||||
const templates = [
|
||||
{ id: 't1', name: 'E-commerce', description: 'For shops' },
|
||||
];
|
||||
mockFetchAvailableTemplates.mockResolvedValue(templates);
|
||||
|
||||
await useSaaSStore.getState().fetchAvailableTemplates();
|
||||
|
||||
expect(useSaaSStore.getState().availableTemplates).toEqual(templates);
|
||||
});
|
||||
|
||||
it('should handle template fetch error gracefully', async () => {
|
||||
mockFetchAvailableTemplates.mockRejectedValue(new Error('fail'));
|
||||
|
||||
await useSaaSStore.getState().fetchAvailableTemplates();
|
||||
|
||||
expect(useSaaSStore.getState().availableTemplates).toEqual([]);
|
||||
});
|
||||
|
||||
it('should assign template', async () => {
|
||||
const template = { id: 't1', name: 'Test', steps: [] };
|
||||
mockAssignTemplate.mockResolvedValue(template);
|
||||
|
||||
await useSaaSStore.getState().assignTemplate('t1');
|
||||
|
||||
expect(useSaaSStore.getState().assignedTemplate).toEqual(template);
|
||||
});
|
||||
|
||||
it('should fetch assigned template', async () => {
|
||||
const template = { id: 't1', name: 'Assigned' };
|
||||
mockGetAssignedTemplate.mockResolvedValue(template);
|
||||
|
||||
await useSaaSStore.getState().fetchAssignedTemplate();
|
||||
|
||||
expect(useSaaSStore.getState().assignedTemplate).toEqual(template);
|
||||
});
|
||||
|
||||
it('should unassign template', async () => {
|
||||
useSaaSStore.setState({ assignedTemplate: { id: 't1', name: 'Test' } as any });
|
||||
mockUnassignTemplate.mockResolvedValue({});
|
||||
|
||||
await useSaaSStore.getState().unassignTemplate();
|
||||
|
||||
expect(useSaaSStore.getState().assignedTemplate).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// clearError
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('saasStore — utility', () => {
|
||||
it('clearError should clear error', () => {
|
||||
useSaaSStore.setState({ error: 'Test error' });
|
||||
useSaaSStore.getState().clearError();
|
||||
expect(useSaaSStore.getState().error).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user