|
|
@@ -0,0 +1,320 @@
|
|
|
+import { Test, TestingModule } from '@nestjs/testing';
|
|
|
+import chokidar from 'chokidar';
|
|
|
+import { DatasetsService } from './datasets.service';
|
|
|
+import { DbService } from './db.service';
|
|
|
+import { EventsGateway } from './events.gateway';
|
|
|
+import { TaskQueueService } from './task-queue.service';
|
|
|
+import { WatcherService } from './watcher.service';
|
|
|
+
|
|
|
+// Mock chokidar
|
|
|
+jest.mock('chokidar', () => ({
|
|
|
+ watch: jest.fn(),
|
|
|
+}));
|
|
|
+
|
|
|
+// Mock fs
|
|
|
+jest.mock('fs', () => ({
|
|
|
+ statSync: jest.fn((path: string) => ({
|
|
|
+ isDirectory: () => path.endsWith('subdir') || !path.includes('.'),
|
|
|
+ })),
|
|
|
+}));
|
|
|
+
|
|
|
+// Mock Worker
|
|
|
+jest.mock('worker_threads', () => ({
|
|
|
+ Worker: jest.fn().mockImplementation(() => ({
|
|
|
+ on: jest.fn(),
|
|
|
+ postMessage: jest.fn(),
|
|
|
+ })),
|
|
|
+}));
|
|
|
+
|
|
|
+// Mock DatasetsService
|
|
|
+jest.mock('./datasets.service', () => ({
|
|
|
+ DatasetsService: jest.fn().mockImplementation(() => ({
|
|
|
+ getEnabledDatasetPaths: jest.fn(),
|
|
|
+ getDatasetConfig: jest.fn(),
|
|
|
+ getDatasetExts: jest.fn(),
|
|
|
+ })),
|
|
|
+}));
|
|
|
+
|
|
|
+// Mock DbService
|
|
|
+jest.mock('./db.service', () => ({
|
|
|
+ DbService: jest.fn().mockImplementation(() => ({})),
|
|
|
+}));
|
|
|
+
|
|
|
+// Mock EventsGateway
|
|
|
+jest.mock('./events.gateway', () => ({
|
|
|
+ EventsGateway: jest.fn().mockImplementation(() => ({
|
|
|
+ emitFileUpdate: jest.fn(),
|
|
|
+ emitWatcherUpdate: jest.fn(),
|
|
|
+ })),
|
|
|
+}));
|
|
|
+
|
|
|
+// Mock TaskQueueService
|
|
|
+jest.mock('./task-queue.service', () => ({
|
|
|
+ TaskQueueService: jest.fn().mockImplementation(() => ({})),
|
|
|
+}));
|
|
|
+
|
|
|
+describe('WatcherService', () => {
|
|
|
+ let service: WatcherService;
|
|
|
+ let datasetsService: jest.Mocked<DatasetsService>;
|
|
|
+ let dbService: jest.Mocked<DbService>;
|
|
|
+ let eventsGateway: jest.Mocked<EventsGateway>;
|
|
|
+ let taskQueueService: jest.Mocked<TaskQueueService>;
|
|
|
+
|
|
|
+ beforeEach(async () => {
|
|
|
+ const mockDatasetsService = {
|
|
|
+ getEnabledDatasetPaths: jest.fn(),
|
|
|
+ getDatasetConfig: jest.fn(),
|
|
|
+ getDatasetExts: jest.fn(),
|
|
|
+ };
|
|
|
+
|
|
|
+ const mockDbService = {};
|
|
|
+ const mockEventsGateway = {
|
|
|
+ emitFileUpdate: jest.fn(),
|
|
|
+ emitWatcherUpdate: jest.fn(),
|
|
|
+ };
|
|
|
+ const mockTaskQueueService = {};
|
|
|
+
|
|
|
+ const module: TestingModule = await Test.createTestingModule({
|
|
|
+ providers: [
|
|
|
+ WatcherService,
|
|
|
+ {
|
|
|
+ provide: DatasetsService,
|
|
|
+ useValue: mockDatasetsService,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ provide: DbService,
|
|
|
+ useValue: mockDbService,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ provide: EventsGateway,
|
|
|
+ useValue: mockEventsGateway,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ provide: TaskQueueService,
|
|
|
+ useValue: mockTaskQueueService,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ }).compile();
|
|
|
+
|
|
|
+ service = module.get<WatcherService>(WatcherService);
|
|
|
+ datasetsService = module.get(DatasetsService);
|
|
|
+ dbService = module.get(DbService);
|
|
|
+ eventsGateway = module.get(EventsGateway);
|
|
|
+ taskQueueService = module.get(TaskQueueService);
|
|
|
+ });
|
|
|
+
|
|
|
+ afterEach(() => {
|
|
|
+ jest.clearAllMocks();
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('getDatasetExts', () => {
|
|
|
+ it('should return exts array for a dataset with exts configured', () => {
|
|
|
+ const mockConfig = {
|
|
|
+ movies: {
|
|
|
+ enabled: true,
|
|
|
+ exts: ['.mkv', '.mp4', '.avi'],
|
|
|
+ '/path/to/movies': {},
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ datasetsService.getDatasetConfig.mockReturnValue(mockConfig);
|
|
|
+ datasetsService.getDatasetExts.mockReturnValue(['.mkv', '.mp4', '.avi']);
|
|
|
+
|
|
|
+ const exts = datasetsService.getDatasetExts('movies');
|
|
|
+ expect(exts).toEqual(['.mkv', '.mp4', '.avi']);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should return null for a dataset without exts configured', () => {
|
|
|
+ const mockConfig = {
|
|
|
+ movies: {
|
|
|
+ enabled: true,
|
|
|
+ '/path/to/movies': {},
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ datasetsService.getDatasetConfig.mockReturnValue(mockConfig);
|
|
|
+ datasetsService.getDatasetExts.mockReturnValue(null);
|
|
|
+
|
|
|
+ const exts = datasetsService.getDatasetExts('movies');
|
|
|
+ expect(exts).toBeNull();
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('Chokidar options setup', () => {
|
|
|
+ it('should set up ignored function to filter files by extensions', () => {
|
|
|
+ const mockConfig = {
|
|
|
+ movies: {
|
|
|
+ enabled: true,
|
|
|
+ exts: ['.mkv', '.mp4'],
|
|
|
+ '/path/to/movies': {},
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ datasetsService.getEnabledDatasetPaths.mockReturnValue([
|
|
|
+ '/path/to/movies',
|
|
|
+ ]);
|
|
|
+ datasetsService.getDatasetConfig.mockReturnValue(mockConfig);
|
|
|
+
|
|
|
+ // Mock the getDatasetFromPath method to return 'movies' for our test path
|
|
|
+ jest
|
|
|
+ .spyOn(service as any, 'getDatasetFromPath')
|
|
|
+ .mockReturnValue('movies');
|
|
|
+
|
|
|
+ const mockWatcher = {
|
|
|
+ on: jest.fn().mockReturnThis(),
|
|
|
+ };
|
|
|
+ (chokidar.watch as jest.Mock).mockReturnValue(mockWatcher);
|
|
|
+
|
|
|
+ service.start();
|
|
|
+
|
|
|
+ // Verify chokidar.watch was called
|
|
|
+ expect(chokidar.watch).toHaveBeenCalledWith(
|
|
|
+ ['/path/to/movies'],
|
|
|
+ expect.objectContaining({
|
|
|
+ ignored: expect.any(Function),
|
|
|
+ }),
|
|
|
+ );
|
|
|
+
|
|
|
+ // Get the ignored function from the call
|
|
|
+ const callArgs = (chokidar.watch as jest.Mock).mock.calls[0];
|
|
|
+ const options = callArgs[1];
|
|
|
+ const ignoredFunction = options.ignored;
|
|
|
+
|
|
|
+ // Test the ignored function
|
|
|
+ expect(ignoredFunction('/path/to/movies/movie.mkv')).toBe(false); // Should watch
|
|
|
+ expect(ignoredFunction('/path/to/movies/movie.mp4')).toBe(false); // Should watch
|
|
|
+ expect(ignoredFunction('/path/to/movies/movie.nfo')).toBe(true); // Should ignore
|
|
|
+ expect(ignoredFunction('/path/to/movies/movie.txt')).toBe(true); // Should ignore
|
|
|
+ expect(ignoredFunction('/path/to/movies/subdir')).toBe(false); // Should not ignore directories
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should watch all files when no exts are configured', () => {
|
|
|
+ const mockConfig = {
|
|
|
+ movies: {
|
|
|
+ enabled: true,
|
|
|
+ '/path/to/movies': {},
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ datasetsService.getEnabledDatasetPaths.mockReturnValue([
|
|
|
+ '/path/to/movies',
|
|
|
+ ]);
|
|
|
+ datasetsService.getDatasetConfig.mockReturnValue(mockConfig);
|
|
|
+
|
|
|
+ // Mock the getDatasetFromPath method to return 'movies' for our test path
|
|
|
+ jest
|
|
|
+ .spyOn(service as any, 'getDatasetFromPath')
|
|
|
+ .mockReturnValue('movies');
|
|
|
+
|
|
|
+ const mockWatcher = {
|
|
|
+ on: jest.fn().mockReturnThis(),
|
|
|
+ };
|
|
|
+ (chokidar.watch as jest.Mock).mockReturnValue(mockWatcher);
|
|
|
+
|
|
|
+ service.start();
|
|
|
+
|
|
|
+ // Get the ignored function from the call
|
|
|
+ const callArgs = (chokidar.watch as jest.Mock).mock.calls[0];
|
|
|
+ const options = callArgs[1];
|
|
|
+ const ignoredFunction = options.ignored;
|
|
|
+
|
|
|
+ // Test the ignored function - should watch all files when no exts configured
|
|
|
+ expect(ignoredFunction('/path/to/movies/movie.mkv')).toBe(false);
|
|
|
+ expect(ignoredFunction('/path/to/movies/movie.nfo')).toBe(false);
|
|
|
+ expect(ignoredFunction('/path/to/movies/movie.txt')).toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should ignore files that do not belong to any dataset', () => {
|
|
|
+ const mockConfig = {
|
|
|
+ movies: {
|
|
|
+ enabled: true,
|
|
|
+ exts: ['.mkv', '.mp4'],
|
|
|
+ '/path/to/movies': {},
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ datasetsService.getEnabledDatasetPaths.mockReturnValue([
|
|
|
+ '/path/to/movies',
|
|
|
+ ]);
|
|
|
+ datasetsService.getDatasetConfig.mockReturnValue(mockConfig);
|
|
|
+
|
|
|
+ // Mock the getDatasetFromPath method to return null for files outside datasets
|
|
|
+ jest.spyOn(service as any, 'getDatasetFromPath').mockReturnValue(null);
|
|
|
+
|
|
|
+ const mockWatcher = {
|
|
|
+ on: jest.fn().mockReturnThis(),
|
|
|
+ };
|
|
|
+ (chokidar.watch as jest.Mock).mockReturnValue(mockWatcher);
|
|
|
+
|
|
|
+ service.start();
|
|
|
+
|
|
|
+ // Get the ignored function from the call
|
|
|
+ const callArgs = (chokidar.watch as jest.Mock).mock.calls[0];
|
|
|
+ const options = callArgs[1];
|
|
|
+ const ignoredFunction = options.ignored;
|
|
|
+
|
|
|
+ // Test the ignored function - should ignore files not in any dataset
|
|
|
+ expect(ignoredFunction('/some/other/path/movie.mkv')).toBe(true);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('handleFileAdded', () => {
|
|
|
+ it('should skip processing files with extensions not in dataset exts', () => {
|
|
|
+ const mockConfig = {
|
|
|
+ movies: {
|
|
|
+ enabled: true,
|
|
|
+ exts: ['.mkv', '.mp4'],
|
|
|
+ '/path/to/movies': {},
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ datasetsService.getDatasetConfig.mockReturnValue(mockConfig);
|
|
|
+
|
|
|
+ // Mock the getDatasetFromPath method
|
|
|
+ jest
|
|
|
+ .spyOn(service as any, 'getDatasetFromPath')
|
|
|
+ .mockReturnValue('movies');
|
|
|
+
|
|
|
+ // Spy on logger to check if debug message is logged
|
|
|
+ const loggerSpy = jest.spyOn(service['logger'], 'debug');
|
|
|
+
|
|
|
+ // Call handleFileAdded with a .nfo file
|
|
|
+ (service as any).handleFileAdded('/path/to/movies/movie.nfo');
|
|
|
+
|
|
|
+ // Should log debug message and return early
|
|
|
+ expect(loggerSpy).toHaveBeenCalledWith(
|
|
|
+ 'Skipping file /path/to/movies/movie.nfo - extension not in dataset exts array',
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should process files with extensions in dataset exts', () => {
|
|
|
+ const mockConfig = {
|
|
|
+ movies: {
|
|
|
+ enabled: true,
|
|
|
+ exts: ['.mkv', '.mp4'],
|
|
|
+ '/path/to/movies': {},
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ datasetsService.getDatasetConfig.mockReturnValue(mockConfig);
|
|
|
+
|
|
|
+ // Mock the getDatasetFromPath method
|
|
|
+ jest
|
|
|
+ .spyOn(service as any, 'getDatasetFromPath')
|
|
|
+ .mockReturnValue('movies');
|
|
|
+
|
|
|
+ // Mock the validation worker
|
|
|
+ const postMessageSpy = jest.fn();
|
|
|
+ (service as any).validationWorker.postMessage = postMessageSpy;
|
|
|
+
|
|
|
+ // Call handleFileAdded with a .mkv file
|
|
|
+ (service as any).handleFileAdded('/path/to/movies/movie.mkv');
|
|
|
+
|
|
|
+ // Should call validation worker
|
|
|
+ expect(postMessageSpy).toHaveBeenCalledWith({
|
|
|
+ type: 'validate_file',
|
|
|
+ file: '/path/to/movies/movie.mkv',
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|