import { PocketBaseService, PocketBaseError } from '../src/pocketbase-service.js';
import { appConfig } from '../src/utils/config.js';
// Mock PocketBase to avoid real API calls during testing
jest.mock('pocketbase', () => {
return jest.fn().mockImplementation(() => ({
autoCancellation: jest.fn(),
authStore: {
isValid: false,
token: null,
record: null,
onChange: jest.fn(),
clear: jest.fn(),
save: jest.fn(),
},
collection: jest.fn().mockReturnValue({
authWithPassword: jest.fn(),
authRefresh: jest.fn(),
create: jest.fn(),
getList: jest.fn(),
getOne: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
requestPasswordReset: jest.fn(),
confirmPasswordReset: jest.fn(),
impersonate: jest.fn(),
}),
collections: {
getList: jest.fn(),
getOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
files: {
getURL: jest.fn(),
getToken: jest.fn(),
},
health: {
check: jest.fn(),
},
logs: {
getList: jest.fn(),
},
backups: {
create: jest.fn(),
getFullList: jest.fn(),
},
settings: {
getAll: jest.fn(),
update: jest.fn(),
},
}));
});
describe('PocketBaseService', () => {
let service: PocketBaseService;
beforeEach(() => {
service = new PocketBaseService();
jest.clearAllMocks();
});
describe('constructor', () => {
it('should create user and admin clients', () => {
expect(service).toBeDefined();
expect(service.getUserClient()).toBeDefined();
expect(service.getAdminClient()).toBeDefined();
});
it('should use correct URL from config', () => {
expect(appConfig.pocketbase.url).toBe('http://10.69.100.111:8090');
});
});
describe('user authentication', () => {
it('should handle successful login', async () => {
const mockAuthData = {
token: 'test-token',
record: {
id: 'user123',
email: 'test@example.com',
verified: true,
created: '2023-01-01T00:00:00.000Z',
updated: '2023-01-01T00:00:00.000Z',
},
};
const mockCollection = service.getUserClient().collection('users');
(mockCollection.authWithPassword as jest.Mock).mockResolvedValue(mockAuthData);
const result = await service.loginUser('test@example.com', 'password');
expect(result).toEqual(mockAuthData);
expect(mockCollection.authWithPassword).toHaveBeenCalledWith(
'test@example.com',
'password',
undefined,
);
});
it('should handle login failure', async () => {
const mockError = {
response: {
message: 'Invalid credentials',
code: 400,
},
};
const mockCollection = service.getUserClient().collection('users');
(mockCollection.authWithPassword as jest.Mock).mockRejectedValue(mockError);
await expect(service.loginUser('test@example.com', 'wrong-password'))
.rejects
.toThrow(PocketBaseError);
});
it('should handle user registration', async () => {
const mockCreateData = {
id: 'user123',
email: 'test@example.com',
created: '2023-01-01T00:00:00.000Z',
updated: '2023-01-01T00:00:00.000Z',
};
const mockAuthData = {
token: 'test-token',
record: mockCreateData,
};
const mockCollection = service.getUserClient().collection('users');
(mockCollection.create as jest.Mock).mockResolvedValue(mockCreateData);
(mockCollection.authWithPassword as jest.Mock).mockResolvedValue(mockAuthData);
const result = await service.registerUser(
'test@example.com',
'password',
'password',
{ name: 'Test User' },
);
expect(result).toEqual(mockAuthData);
expect(mockCollection.create).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password',
passwordConfirm: 'password',
name: 'Test User',
});
});
it('should refresh authentication', async () => {
const mockAuthData = {
token: 'new-token',
record: {
id: 'user123',
email: 'test@example.com',
},
};
// Mock auth store as valid
service.getUserClient().authStore.isValid = true;
const mockCollection = service.getUserClient().collection('users');
(mockCollection.authRefresh as jest.Mock).mockResolvedValue(mockAuthData);
const result = await service.refreshUserAuth();
expect(result).toEqual(mockAuthData);
expect(mockCollection.authRefresh).toHaveBeenCalled();
});
it('should handle refresh failure when not authenticated', async () => {
// Mock auth store as invalid
service.getUserClient().authStore.isValid = false;
await expect(service.refreshUserAuth())
.rejects
.toThrow('No valid user session to refresh');
});
it('should clear user authentication', () => {
const mockClear = jest.fn();
service.getUserClient().authStore.clear = mockClear;
service.logoutUser();
expect(mockClear).toHaveBeenCalled();
});
});
describe('admin authentication', () => {
it('should authenticate as admin', async () => {
const mockAuthData = {
token: 'admin-token',
record: {
id: 'admin123',
email: 'admin@example.com',
created: '2023-01-01T00:00:00.000Z',
updated: '2023-01-01T00:00:00.000Z',
},
};
const mockCollection = service.getAdminClient().collection('_superusers');
(mockCollection.authWithPassword as jest.Mock).mockResolvedValue(mockAuthData);
const result = await service.authenticateAdmin();
expect(result).toEqual(mockAuthData);
expect(mockCollection.authWithPassword).toHaveBeenCalledWith(
appConfig.pocketbase.superuser.email,
appConfig.pocketbase.superuser.password,
{ autoRefreshThreshold: 30 * 60 },
);
});
});
describe('password management', () => {
it('should request password reset', async () => {
const mockCollection = service.getUserClient().collection('users');
(mockCollection.requestPasswordReset as jest.Mock).mockResolvedValue(undefined);
const result = await service.requestPasswordReset('test@example.com');
expect(result).toBe(true);
expect(mockCollection.requestPasswordReset).toHaveBeenCalledWith('test@example.com');
});
it('should confirm password reset', async () => {
const mockCollection = service.getUserClient().collection('users');
(mockCollection.confirmPasswordReset as jest.Mock).mockResolvedValue(undefined);
const result = await service.confirmPasswordReset(
'reset-token',
'newpassword',
'newpassword',
);
expect(result).toBe(true);
expect(mockCollection.confirmPasswordReset).toHaveBeenCalledWith(
'reset-token',
'newpassword',
'newpassword',
);
});
});
describe('collection management', () => {
it('should list collections', async () => {
const mockCollections = {
page: 1,
perPage: 30,
totalItems: 2,
totalPages: 1,
items: [
{
id: 'col1',
name: 'users',
type: 'auth',
schema: [],
created: '2023-01-01T00:00:00.000Z',
updated: '2023-01-01T00:00:00.000Z',
},
{
id: 'col2',
name: 'posts',
type: 'base',
schema: [],
created: '2023-01-01T00:00:00.000Z',
updated: '2023-01-01T00:00:00.000Z',
},
],
};
(service.getAdminClient().collections.getList as jest.Mock)
.mockResolvedValue(mockCollections);
// Mock admin authentication
jest.spyOn(service, 'ensureAdminAuth').mockResolvedValue();
const result = await service.listCollections({ page: 1, perPage: 30 });
expect(result).toEqual(mockCollections);
expect(service.getAdminClient().collections.getList).toHaveBeenCalledWith(
1,
30,
{ sort: undefined, filter: undefined },
);
});
it('should get a specific collection', async () => {
const mockCollection = {
id: 'col1',
name: 'users',
type: 'auth',
schema: [],
created: '2023-01-01T00:00:00.000Z',
updated: '2023-01-01T00:00:00.000Z',
};
(service.getAdminClient().collections.getOne as jest.Mock)
.mockResolvedValue(mockCollection);
// Mock admin authentication
jest.spyOn(service, 'ensureAdminAuth').mockResolvedValue();
const result = await service.getCollection('users');
expect(result).toEqual(mockCollection);
expect(service.getAdminClient().collections.getOne).toHaveBeenCalledWith('users');
});
it('should create a collection', async () => {
const collectionData = {
name: 'test_collection',
type: 'base',
schema: [],
};
const mockCreatedCollection = {
id: 'new_col',
...collectionData,
created: '2023-01-01T00:00:00.000Z',
updated: '2023-01-01T00:00:00.000Z',
};
(service.getAdminClient().collections.create as jest.Mock)
.mockResolvedValue(mockCreatedCollection);
// Mock admin authentication
jest.spyOn(service, 'ensureAdminAuth').mockResolvedValue();
const result = await service.createCollection(collectionData);
expect(result).toEqual(mockCreatedCollection);
expect(service.getAdminClient().collections.create).toHaveBeenCalledWith(collectionData);
});
});
describe('record management', () => {
it('should list records', async () => {
const mockRecords = {
page: 1,
perPage: 30,
totalItems: 1,
totalPages: 1,
items: [
{
id: 'rec1',
title: 'Test Record',
created: '2023-01-01T00:00:00.000Z',
updated: '2023-01-01T00:00:00.000Z',
collectionId: 'col1',
collectionName: 'posts',
},
],
};
// Mock user as authenticated
jest.spyOn(service, 'isUserAuthenticated').mockReturnValue(true);
const mockCollection = service.getUserClient().collection('posts');
(mockCollection.getList as jest.Mock).mockResolvedValue(mockRecords);
const result = await service.listRecords('posts', { page: 1, perPage: 30 });
expect(result).toEqual(mockRecords);
expect(mockCollection.getList).toHaveBeenCalledWith(
1,
30,
{
sort: undefined,
filter: undefined,
expand: undefined,
fields: undefined,
},
);
});
it('should get a specific record', async () => {
const mockRecord = {
id: 'rec1',
title: 'Test Record',
created: '2023-01-01T00:00:00.000Z',
updated: '2023-01-01T00:00:00.000Z',
collectionId: 'col1',
collectionName: 'posts',
};
// Mock user as authenticated
jest.spyOn(service, 'isUserAuthenticated').mockReturnValue(true);
const mockCollection = service.getUserClient().collection('posts');
(mockCollection.getOne as jest.Mock).mockResolvedValue(mockRecord);
const result = await service.getRecord('posts', 'rec1');
expect(result).toEqual(mockRecord);
expect(mockCollection.getOne).toHaveBeenCalledWith('rec1', undefined);
});
it('should create a record', async () => {
const recordData = { title: 'New Record', content: 'Test content' };
const mockCreatedRecord = {
id: 'new_rec',
...recordData,
created: '2023-01-01T00:00:00.000Z',
updated: '2023-01-01T00:00:00.000Z',
collectionId: 'col1',
collectionName: 'posts',
};
// Mock user as authenticated
jest.spyOn(service, 'isUserAuthenticated').mockReturnValue(true);
const mockCollection = service.getUserClient().collection('posts');
(mockCollection.create as jest.Mock).mockResolvedValue(mockCreatedRecord);
const result = await service.createRecord('posts', recordData);
expect(result).toEqual(mockCreatedRecord);
expect(mockCollection.create).toHaveBeenCalledWith(recordData, undefined);
});
});
describe('file management', () => {
it('should generate file URL', () => {
const mockRecord = {
id: 'rec1',
collectionId: 'col1',
collectionName: 'posts',
created: '2023-01-01T00:00:00.000Z',
updated: '2023-01-01T00:00:00.000Z',
};
const mockGetURL = jest.fn().mockReturnValue('http://example.com/file.jpg');
service.getUserClient().files.getURL = mockGetURL;
const result = service.getFileUrl(mockRecord, 'file.jpg', { thumb: '100x100' });
expect(result).toBe('http://example.com/file.jpg');
expect(mockGetURL).toHaveBeenCalledWith(mockRecord, 'file.jpg', { thumb: '100x100' });
});
it('should get file token', async () => {
const mockToken = { token: 'file-token-123' };
// Mock user as authenticated
jest.spyOn(service, 'isUserAuthenticated').mockReturnValue(true);
(service.getUserClient().files.getToken as jest.Mock).mockResolvedValue(mockToken);
const result = await service.getFileToken();
expect(result).toBe('file-token-123');
expect(service.getUserClient().files.getToken).toHaveBeenCalled();
});
});
describe('system operations', () => {
it('should perform health check', async () => {
const mockHealth = {
code: 200,
message: 'API is healthy',
data: {},
};
(service.getAdminClient().health.check as jest.Mock).mockResolvedValue(mockHealth);
const result = await service.healthCheck();
expect(result).toEqual(mockHealth);
expect(service.getAdminClient().health.check).toHaveBeenCalled();
});
it('should get logs', async () => {
const mockLogs = {
page: 1,
perPage: 30,
totalItems: 1,
totalPages: 1,
items: [
{
id: 'log1',
level: 'info',
message: 'Test log',
data: {},
created: '2023-01-01T00:00:00.000Z',
},
],
};
(service.getAdminClient().logs.getList as jest.Mock).mockResolvedValue(mockLogs);
// Mock admin authentication
jest.spyOn(service, 'ensureAdminAuth').mockResolvedValue();
const result = await service.getLogs({ page: 1, perPage: 30 });
expect(result).toEqual(mockLogs);
expect(service.getAdminClient().logs.getList).toHaveBeenCalledWith(
1,
30,
{ sort: undefined, filter: undefined },
);
});
});
describe('error handling', () => {
it('should handle PocketBase errors correctly', async () => {
const mockError = {
response: {
message: 'Validation failed',
code: 400,
data: { email: { code: 'validation_required' } },
},
};
const mockCollection = service.getUserClient().collection('users');
(mockCollection.authWithPassword as jest.Mock).mockRejectedValue(mockError);
try {
await service.loginUser('', 'password');
} catch (error) {
expect(error).toBeInstanceOf(PocketBaseError);
expect((error as PocketBaseError).message).toBe('Validation failed');
expect((error as PocketBaseError).status).toBe(400);
expect((error as PocketBaseError).data).toEqual({ email: { code: 'validation_required' } });
}
});
it('should handle generic errors', async () => {
const mockError = new Error('Network error');
const mockCollection = service.getUserClient().collection('users');
(mockCollection.authWithPassword as jest.Mock).mockRejectedValue(mockError);
try {
await service.loginUser('test@example.com', 'password');
} catch (error) {
expect(error).toBeInstanceOf(PocketBaseError);
expect((error as PocketBaseError).message).toBe('Network error');
}
});
});
});