@homebridge-plugins/homebridge-air
Version:
The AirNow plugin allows you to monitor the current AirQuality for your Zip Code from HomeKit and Siri.
194 lines • 8.86 kB
JavaScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AirPlatform } from '../platform.js';
// Mock the dependencies
const mockLog = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
};
const mockAPI = {
hap: {
uuid: {
generate: vi.fn().mockReturnValue('test-uuid'),
},
},
platformAccessory: vi.fn(),
registerPlatformAccessories: vi.fn(),
updatePlatformAccessories: vi.fn(),
unregisterPlatformAccessories: vi.fn(),
matter: {
unregisterPlatformAccessories: vi.fn(),
},
on: vi.fn(), // Mock the event listener
};
const mockConfig = {
name: 'Air Quality',
options: {
allowInvalidCharacters: false,
},
};
describe('airPlatform validateAndCleanDisplayName', () => {
let platform;
beforeEach(() => {
// Create platform instance with mocked dependencies
platform = new AirPlatform(mockLog, mockConfig, mockAPI);
vi.clearAllMocks();
});
it('should remove invalid "/" character from device name', async () => {
const result = await platform.validateAndCleanDisplayName('Update/Restart Failure', 'city', 'Update/Restart Failure');
expect(result).toBe('UpdateRestart Failure');
expect(mockLog.warn).toHaveBeenCalledWith(expect.stringContaining('WARNING: The accessory \'Update/Restart Failure\' has an invalid \'city\' characteristic (\'Update/Restart Failure\')'));
});
it('should remove invalid characters and clean start/end', async () => {
const result = await platform.validateAndCleanDisplayName('Test Device', 'city', '/Update/Restart/');
expect(result).toBe('UpdateRestart');
expect(mockLog.warn).toHaveBeenCalledTimes(3); // warning + invalid chars + start/end cleanup
});
it('should return unchanged name for valid characters', async () => {
const result = await platform.validateAndCleanDisplayName('Valid City', 'city', 'Valid City');
expect(result).toBe('Valid City');
expect(mockLog.warn).not.toHaveBeenCalled();
});
it('should handle single character names after cleanup', async () => {
const result = await platform.validateAndCleanDisplayName('Test', 'city', 'A/B');
expect(result).toBe('AB');
});
it('should return original value when allowInvalidCharacters is true', async () => {
platform.config.options = { allowInvalidCharacters: true };
const result = await platform.validateAndCleanDisplayName('Test', 'city', 'Update/Restart Failure');
expect(result).toBe('Update/Restart Failure');
expect(mockLog.warn).not.toHaveBeenCalled();
});
it('should handle multiple consecutive invalid characters', async () => {
const result = await platform.validateAndCleanDisplayName('Test', 'city', 'Update///Restart***Failure');
expect(result).toBe('UpdateRestartFailure');
expect(mockLog.warn).toHaveBeenCalled();
});
it('should handle AQICN station ID format without warnings', async () => {
const result = await platform.validateAndCleanDisplayName('/station/@92323', 'city', '/station/@92323', 'aqicn');
expect(result).toBe('Station 92323');
expect(mockLog.warn).not.toHaveBeenCalled();
});
it('should handle AQICN station name format', async () => {
const result = await platform.validateAndCleanDisplayName('/station/winterthur-veltheim/switzerland', 'city', '/station/winterthur-veltheim/switzerland', 'aqicn');
expect(result).toBe('Winterthur Veltheim Switzerland');
expect(mockLog.warn).not.toHaveBeenCalled();
});
it('should handle AQICN city path format', async () => {
const result = await platform.validateAndCleanDisplayName('/city/switzerland/zurich', 'city', '/city/switzerland/zurich', 'aqicn');
expect(result).toBe('Switzerland Zurich');
expect(mockLog.warn).not.toHaveBeenCalled();
});
it('should handle regular AQICN city names normally', async () => {
const result = await platform.validateAndCleanDisplayName('Zurich', 'city', 'Zurich', 'aqicn');
expect(result).toBe('Zurich');
expect(mockLog.warn).not.toHaveBeenCalled();
});
it('should still validate non-AQICN providers normally', async () => {
const result = await platform.validateAndCleanDisplayName('/station/@92323', 'city', '/station/@92323', 'airnow');
expect(result).toBe('station92323');
expect(mockLog.warn).toHaveBeenCalled();
});
it('should still validate non-city fields normally for AQICN', async () => {
const result = await platform.validateAndCleanDisplayName('/station/@92323', 'name', '/station/@92323', 'aqicn');
expect(result).toBe('station92323');
expect(mockLog.warn).toHaveBeenCalled();
});
});
describe('airPlatform generateAqicnDisplayName', () => {
let platform;
beforeEach(() => {
platform = new AirPlatform(mockLog, mockConfig, mockAPI);
vi.clearAllMocks();
});
it('should convert station ID format to readable name', () => {
const result = platform.generateAqicnDisplayName('/station/@92323');
expect(result).toBe('Station 92323');
});
it('should convert station name format to readable name', () => {
const result = platform.generateAqicnDisplayName('/station/winterthur-veltheim/switzerland');
expect(result).toBe('Winterthur Veltheim Switzerland');
});
it('should convert city path format to readable name', () => {
const result = platform.generateAqicnDisplayName('/city/switzerland/zurich-airport');
expect(result).toBe('Switzerland Zurich Airport');
});
it('should handle single word station names', () => {
const result = platform.generateAqicnDisplayName('/station/zurich');
expect(result).toBe('Zurich');
});
it('should return regular city names unchanged', () => {
const result = platform.generateAqicnDisplayName('Zurich');
expect(result).toBe('Zurich');
});
it('should handle empty string', () => {
const result = platform.generateAqicnDisplayName('');
expect(result).toBe('');
});
});
describe('airPlatform matter fallback cleanup', () => {
let platform;
beforeEach(() => {
platform = new AirPlatform(mockLog, mockConfig, mockAPI);
vi.clearAllMocks();
});
it('should unregister stale matter accessory while in HAP mode', async () => {
const staleAccessory = {
UUID: 'matter-uuid-1',
displayName: 'Matter Device 1',
};
platform.configureMatterAccessory(staleAccessory);
// configureMatterAccessory kicks off async cleanup without awaiting.
await Promise.resolve();
await Promise.resolve();
expect(mockAPI.matter.unregisterPlatformAccessories).toHaveBeenCalledWith('@homebridge-plugins/homebridge-air', 'Air', [staleAccessory]);
});
});
describe('airPlatform verifyConfig provider validation', () => {
let platform;
beforeEach(() => {
platform = new AirPlatform(mockLog, mockConfig, mockAPI);
vi.clearAllMocks();
});
it('should require AirNow location as zip+city or lat+lon', async () => {
platform.config.devices = [
{
provider: 'airnow',
apiKey: 'test-key',
city: undefined,
zipCode: undefined,
latitude: undefined,
longitude: undefined,
},
];
const errorSpy = vi.spyOn(platform, 'errorLog').mockResolvedValue(undefined);
await platform.verifyConfig();
expect(errorSpy).toHaveBeenCalledWith('AirNow requires either (zipCode + city) or (latitude + longitude)');
});
it('should allow AQICN with city path and no coordinates', async () => {
platform.config.devices = [
{
provider: 'aqicn',
apiKey: 'test-key',
city: '/station/@92323',
},
];
const errorSpy = vi.spyOn(platform, 'errorLog').mockResolvedValue(undefined);
await platform.verifyConfig();
expect(errorSpy).not.toHaveBeenCalledWith('AQICN requires either city/station path/URL or (latitude + longitude)');
});
it('should report missing longitude when only latitude is provided', async () => {
platform.config.devices = [
{
provider: 'airnow',
apiKey: 'test-key',
latitude: 47.5,
},
];
const errorSpy = vi.spyOn(platform, 'errorLog').mockResolvedValue(undefined);
await platform.verifyConfig();
expect(errorSpy).toHaveBeenCalledWith('Missing your Longitude');
});
});
//# sourceMappingURL=platform.test.js.map