162 lines
5.7 KiB
TypeScript
162 lines
5.7 KiB
TypeScript
import '@angular/compiler';
|
|
import { Injector, runInInjectionContext } from '@angular/core';
|
|
import {
|
|
beforeEach,
|
|
describe,
|
|
expect,
|
|
it,
|
|
vi
|
|
} from 'vitest';
|
|
|
|
import { PlatformService } from '../../core/platform';
|
|
import { BrowserDatabaseService } from './browser-database.service';
|
|
import { CapacitorDatabaseService } from './capacitor-database.service';
|
|
import { DatabaseService } from './database.service';
|
|
import { ElectronDatabaseService } from './electron-database.service';
|
|
|
|
function installLocalStorageMock(): void {
|
|
const store = new Map<string, string>();
|
|
|
|
vi.stubGlobal('localStorage', {
|
|
getItem: (key: string) => store.get(key) ?? null,
|
|
setItem: (key: string, value: string) => store.set(key, String(value)),
|
|
removeItem: (key: string) => store.delete(key),
|
|
clear: () => store.clear(),
|
|
key: (index: number) => Array.from(store.keys())[index] ?? null,
|
|
get length() {
|
|
return store.size;
|
|
}
|
|
});
|
|
}
|
|
|
|
describe('DatabaseService', () => {
|
|
let browserDatabase: {
|
|
getBansForRoom: ReturnType<typeof vi.fn>;
|
|
initialize: ReturnType<typeof vi.fn>;
|
|
};
|
|
let capacitorDatabase: {
|
|
getBansForRoom: ReturnType<typeof vi.fn>;
|
|
initialize: ReturnType<typeof vi.fn>;
|
|
};
|
|
let electronDatabase: {
|
|
getBansForRoom: ReturnType<typeof vi.fn>;
|
|
initialize: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
beforeEach(() => {
|
|
installLocalStorageMock();
|
|
localStorage.clear();
|
|
|
|
browserDatabase = {
|
|
getBansForRoom: vi.fn(() => Promise.resolve([])),
|
|
initialize: vi.fn(() => Promise.resolve())
|
|
};
|
|
|
|
capacitorDatabase = {
|
|
getBansForRoom: vi.fn(() => Promise.resolve([])),
|
|
initialize: vi.fn(() => Promise.resolve())
|
|
};
|
|
|
|
electronDatabase = {
|
|
getBansForRoom: vi.fn(() => Promise.resolve([])),
|
|
initialize: vi.fn(() => Promise.resolve())
|
|
};
|
|
});
|
|
|
|
function createService(platform: Pick<PlatformService, 'isBrowser' | 'isElectron' | 'isCapacitor'>): DatabaseService {
|
|
const injector = Injector.create({
|
|
providers: [
|
|
DatabaseService,
|
|
{ provide: PlatformService, useValue: platform },
|
|
{ provide: BrowserDatabaseService, useValue: browserDatabase },
|
|
{ provide: CapacitorDatabaseService, useValue: capacitorDatabase },
|
|
{ provide: ElectronDatabaseService, useValue: electronDatabase }
|
|
]
|
|
});
|
|
|
|
return runInInjectionContext(injector, () => injector.get(DatabaseService));
|
|
}
|
|
|
|
it('initializes the browser backend before the first delegated read', async () => {
|
|
const service = createService({ isBrowser: true, isElectron: false, isCapacitor: false });
|
|
|
|
await service.getBansForRoom('room-1');
|
|
|
|
expect(browserDatabase.initialize).toHaveBeenCalledTimes(1);
|
|
expect(browserDatabase.getBansForRoom).toHaveBeenCalledWith('room-1');
|
|
expect(capacitorDatabase.initialize).not.toHaveBeenCalled();
|
|
expect(service.isReady()).toBe(true);
|
|
});
|
|
|
|
it('rechecks backend initialization when the user scope changes during an in-flight initialize call', async () => {
|
|
let finishInitialInitialize!: () => void;
|
|
|
|
browserDatabase.initialize = vi.fn()
|
|
.mockImplementationOnce(() => new Promise<void>((resolve) => {
|
|
finishInitialInitialize = resolve;
|
|
}))
|
|
.mockResolvedValue(undefined);
|
|
|
|
localStorage.setItem('metoyou_currentUserId', 'user-a');
|
|
|
|
const service = createService({ isBrowser: true, isElectron: false, isCapacitor: false });
|
|
const initialInitialize = service.initialize();
|
|
|
|
localStorage.setItem('metoyou_currentUserId', 'user-b');
|
|
const initializeAfterScopeChange = service.initialize();
|
|
|
|
expect(browserDatabase.initialize).toHaveBeenCalledTimes(1);
|
|
|
|
finishInitialInitialize();
|
|
await Promise.all([initialInitialize, initializeAfterScopeChange]);
|
|
|
|
expect(browserDatabase.initialize).toHaveBeenCalledTimes(2);
|
|
expect(service.isReady()).toBe(true);
|
|
});
|
|
|
|
it('does not reinitialize the browser backend for repeated reads in the same user scope', async () => {
|
|
const service = createService({ isBrowser: true, isElectron: false, isCapacitor: false });
|
|
|
|
await service.getBansForRoom('room-1');
|
|
await service.getBansForRoom('room-2');
|
|
|
|
expect(browserDatabase.initialize).toHaveBeenCalledTimes(1);
|
|
expect(browserDatabase.getBansForRoom).toHaveBeenCalledWith('room-1');
|
|
expect(browserDatabase.getBansForRoom).toHaveBeenCalledWith('room-2');
|
|
});
|
|
|
|
it('reinitializes the browser backend when the stored user scope changes', async () => {
|
|
localStorage.setItem('metoyou_currentUserId', 'user-a');
|
|
|
|
const service = createService({ isBrowser: true, isElectron: false, isCapacitor: false });
|
|
|
|
await service.getBansForRoom('room-1');
|
|
localStorage.setItem('metoyou_currentUserId', 'user-b');
|
|
await service.getBansForRoom('room-2');
|
|
|
|
expect(browserDatabase.initialize).toHaveBeenCalledTimes(2);
|
|
expect(browserDatabase.getBansForRoom).toHaveBeenCalledWith('room-2');
|
|
});
|
|
|
|
it('routes Capacitor shells to native SQLite instead of IndexedDB', async () => {
|
|
const service = createService({ isBrowser: false, isElectron: false, isCapacitor: true });
|
|
|
|
await service.getBansForRoom('room-1');
|
|
|
|
expect(capacitorDatabase.initialize).toHaveBeenCalledTimes(1);
|
|
expect(capacitorDatabase.getBansForRoom).toHaveBeenCalledWith('room-1');
|
|
expect(browserDatabase.initialize).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('routes Electron shells to the IPC SQLite backend', async () => {
|
|
const service = createService({ isBrowser: false, isElectron: true, isCapacitor: false });
|
|
|
|
await service.getBansForRoom('room-1');
|
|
|
|
expect(electronDatabase.initialize).toHaveBeenCalledTimes(1);
|
|
expect(electronDatabase.getBansForRoom).toHaveBeenCalledWith('room-1');
|
|
expect(browserDatabase.initialize).not.toHaveBeenCalled();
|
|
expect(capacitorDatabase.initialize).not.toHaveBeenCalled();
|
|
});
|
|
});
|