// tests/drone-service.test.ts // Copyright (C) 2026 Robert Colbert // All Rights Reserved import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { Types } from 'mongoose'; import { DroneStatus, IDroneRegistration } from '@gadget/api'; import { createMockUser, createMockDroneRegistration } from './fixtures'; // Mock SocketService vi.mock('../src/services/socket'); import DroneService from '../src/services/drone'; import SocketService from '../src/services/socket'; describe('DroneService.requestTermination', () => { let mockUser: any; let mockDrone: any; let mockDroneSession: any; let mockSocket: any; beforeEach(() => { vi.clearAllMocks(); mockUser = createMockUser(); mockDrone = createMockDroneRegistration(mockUser); mockSocket = { emit: vi.fn(), }; mockDroneSession = { socket: mockSocket, registration: mockDrone, }; // Setup mocks for DroneService methods vi.spyOn(DroneService, 'getById').mockResolvedValue(mockDrone); vi.spyOn(DroneService, 'setStatus').mockResolvedValue(mockDrone); vi.spyOn(SocketService, 'getDroneSession').mockReturnValue(mockDroneSession); }); it('should return error if drone is already offline', async () => { const offlineDrone = createMockDroneRegistration(mockUser, { status: DroneStatus.Offline, }); vi.spyOn(DroneService, 'getById').mockResolvedValue(offlineDrone); const result = await DroneService.requestTermination(offlineDrone._id); expect(result.success).toBe(false); expect(result.message).toBe('Drone is already offline'); expect(SocketService.getDroneSession).not.toHaveBeenCalled(); }); it('should mark drone offline if not connected and return success', async () => { const connectedDrone = createMockDroneRegistration(mockUser, { status: DroneStatus.Available, }); vi.spyOn(DroneService, 'getById').mockResolvedValue(connectedDrone); vi.spyOn(SocketService, 'getDroneSession').mockImplementation(() => { throw new Error('drone session not found'); }); const result = await DroneService.requestTermination(connectedDrone._id); expect(result.success).toBe(true); expect(result.message).toContain('not connected'); expect(DroneService.setStatus).toHaveBeenCalledWith(connectedDrone, DroneStatus.Offline); }); it('should emit requestTermination to drone socket', async () => { const availableDrone = createMockDroneRegistration(mockUser, { status: DroneStatus.Available, }); vi.spyOn(DroneService, 'getById') .mockResolvedValueOnce(availableDrone) .mockResolvedValueOnce(availableDrone) .mockResolvedValueOnce(createMockDroneRegistration(mockUser, { status: DroneStatus.Offline })); vi.spyOn(SocketService, 'getDroneSession').mockReturnValue(mockDroneSession); // Mock socket emit to call the callback with success mockSocket.emit.mockImplementation((event: string, cb: Function) => { if (event === 'requestTermination') { cb(true); } }); const result = await DroneService.requestTermination(availableDrone._id); expect(SocketService.getDroneSession).toHaveBeenCalledWith(availableDrone); expect(mockSocket.emit).toHaveBeenCalledWith('requestTermination', expect.any(Function)); expect(result.success).toBe(true); }); it('should handle drone rejection of termination', async () => { const availableDrone = createMockDroneRegistration(mockUser, { status: DroneStatus.Available, }); vi.spyOn(DroneService, 'getById').mockResolvedValue(availableDrone); vi.spyOn(SocketService, 'getDroneSession').mockReturnValue(mockDroneSession); // Mock socket emit to call the callback with failure mockSocket.emit.mockImplementation((event: string, cb: Function) => { if (event === 'requestTermination') { cb(false); } }); const result = await DroneService.requestTermination(availableDrone._id); expect(result.success).toBe(false); expect(result.message).toBe('Drone rejected termination request'); }); it('should timeout and force offline after 10 seconds', async () => { vi.useFakeTimers(); const availableDrone = createMockDroneRegistration(mockUser, { status: DroneStatus.Available, }); vi.spyOn(DroneService, 'getById').mockResolvedValue(availableDrone); vi.spyOn(SocketService, 'getDroneSession').mockReturnValue(mockDroneSession); // Mock socket emit but never call the callback (simulating no response) mockSocket.emit.mockImplementation(() => { // Never call callback }); const terminationPromise = DroneService.requestTermination(availableDrone._id); // Advance time past 10 second timeout await vi.advanceTimersByTimeAsync(10000); const result = await terminationPromise; expect(result.success).toBe(true); expect(result.message).toContain('timed out'); expect(DroneService.setStatus).toHaveBeenCalledWith(availableDrone, DroneStatus.Offline); vi.useRealTimers(); }); it('should poll for offline status after drone accepts termination', async () => { vi.useFakeTimers(); const availableDrone = createMockDroneRegistration(mockUser, { status: DroneStatus.Available, }); const offlineDrone = createMockDroneRegistration(mockUser, { status: DroneStatus.Offline, }); vi.spyOn(DroneService, 'getById') .mockResolvedValueOnce(availableDrone) // Initial check .mockResolvedValueOnce(offlineDrone); // Poll after accept vi.spyOn(SocketService, 'getDroneSession').mockReturnValue(mockDroneSession); // Mock socket emit to call callback with success immediately mockSocket.emit.mockImplementation((event: string, cb: Function) => { if (event === 'requestTermination') { cb(true); } }); const terminationPromise = DroneService.requestTermination(availableDrone._id); // Advance time to allow callback and one poll cycle (500ms interval) await vi.advanceTimersByTimeAsync(600); const result = await terminationPromise; expect(result.success).toBe(true); expect(result.message).toBeUndefined(); vi.useRealTimers(); }); });