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).
587 lines
22 KiB
TypeScript
587 lines
22 KiB
TypeScript
/**
|
|
* 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();
|
|
});
|
|
});
|