8eff0833fa
Add comprehensive tests for all 10 exported API objects: authApi, userApi, mailAccountsApi, processingRunsApi, gmailApi, smtpApi, adminApi, notificationsApi, adminNotificationsApi, and versionApi. Each method is tested for correct HTTP method, URL, parameters/data, and return value. Total api.test.ts tests: 65 (up from 9). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
862 lines
30 KiB
TypeScript
862 lines
30 KiB
TypeScript
/**
|
|
* Tests for the Axios API instance configuration — interceptors and
|
|
* default headers. We use jest.mock to stub axios.create so we can
|
|
* inspect the interceptor callbacks that api.ts registers.
|
|
*/
|
|
|
|
// Capture interceptor callbacks registered by api.ts
|
|
type InterceptorFn = (config: Record<string, unknown>) => unknown;
|
|
type ErrorFn = (error: unknown) => unknown;
|
|
|
|
let requestInterceptor: InterceptorFn | null = null;
|
|
let responseSuccessInterceptor: InterceptorFn | null = null;
|
|
let responseErrorInterceptor: ErrorFn | null = null;
|
|
|
|
const mockCreate = jest.fn();
|
|
|
|
const mockAxiosInstance = {
|
|
interceptors: {
|
|
request: {
|
|
use: jest.fn((fn: InterceptorFn) => {
|
|
requestInterceptor = fn;
|
|
}),
|
|
},
|
|
response: {
|
|
use: jest.fn((successFn: InterceptorFn, errorFn: ErrorFn) => {
|
|
responseSuccessInterceptor = successFn;
|
|
responseErrorInterceptor = errorFn;
|
|
}),
|
|
},
|
|
},
|
|
get: jest.fn(),
|
|
post: jest.fn(),
|
|
put: jest.fn(),
|
|
patch: jest.fn(),
|
|
delete: jest.fn(),
|
|
};
|
|
|
|
mockCreate.mockReturnValue(mockAxiosInstance);
|
|
|
|
jest.mock('axios', () => ({
|
|
__esModule: true,
|
|
default: {
|
|
create: mockCreate,
|
|
},
|
|
}));
|
|
|
|
// Force module initialization to capture interceptors
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
require('./api');
|
|
|
|
describe('API module setup', () => {
|
|
it('should create an axios instance with correct baseURL', () => {
|
|
expect(mockCreate).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
baseURL: '/api/v1',
|
|
headers: expect.objectContaining({
|
|
'Content-Type': 'application/json',
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should register request and response interceptors', () => {
|
|
expect(mockAxiosInstance.interceptors.request.use).toHaveBeenCalled();
|
|
expect(mockAxiosInstance.interceptors.response.use).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Request interceptor', () => {
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
});
|
|
|
|
it('should attach Authorization header when access_token exists', () => {
|
|
localStorage.setItem('access_token', 'test-jwt-token');
|
|
const config = { headers: {} as Record<string, string> };
|
|
const result = requestInterceptor!(config) as typeof config;
|
|
expect(result.headers.Authorization).toBe('Bearer test-jwt-token');
|
|
});
|
|
|
|
it('should not attach Authorization header when no token exists', () => {
|
|
const config = { headers: {} as Record<string, string> };
|
|
const result = requestInterceptor!(config) as typeof config;
|
|
expect(result.headers.Authorization).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Response interceptor', () => {
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
});
|
|
|
|
it('should pass through successful responses', () => {
|
|
const response = { data: { ok: true }, status: 200 };
|
|
const result = responseSuccessInterceptor!(response);
|
|
expect(result).toBe(response);
|
|
});
|
|
|
|
it('should clear auth state on 401', async () => {
|
|
localStorage.setItem('access_token', 'expired-token');
|
|
localStorage.setItem('user', '{"id":1}');
|
|
|
|
const error = { response: { status: 401 } };
|
|
await expect(responseErrorInterceptor!(error)).rejects.toBe(error);
|
|
|
|
expect(localStorage.getItem('access_token')).toBeNull();
|
|
expect(localStorage.getItem('user')).toBeNull();
|
|
});
|
|
|
|
it('should not clear auth state for non-401 errors', async () => {
|
|
localStorage.setItem('access_token', 'valid-token');
|
|
|
|
const error = { response: { status: 500 } };
|
|
await expect(responseErrorInterceptor!(error)).rejects.toBe(error);
|
|
|
|
expect(localStorage.getItem('access_token')).toBe('valid-token');
|
|
});
|
|
|
|
it('should reject with the error for non-401 errors', async () => {
|
|
const error = { response: { status: 403 } };
|
|
await expect(responseErrorInterceptor!(error)).rejects.toBe(error);
|
|
});
|
|
|
|
it('should handle errors without a response object', async () => {
|
|
const error = new Error('Network error');
|
|
await expect(responseErrorInterceptor!(error)).rejects.toBe(error);
|
|
});
|
|
});
|
|
|
|
// ── API Object Tests ────────────────────────────────────────────────────
|
|
|
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
const {
|
|
authApi,
|
|
userApi,
|
|
mailAccountsApi,
|
|
processingRunsApi,
|
|
gmailApi,
|
|
smtpApi,
|
|
adminApi,
|
|
notificationsApi,
|
|
adminNotificationsApi,
|
|
versionApi,
|
|
} = require('./api');
|
|
|
|
// ── authApi ─────────────────────────────────────────────────────────────
|
|
|
|
describe('authApi', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('login should post form data to /auth/login', async () => {
|
|
const tokenData = { access_token: 'tok', refresh_token: 'ref', token_type: 'bearer' };
|
|
mockAxiosInstance.post.mockResolvedValue({ data: tokenData });
|
|
|
|
const result = await authApi.login({ username: 'u@test.com', password: 'pw' });
|
|
|
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith(
|
|
'/auth/login',
|
|
expect.any(URLSearchParams),
|
|
expect.objectContaining({
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
})
|
|
);
|
|
const formData: URLSearchParams = mockAxiosInstance.post.mock.calls[0][1];
|
|
expect(formData.get('username')).toBe('u@test.com');
|
|
expect(formData.get('password')).toBe('pw');
|
|
expect(result).toEqual(tokenData);
|
|
});
|
|
|
|
it('register should post user data to /auth/register', async () => {
|
|
const user = { id: 1, email: 'u@test.com', full_name: 'Test' };
|
|
mockAxiosInstance.post.mockResolvedValue({ data: user });
|
|
|
|
const result = await authApi.register({ email: 'u@test.com', password: 'pw', full_name: 'Test' });
|
|
|
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/auth/register', {
|
|
email: 'u@test.com',
|
|
password: 'pw',
|
|
full_name: 'Test',
|
|
});
|
|
expect(result).toEqual(user);
|
|
});
|
|
|
|
it('googleAuth should post code and redirect_uri to /auth/google', async () => {
|
|
const tokenData = { access_token: 'tok', refresh_token: 'ref', token_type: 'bearer' };
|
|
mockAxiosInstance.post.mockResolvedValue({ data: tokenData });
|
|
|
|
const result = await authApi.googleAuth('code123', 'http://redirect');
|
|
|
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/auth/google', {
|
|
code: 'code123',
|
|
redirect_uri: 'http://redirect',
|
|
});
|
|
expect(result).toEqual(tokenData);
|
|
});
|
|
|
|
it('getGoogleAuthUrl should get authorization URL with redirect_uri param', async () => {
|
|
mockAxiosInstance.get.mockResolvedValue({
|
|
data: { authorization_url: 'https://google.com/auth' },
|
|
});
|
|
|
|
const result = await authApi.getGoogleAuthUrl('http://redirect');
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/auth/google/authorize-url', {
|
|
params: { redirect_uri: 'http://redirect' },
|
|
});
|
|
expect(result).toBe('https://google.com/auth');
|
|
});
|
|
});
|
|
|
|
// ── userApi ─────────────────────────────────────────────────────────────
|
|
|
|
describe('userApi', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('getCurrentUser should get /users/me', async () => {
|
|
const user = { id: 1, email: 'u@test.com' };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: user });
|
|
|
|
const result = await userApi.getCurrentUser();
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/users/me');
|
|
expect(result).toEqual(user);
|
|
});
|
|
|
|
it('updateProfile should put data to /users/me', async () => {
|
|
const user = { id: 1, email: 'new@test.com', full_name: 'New' };
|
|
mockAxiosInstance.put.mockResolvedValue({ data: user });
|
|
|
|
const result = await userApi.updateProfile({ full_name: 'New', email: 'new@test.com' });
|
|
|
|
expect(mockAxiosInstance.put).toHaveBeenCalledWith('/users/me', {
|
|
full_name: 'New',
|
|
email: 'new@test.com',
|
|
});
|
|
expect(result).toEqual(user);
|
|
});
|
|
});
|
|
|
|
// ── mailAccountsApi ─────────────────────────────────────────────────────
|
|
|
|
describe('mailAccountsApi', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('list should get /mail-accounts', async () => {
|
|
const accounts = [{ id: 1, name: 'acc1' }];
|
|
mockAxiosInstance.get.mockResolvedValue({ data: accounts });
|
|
|
|
const result = await mailAccountsApi.list();
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/mail-accounts');
|
|
expect(result).toEqual(accounts);
|
|
});
|
|
|
|
it('create should post data to /mail-accounts', async () => {
|
|
const account = { id: 1, name: 'new' };
|
|
const createData = {
|
|
name: 'new',
|
|
email_address: 'a@b.com',
|
|
protocol: 'imap',
|
|
host: 'imap.b.com',
|
|
port: 993,
|
|
use_ssl: true,
|
|
username: 'a@b.com',
|
|
password: 'secret',
|
|
forward_to: 'c@d.com',
|
|
};
|
|
mockAxiosInstance.post.mockResolvedValue({ data: account });
|
|
|
|
const result = await mailAccountsApi.create(createData);
|
|
|
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/mail-accounts', createData);
|
|
expect(result).toEqual(account);
|
|
});
|
|
|
|
it('update should put data to /mail-accounts/:id', async () => {
|
|
const account = { id: 5, name: 'updated' };
|
|
mockAxiosInstance.put.mockResolvedValue({ data: account });
|
|
|
|
const result = await mailAccountsApi.update(5, { name: 'updated' });
|
|
|
|
expect(mockAxiosInstance.put).toHaveBeenCalledWith('/mail-accounts/5', { name: 'updated' });
|
|
expect(result).toEqual(account);
|
|
});
|
|
|
|
it('toggle should patch /mail-accounts/:id/toggle', async () => {
|
|
const account = { id: 3, is_enabled: false };
|
|
mockAxiosInstance.patch.mockResolvedValue({ data: account });
|
|
|
|
const result = await mailAccountsApi.toggle(3);
|
|
|
|
expect(mockAxiosInstance.patch).toHaveBeenCalledWith('/mail-accounts/3/toggle');
|
|
expect(result).toEqual(account);
|
|
});
|
|
|
|
it('pullNow should post to /mail-accounts/:id/pull-now', async () => {
|
|
const msg = { message: 'Pull initiated' };
|
|
mockAxiosInstance.post.mockResolvedValue({ data: msg });
|
|
|
|
const result = await mailAccountsApi.pullNow(7);
|
|
|
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/mail-accounts/7/pull-now');
|
|
expect(result).toEqual(msg);
|
|
});
|
|
|
|
it('delete should delete /mail-accounts/:id', async () => {
|
|
mockAxiosInstance.delete.mockResolvedValue({});
|
|
|
|
await mailAccountsApi.delete(4);
|
|
|
|
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/mail-accounts/4');
|
|
});
|
|
|
|
it('test should post config to /mail-accounts/test', async () => {
|
|
const response = { success: true, message: 'OK' };
|
|
const config = {
|
|
host: 'imap.test.com',
|
|
port: 993,
|
|
protocol: 'imap',
|
|
username: 'user',
|
|
password: 'pass',
|
|
use_ssl: true,
|
|
};
|
|
mockAxiosInstance.post.mockResolvedValue({ data: response });
|
|
|
|
const result = await mailAccountsApi.test(config);
|
|
|
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/mail-accounts/test', config);
|
|
expect(result).toEqual(response);
|
|
});
|
|
|
|
it('testExisting should post to /mail-accounts/:id/test', async () => {
|
|
const response = { success: true, message: 'Connected' };
|
|
mockAxiosInstance.post.mockResolvedValue({ data: response });
|
|
|
|
const result = await mailAccountsApi.testExisting(10);
|
|
|
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/mail-accounts/10/test');
|
|
expect(result).toEqual(response);
|
|
});
|
|
|
|
it('autoDetect should post email address to /mail-accounts/auto-detect', async () => {
|
|
const response = { success: true, suggestions: [{ protocol: 'imap', host: 'imap.x.com' }] };
|
|
mockAxiosInstance.post.mockResolvedValue({ data: response });
|
|
|
|
const result = await mailAccountsApi.autoDetect('user@x.com');
|
|
|
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/mail-accounts/auto-detect', {
|
|
email_address: 'user@x.com',
|
|
});
|
|
expect(result).toEqual(response);
|
|
});
|
|
});
|
|
|
|
// ── processingRunsApi ───────────────────────────────────────────────────
|
|
|
|
describe('processingRunsApi', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('list should get /processing-runs with params', async () => {
|
|
const paginated = { items: [], total: 0, page: 1, page_size: 20, pages: 0 };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: paginated });
|
|
|
|
const params = { page: 1, page_size: 20, status: 'completed' };
|
|
const result = await processingRunsApi.list(params);
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/processing-runs', { params });
|
|
expect(result).toEqual(paginated);
|
|
});
|
|
|
|
it('list should work without params', async () => {
|
|
const paginated = { items: [], total: 0, page: 1, page_size: 20, pages: 0 };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: paginated });
|
|
|
|
const result = await processingRunsApi.list();
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/processing-runs', { params: undefined });
|
|
expect(result).toEqual(paginated);
|
|
});
|
|
|
|
it('get should get /processing-runs/:id', async () => {
|
|
const run = { id: 42, status: 'completed' };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: run });
|
|
|
|
const result = await processingRunsApi.get(42);
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/processing-runs/42');
|
|
expect(result).toEqual(run);
|
|
});
|
|
|
|
it('getLogs should get /processing-runs/:id/logs with params', async () => {
|
|
const paginated = { items: [], total: 0, page: 1, page_size: 50, pages: 0 };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: paginated });
|
|
|
|
const params = { page: 2, page_size: 50 };
|
|
const result = await processingRunsApi.getLogs(42, params);
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/processing-runs/42/logs', { params });
|
|
expect(result).toEqual(paginated);
|
|
});
|
|
|
|
it('getLogs should work without params', async () => {
|
|
const paginated = { items: [], total: 0, page: 1, page_size: 20, pages: 0 };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: paginated });
|
|
|
|
const result = await processingRunsApi.getLogs(10);
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/processing-runs/10/logs', {
|
|
params: undefined,
|
|
});
|
|
expect(result).toEqual(paginated);
|
|
});
|
|
|
|
it('listForAccount should get /mail-accounts/:id/processing-runs with params', async () => {
|
|
const paginated = { items: [], total: 0, page: 1, page_size: 20, pages: 0 };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: paginated });
|
|
|
|
const params = { page: 1, page_size: 20, status: 'failed' };
|
|
const result = await processingRunsApi.listForAccount(5, params);
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/mail-accounts/5/processing-runs', {
|
|
params,
|
|
});
|
|
expect(result).toEqual(paginated);
|
|
});
|
|
|
|
it('listLogsForAccount should get /mail-accounts/:id/logs with params', async () => {
|
|
const paginated = { items: [], total: 0, page: 1, page_size: 20, pages: 0 };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: paginated });
|
|
|
|
const params = { page: 1, page_size: 20, level: 'error' };
|
|
const result = await processingRunsApi.listLogsForAccount(3, params);
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/mail-accounts/3/logs', { params });
|
|
expect(result).toEqual(paginated);
|
|
});
|
|
});
|
|
|
|
// ── gmailApi ────────────────────────────────────────────────────────────
|
|
|
|
describe('gmailApi', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('getAuthorizeUrl should get /providers/gmail/authorize-url with redirect_uri', async () => {
|
|
mockAxiosInstance.get.mockResolvedValue({
|
|
data: { authorization_url: 'https://accounts.google.com/o/oauth2' },
|
|
});
|
|
|
|
const result = await gmailApi.getAuthorizeUrl('http://localhost/callback');
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/providers/gmail/authorize-url', {
|
|
params: { redirect_uri: 'http://localhost/callback' },
|
|
});
|
|
expect(result).toBe('https://accounts.google.com/o/oauth2');
|
|
});
|
|
|
|
it('saveCallback should post code and redirect_uri to /providers/gmail/callback', async () => {
|
|
const credential = { id: 1, gmail_email: 'test@gmail.com', is_valid: true };
|
|
mockAxiosInstance.post.mockResolvedValue({ data: credential });
|
|
|
|
const result = await gmailApi.saveCallback('authcode', 'http://localhost/callback');
|
|
|
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/providers/gmail/callback', {
|
|
code: 'authcode',
|
|
redirect_uri: 'http://localhost/callback',
|
|
});
|
|
expect(result).toEqual(credential);
|
|
});
|
|
|
|
it('getCredential should get /providers/gmail-credential', async () => {
|
|
const credential = { id: 1, gmail_email: 'test@gmail.com', is_valid: true };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: credential });
|
|
|
|
const result = await gmailApi.getCredential();
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/providers/gmail-credential');
|
|
expect(result).toEqual(credential);
|
|
});
|
|
|
|
it('disconnect should delete /providers/gmail-credential', async () => {
|
|
mockAxiosInstance.delete.mockResolvedValue({});
|
|
|
|
await gmailApi.disconnect();
|
|
|
|
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/providers/gmail-credential');
|
|
});
|
|
|
|
it('sendDebugEmail should post to /providers/gmail/debug-email', async () => {
|
|
const response = {
|
|
message: 'Sent',
|
|
message_id: 'msg1',
|
|
thread_id: 'thr1',
|
|
label_ids: ['INBOX'],
|
|
};
|
|
mockAxiosInstance.post.mockResolvedValue({ data: response });
|
|
|
|
const result = await gmailApi.sendDebugEmail();
|
|
|
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/providers/gmail/debug-email');
|
|
expect(result).toEqual(response);
|
|
});
|
|
|
|
it('updateImportLabels should put label templates to /providers/gmail-credential/labels', async () => {
|
|
const credential = { id: 1, import_label_templates: ['Label1'] };
|
|
mockAxiosInstance.put.mockResolvedValue({ data: credential });
|
|
|
|
const result = await gmailApi.updateImportLabels(['Label1']);
|
|
|
|
expect(mockAxiosInstance.put).toHaveBeenCalledWith('/providers/gmail-credential/labels', {
|
|
import_label_templates: ['Label1'],
|
|
});
|
|
expect(result).toEqual(credential);
|
|
});
|
|
});
|
|
|
|
// ── smtpApi ─────────────────────────────────────────────────────────────
|
|
|
|
describe('smtpApi', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('get should get /users/smtp-config', async () => {
|
|
const config = { id: 1, host: 'smtp.test.com', port: 587 };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: config });
|
|
|
|
const result = await smtpApi.get();
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/users/smtp-config');
|
|
expect(result).toEqual(config);
|
|
});
|
|
|
|
it('save should put config to /users/smtp-config', async () => {
|
|
const config = { id: 1, host: 'smtp.test.com', port: 587 };
|
|
const saveData = { host: 'smtp.test.com', port: 587, username: 'user', use_tls: true };
|
|
mockAxiosInstance.put.mockResolvedValue({ data: config });
|
|
|
|
const result = await smtpApi.save(saveData);
|
|
|
|
expect(mockAxiosInstance.put).toHaveBeenCalledWith('/users/smtp-config', saveData);
|
|
expect(result).toEqual(config);
|
|
});
|
|
|
|
it('remove should delete /users/smtp-config', async () => {
|
|
mockAxiosInstance.delete.mockResolvedValue({});
|
|
|
|
await smtpApi.remove();
|
|
|
|
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/users/smtp-config');
|
|
});
|
|
});
|
|
|
|
// ── adminApi ────────────────────────────────────────────────────────────
|
|
|
|
describe('adminApi', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('getStats should get /admin/stats', async () => {
|
|
const stats = { total_users: 10, total_mail_accounts: 20, total_processing_runs: 100 };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: stats });
|
|
|
|
const result = await adminApi.getStats();
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/admin/stats');
|
|
expect(result).toEqual(stats);
|
|
});
|
|
|
|
it('listUsers should get /admin/users with skip and limit', async () => {
|
|
const users = [{ id: 1, email: 'a@b.com' }];
|
|
mockAxiosInstance.get.mockResolvedValue({ data: users });
|
|
|
|
const result = await adminApi.listUsers(10, 50);
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/admin/users', {
|
|
params: { skip: 10, limit: 50 },
|
|
});
|
|
expect(result).toEqual(users);
|
|
});
|
|
|
|
it('listUsers should use defaults for skip and limit', async () => {
|
|
mockAxiosInstance.get.mockResolvedValue({ data: [] });
|
|
|
|
await adminApi.listUsers();
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/admin/users', {
|
|
params: { skip: 0, limit: 100 },
|
|
});
|
|
});
|
|
|
|
it('getUser should get /admin/users/:id', async () => {
|
|
const user = { id: 5, email: 'u@t.com' };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: user });
|
|
|
|
const result = await adminApi.getUser(5);
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/admin/users/5');
|
|
expect(result).toEqual(user);
|
|
});
|
|
|
|
it('updateUser should put data to /admin/users/:id', async () => {
|
|
const user = { id: 5, is_active: false };
|
|
mockAxiosInstance.put.mockResolvedValue({ data: user });
|
|
|
|
const result = await adminApi.updateUser(5, { is_active: false });
|
|
|
|
expect(mockAxiosInstance.put).toHaveBeenCalledWith('/admin/users/5', { is_active: false });
|
|
expect(result).toEqual(user);
|
|
});
|
|
|
|
it('deleteUser should delete /admin/users/:id', async () => {
|
|
mockAxiosInstance.delete.mockResolvedValue({});
|
|
|
|
await adminApi.deleteUser(5);
|
|
|
|
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/admin/users/5');
|
|
});
|
|
|
|
it('listPlans should get /admin/plans', async () => {
|
|
const plans = [{ id: 1, tier: 'free' }];
|
|
mockAxiosInstance.get.mockResolvedValue({ data: plans });
|
|
|
|
const result = await adminApi.listPlans();
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/admin/plans');
|
|
expect(result).toEqual(plans);
|
|
});
|
|
|
|
it('createPlan should post data to /admin/plans', async () => {
|
|
const plan = { id: 1, tier: 'pro', name: 'Pro Plan' };
|
|
const createData = {
|
|
tier: 'pro',
|
|
name: 'Pro Plan',
|
|
price_monthly: 9.99,
|
|
max_mail_accounts: 10,
|
|
max_emails_per_day: 1000,
|
|
check_interval_minutes: 5,
|
|
support_level: 'email',
|
|
is_active: true,
|
|
};
|
|
mockAxiosInstance.post.mockResolvedValue({ data: plan });
|
|
|
|
const result = await adminApi.createPlan(createData);
|
|
|
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/admin/plans', createData);
|
|
expect(result).toEqual(plan);
|
|
});
|
|
|
|
it('updatePlan should put data to /admin/plans/:id', async () => {
|
|
const plan = { id: 2, name: 'Updated Plan' };
|
|
mockAxiosInstance.put.mockResolvedValue({ data: plan });
|
|
|
|
const result = await adminApi.updatePlan(2, { name: 'Updated Plan' });
|
|
|
|
expect(mockAxiosInstance.put).toHaveBeenCalledWith('/admin/plans/2', { name: 'Updated Plan' });
|
|
expect(result).toEqual(plan);
|
|
});
|
|
|
|
it('deletePlan should delete /admin/plans/:id', async () => {
|
|
mockAxiosInstance.delete.mockResolvedValue({});
|
|
|
|
await adminApi.deletePlan(3);
|
|
|
|
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/admin/plans/3');
|
|
});
|
|
|
|
it('listProcessingRuns should get /admin/processing-runs with params', async () => {
|
|
const paginated = { items: [], total: 0, page: 1, page_size: 20, pages: 0 };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: paginated });
|
|
|
|
const params = { page: 1, page_size: 20, user_id: 5 };
|
|
const result = await adminApi.listProcessingRuns(params);
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/admin/processing-runs', { params });
|
|
expect(result).toEqual(paginated);
|
|
});
|
|
|
|
it('listProcessingRuns should work without params', async () => {
|
|
const paginated = { items: [], total: 0, page: 1, page_size: 20, pages: 0 };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: paginated });
|
|
|
|
const result = await adminApi.listProcessingRuns();
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/admin/processing-runs', {
|
|
params: undefined,
|
|
});
|
|
expect(result).toEqual(paginated);
|
|
});
|
|
|
|
it('listProcessingLogs should get /admin/processing-logs with params', async () => {
|
|
const paginated = { items: [], total: 0, page: 1, page_size: 20, pages: 0 };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: paginated });
|
|
|
|
const params = { page: 1, page_size: 20, level: 'error' };
|
|
const result = await adminApi.listProcessingLogs(params);
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/admin/processing-logs', { params });
|
|
expect(result).toEqual(paginated);
|
|
});
|
|
|
|
it('listProcessingLogs should work without params', async () => {
|
|
const paginated = { items: [], total: 0, page: 1, page_size: 20, pages: 0 };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: paginated });
|
|
|
|
const result = await adminApi.listProcessingLogs();
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/admin/processing-logs', {
|
|
params: undefined,
|
|
});
|
|
expect(result).toEqual(paginated);
|
|
});
|
|
});
|
|
|
|
// ── notificationsApi ────────────────────────────────────────────────────
|
|
|
|
describe('notificationsApi', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('list should get /notifications', async () => {
|
|
const configs = [{ id: 1, name: 'Notif 1' }];
|
|
mockAxiosInstance.get.mockResolvedValue({ data: configs });
|
|
|
|
const result = await notificationsApi.list();
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/notifications');
|
|
expect(result).toEqual(configs);
|
|
});
|
|
|
|
it('create should post data to /notifications', async () => {
|
|
const config = { id: 1, name: 'New Notif', channel: 'email' };
|
|
const createData = { name: 'New Notif', channel: 'email' };
|
|
mockAxiosInstance.post.mockResolvedValue({ data: config });
|
|
|
|
const result = await notificationsApi.create(createData);
|
|
|
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/notifications', createData);
|
|
expect(result).toEqual(config);
|
|
});
|
|
|
|
it('update should put data to /notifications/:id', async () => {
|
|
const config = { id: 2, name: 'Updated' };
|
|
mockAxiosInstance.put.mockResolvedValue({ data: config });
|
|
|
|
const result = await notificationsApi.update(2, { name: 'Updated' });
|
|
|
|
expect(mockAxiosInstance.put).toHaveBeenCalledWith('/notifications/2', { name: 'Updated' });
|
|
expect(result).toEqual(config);
|
|
});
|
|
|
|
it('delete should delete /notifications/:id', async () => {
|
|
mockAxiosInstance.delete.mockResolvedValue({});
|
|
|
|
await notificationsApi.delete(3);
|
|
|
|
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/notifications/3');
|
|
});
|
|
|
|
it('test should post apprise_url to /notifications/test', async () => {
|
|
const response = { success: true, message: 'Test sent' };
|
|
mockAxiosInstance.post.mockResolvedValue({ data: response });
|
|
|
|
const result = await notificationsApi.test('apprise://slack/webhook');
|
|
|
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/notifications/test', {
|
|
apprise_url: 'apprise://slack/webhook',
|
|
});
|
|
expect(result).toEqual(response);
|
|
});
|
|
});
|
|
|
|
// ── adminNotificationsApi ───────────────────────────────────────────────
|
|
|
|
describe('adminNotificationsApi', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('list should get /admin/notifications', async () => {
|
|
const configs = [{ id: 1, name: 'Admin Notif' }];
|
|
mockAxiosInstance.get.mockResolvedValue({ data: configs });
|
|
|
|
const result = await adminNotificationsApi.list();
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/admin/notifications');
|
|
expect(result).toEqual(configs);
|
|
});
|
|
|
|
it('create should post data to /admin/notifications', async () => {
|
|
const config = { id: 1, name: 'Admin Notif', apprise_url: 'apprise://test' };
|
|
const createData = { name: 'Admin Notif', apprise_url: 'apprise://test' };
|
|
mockAxiosInstance.post.mockResolvedValue({ data: config });
|
|
|
|
const result = await adminNotificationsApi.create(createData);
|
|
|
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/admin/notifications', createData);
|
|
expect(result).toEqual(config);
|
|
});
|
|
|
|
it('update should put data to /admin/notifications/:id', async () => {
|
|
const config = { id: 2, name: 'Updated Admin' };
|
|
mockAxiosInstance.put.mockResolvedValue({ data: config });
|
|
|
|
const result = await adminNotificationsApi.update(2, { name: 'Updated Admin' });
|
|
|
|
expect(mockAxiosInstance.put).toHaveBeenCalledWith('/admin/notifications/2', {
|
|
name: 'Updated Admin',
|
|
});
|
|
expect(result).toEqual(config);
|
|
});
|
|
|
|
it('delete should delete /admin/notifications/:id', async () => {
|
|
mockAxiosInstance.delete.mockResolvedValue({});
|
|
|
|
await adminNotificationsApi.delete(4);
|
|
|
|
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/admin/notifications/4');
|
|
});
|
|
|
|
it('test should post apprise_url to /admin/notifications/test', async () => {
|
|
const response = { success: true, message: 'Admin test sent' };
|
|
mockAxiosInstance.post.mockResolvedValue({ data: response });
|
|
|
|
const result = await adminNotificationsApi.test('apprise://discord/webhook');
|
|
|
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/admin/notifications/test', {
|
|
apprise_url: 'apprise://discord/webhook',
|
|
});
|
|
expect(result).toEqual(response);
|
|
});
|
|
});
|
|
|
|
// ── versionApi ──────────────────────────────────────────────────────────
|
|
|
|
describe('versionApi', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('get should get /version', async () => {
|
|
const versionInfo = { version: '1.2.3', build_date: '2024-01-01' };
|
|
mockAxiosInstance.get.mockResolvedValue({ data: versionInfo });
|
|
|
|
const result = await versionApi.get();
|
|
|
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/version');
|
|
expect(result).toEqual(versionInfo);
|
|
});
|
|
});
|