add weather polling service

This commit is contained in:
StarAppeal
2025-09-20 23:09:22 +02:00
parent d85ce74dbe
commit 6f7dc961f6
12 changed files with 358 additions and 118 deletions
+4 -1
View File
@@ -20,6 +20,7 @@ import {SpotifyApiService} from "./services/spotifyApiService";
import {UserService} from "./services/db/UserService";
import {connectToDatabase, disconnectFromDatabase} from "./services/db/database.service";
import {SpotifyTokenService} from "./services/spotifyTokenService";
import {WeatherPollingService} from "./services/weatherPollingService";
interface ServerConfig {
port: number;
@@ -50,7 +51,9 @@ export class Server {
this.userService = await UserService.create();
const spotifyTokenService = new SpotifyTokenService(this.config.spotifyClientId, this.config.spotifyClientSecret);
const spotifyApiService = new SpotifyApiService();
const spotifyPollingService = new SpotifyPollingService(this.userService, spotifyApiService, spotifyTokenService);
const weatherPollingService = new WeatherPollingService();
this._setupMiddleware();
this._setupRoutes(this.userService, spotifyTokenService);
@@ -60,7 +63,7 @@ export class Server {
console.log(`Server is running on port ${this.config.port}`);
});
this.webSocketServer = new ExtendedWebSocketServer(this.httpServer, this.userService, spotifyPollingService);
this.webSocketServer = new ExtendedWebSocketServer(this.httpServer, this.userService, spotifyPollingService, weatherPollingService);
this._setupGracefulShutdown();
+104
View File
@@ -0,0 +1,104 @@
import {appEventBus, USER_UPDATED_EVENT, WEATHER_STATE_UPDATED_EVENT} from "../utils/eventBus";
import { getCurrentWeather } from "./owmApiService";
import {IUser} from "../db/models/user";
export class WeatherPollingService {
private readonly activeLocationPolls: Map<string, NodeJS.Timeout>;
private readonly locationSubscriptions: Map<string, Set<string>>;
private readonly weatherCache: Map<string, any>;
private readonly userLocationCache: Map<string, string>;
constructor() {
this.activeLocationPolls = new Map();
this.locationSubscriptions = new Map();
this.weatherCache = new Map();
this.userLocationCache = new Map();
appEventBus.on(USER_UPDATED_EVENT, (user: IUser) => {
this._handleUserUpdate(user);
});
}
public subscribeUser(uuid: string, location: string): void {
console.log(`[WeatherPolling] User ${uuid} subscribed to location "${location}"`);
if (!this.locationSubscriptions.has(location)) {
this.locationSubscriptions.set(location, new Set());
}
this.locationSubscriptions.get(location)!.add(uuid);
if (!this.activeLocationPolls.has(location)) {
this._startPollingForLocation(location);
} else {
const cachedWeather = this.weatherCache.get(location);
if (cachedWeather) {
appEventBus.emit(WEATHER_STATE_UPDATED_EVENT, { weatherData: cachedWeather, subscribers: [uuid] });
}
}
this.userLocationCache.set(uuid, location);
}
public unsubscribeUser(uuid: string, location: string): void {
console.log(`[WeatherPolling] User ${uuid} unsubscribed from location "${location}"`);
const subscribers = this.locationSubscriptions.get(location);
if (subscribers) {
subscribers.delete(uuid);
if (subscribers.size === 0) {
this._stopPollingForLocation(location);
this.locationSubscriptions.delete(location);
this.weatherCache.delete(location);
}
}
this.userLocationCache.delete(uuid)
}
private _startPollingForLocation(location: string): void {
console.log(`[WeatherPolling] Starting new poll for location: "${location}"`);
const intervalId = setInterval(() => this._pollLocation(location), 1000 * 60 * 10);
this.activeLocationPolls.set(location, intervalId);
this._pollLocation(location);
}
private _stopPollingForLocation(location: string): void {
if (this.activeLocationPolls.has(location)) {
console.log(`[WeatherPolling] Stopping poll for location: "${location}"`);
clearInterval(this.activeLocationPolls.get(location)!);
this.activeLocationPolls.delete(location);
}
}
private async _pollLocation(location: string): Promise<void> {
try {
console.log(`[WeatherPolling] Fetching weather for "${location}"...`);
const weatherData = await getCurrentWeather(location);
if (!weatherData) return;
this.weatherCache.set(location, weatherData);
const subscribers = this.locationSubscriptions.get(location);
if (subscribers && subscribers.size > 0) {
appEventBus.emit(WEATHER_STATE_UPDATED_EVENT, { weatherData, subscribers: Array.from(subscribers) });
}
} catch (error) {
console.error(`[WeatherPolling] Error polling for location "${location}":`, error);
}
}
private _handleUserUpdate(updatedUser: IUser): void {
const uuid = updatedUser.uuid;
const newLocation = updatedUser.location;
const oldLocation = this.userLocationCache.get(uuid);
if (oldLocation && newLocation && oldLocation !== newLocation) {
console.log(`[WeatherPolling] Detected location change for user ${uuid} via User-Update.`);
this.unsubscribeUser(uuid, oldLocation);
this.subscribeUser(uuid, newLocation);
}
}
}
+2 -1
View File
@@ -2,4 +2,5 @@ import { EventEmitter } from 'events';
export const appEventBus = new EventEmitter();
export const USER_UPDATED_EVENT = 'user:updated';
export const SPOTIFY_STATE_UPDATED_EVENT = 'spotify:updated';
export const SPOTIFY_STATE_UPDATED_EVENT = 'spotify:updated';
export const WEATHER_STATE_UPDATED_EVENT = 'weather:updated';
@@ -1,49 +1,24 @@
import {CustomWebsocketEvent} from "./customWebsocketEvent";
import {WebsocketEventType} from "./websocketEventType";
import {NoData} from "./NoData";
import {getCurrentWeather} from "../../../services/owmApiService";
export const WeatherAsyncUpdateEvent = "WEATHER_UPDATE";
import {WeatherPollingService} from "../../../services/weatherPollingService";
import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket";
export class GetWeatherUpdatesEvent extends CustomWebsocketEvent<NoData> {
event = WebsocketEventType.GET_WEATHER_UPDATES;
private readonly weatherPollingService: WeatherPollingService;
handler = async () => {
console.log("Starting weather updates");
this.ws.emit(WebsocketEventType.GET_SINGLE_WEATHER_UPDATE);
if (this.ws.asyncUpdates.has(WeatherAsyncUpdateEvent)) {
console.log("Weather updates already running");
return;
}
this.ws.asyncUpdates.set(WeatherAsyncUpdateEvent, setInterval(() => {
this.ws.emit(WebsocketEventType.GET_SINGLE_WEATHER_UPDATE);
}, 1000 * 60));
}
}
export class GetSingleWeatherUpdateEvent extends CustomWebsocketEvent<NoData> {
event = WebsocketEventType.GET_SINGLE_WEATHER_UPDATE;
handler = async () => {
console.log("Getting single weather update event");
await this.weatherUpdates();
constructor(ws: ExtendedWebSocket, weatherPollingService:WeatherPollingService) {
super(ws);
this.weatherPollingService = weatherPollingService;
}
private async weatherUpdates() {
console.log("Checking weather")
handler = async () => {
const user = this.ws.user;
const weather = await getCurrentWeather(user.location);
console.log(weather);
this.ws.send(JSON.stringify({
type: "WEATHER_UPDATE",
payload: weather,
}), {binary: false});
if (user?.location && user.uuid) {
this.weatherPollingService.subscribeUser(user.uuid, user.location);
}
}
}
@@ -1,17 +1,24 @@
import {CustomWebsocketEvent} from "./customWebsocketEvent";
import {WebsocketEventType} from "./websocketEventType";
import {WeatherAsyncUpdateEvent} from "./getWeatherUpdatesEvent";
import {NoData} from "./NoData";
import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket";
import {WeatherPollingService} from "../../../services/weatherPollingService";
export class StopWeatherUpdatesEvent extends CustomWebsocketEvent<NoData> {
event = WebsocketEventType.STOP_WEATHER_UPDATES;
private readonly weatherPollingService: WeatherPollingService;
constructor(ws:ExtendedWebSocket, weatherPollingService:WeatherPollingService) {
super(ws);
this.weatherPollingService = weatherPollingService;
}
handler = async () => {
if (this.ws.asyncUpdates.has(WeatherAsyncUpdateEvent)) {
clearInterval(this.ws.asyncUpdates.get(WeatherAsyncUpdateEvent));
this.ws.asyncUpdates.delete(WeatherAsyncUpdateEvent);
console.log("Weather updates stopped");
console.log(`User ${this.ws.user.uuid} requested stop weather updates`);
const user = this.ws.user;
if (user?.location && user.uuid) {
this.weatherPollingService.unsubscribeUser(user.uuid, user.location);
}
}
@@ -3,22 +3,22 @@ import {GetSettingsEvent} from "./getSettingsEvent";
import {ErrorEvent} from "./errorEvent";
import {GetSpotifyUpdatesEvent} from "./getSpotifyUpdatesEvent";
import {GetStateEvent} from "./getStateEvent";
import {GetSingleWeatherUpdateEvent, GetWeatherUpdatesEvent} from "./getWeatherUpdatesEvent";
import {GetWeatherUpdatesEvent} from "./getWeatherUpdatesEvent";
import {StopSpotifyUpdatesEvent} from "./stopSpotifyUpdatesEvent";
import {StopWeatherUpdatesEvent} from "./stopWeatherUpdatesEvent";
import { UpdateUserSingleEvent} from "./updateUserEvent";
import {CustomWebsocketEvent} from "./customWebsocketEvent";
import {SpotifyPollingService} from "../../../services/spotifyPollingService";
import {WeatherPollingService} from "../../../services/weatherPollingService";
export function getEventListeners(ws: ExtendedWebSocket, spotifyPollingService: SpotifyPollingService): CustomWebsocketEvent[] {
export function getEventListeners(ws: ExtendedWebSocket, spotifyPollingService: SpotifyPollingService, weatherPollingService:WeatherPollingService): CustomWebsocketEvent[] {
return [
new GetStateEvent(ws),
new GetSettingsEvent(ws),
new GetSpotifyUpdatesEvent(ws, spotifyPollingService),
new StopSpotifyUpdatesEvent(ws, spotifyPollingService),
new GetSingleWeatherUpdateEvent(ws),
new GetWeatherUpdatesEvent(ws),
new StopWeatherUpdatesEvent(ws),
new GetWeatherUpdatesEvent(ws, weatherPollingService),
new StopWeatherUpdatesEvent(ws, weatherPollingService),
new UpdateUserSingleEvent(ws),
new ErrorEvent(ws)
];
+3 -2
View File
@@ -2,9 +2,10 @@ import {ExtendedWebSocket} from "../../interfaces/extendedWebsocket";
import {CustomWebsocketEvent} from "./websocketCustomEvents/customWebsocketEvent";
import {getEventListeners} from "./websocketCustomEvents/websocketEventUtils";
import {SpotifyPollingService} from "../../services/spotifyPollingService";
import {WeatherPollingService} from "../../services/weatherPollingService";
export class WebsocketEventHandler {
constructor(private webSocket: ExtendedWebSocket, private spotifyPollingService: SpotifyPollingService) {
constructor(private webSocket: ExtendedWebSocket, private spotifyPollingService: SpotifyPollingService, private readonly weatherPollingService: WeatherPollingService) {
}
public enableErrorEvent() {
@@ -45,7 +46,7 @@ export class WebsocketEventHandler {
}
public registerCustomEvents() {
const events = getEventListeners(this.webSocket, this.spotifyPollingService);
const events = getEventListeners(this.webSocket, this.spotifyPollingService, this.weatherPollingService);
events.forEach(this.registerCustomEvent, this);
}
+23 -3
View File
@@ -5,19 +5,27 @@ import {ExtendedWebSocket} from "./interfaces/extendedWebsocket";
import {WebsocketServerEventHandler} from "./utils/websocket/websocketServerEventHandler";
import {WebsocketEventHandler} from "./utils/websocket/websocketEventHandler";
import {WebsocketEventType} from "./utils/websocket/websocketCustomEvents/websocketEventType";
import {appEventBus, SPOTIFY_STATE_UPDATED_EVENT, USER_UPDATED_EVENT} from "./utils/eventBus";
import {
appEventBus,
SPOTIFY_STATE_UPDATED_EVENT,
USER_UPDATED_EVENT,
WEATHER_STATE_UPDATED_EVENT
} from "./utils/eventBus";
import {IUser} from "./db/models/user";
import {SpotifyPollingService} from "./services/spotifyPollingService";
import {UserService} from "./services/db/UserService";
import {WeatherPollingService} from "./services/weatherPollingService";
export class ExtendedWebSocketServer {
private readonly _wss: WebSocketServer;
private readonly userService: UserService;
private readonly spotifyPollingService: SpotifyPollingService;
private readonly weatherPollingService: WeatherPollingService;
constructor(server: Server, userService: UserService, spotifyPollingService: SpotifyPollingService) {
constructor(server: Server, userService: UserService, spotifyPollingService: SpotifyPollingService, weatherPollingService: WeatherPollingService) {
this.userService = userService;
this.spotifyPollingService = spotifyPollingService;
this.weatherPollingService = weatherPollingService;
this._wss = new WebSocketServer({
server,
@@ -63,7 +71,7 @@ export class ExtendedWebSocketServer {
private _onNewClientReady(ws: ExtendedWebSocket): void {
console.log("WebSocket client connected and authenticated");
const socketEventHandler = new WebsocketEventHandler(ws, this.spotifyPollingService);
const socketEventHandler = new WebsocketEventHandler(ws, this.spotifyPollingService, this.weatherPollingService);
socketEventHandler.enableErrorEvent();
socketEventHandler.enablePongEvent();
@@ -99,6 +107,18 @@ export class ExtendedWebSocketServer {
}
});
appEventBus.on(WEATHER_STATE_UPDATED_EVENT, ({weatherData, subscribers}) => {
for (const uuid of subscribers) {
const client = this._findClientByUUID(uuid);
if (client) {
client.send(JSON.stringify({
type: "WEATHER_UPDATE",
payload: weatherData,
}), {binary: false});
}
}
});
}
private _findClientByUUID(uuid: string): ExtendedWebSocket | undefined {
@@ -0,0 +1,152 @@
import {describe, it, expect, vi, beforeEach, afterEach, Mocked} from "vitest";
import {appEventBus, USER_UPDATED_EVENT, WEATHER_STATE_UPDATED_EVENT} from "../../src/utils/eventBus";
import {WeatherPollingService} from "../../src/services/weatherPollingService";
import {IUser} from "../../src/db/models/user";
import {getCurrentWeather} from "../../src/services/owmApiService";
vi.mock("../../src/services/owmApiService");
vi.mock("../../src/utils/eventBus", () => ({
appEventBus: {
on: vi.fn(),
emit: vi.fn(),
},
WEATHER_STATE_UPDATED_EVENT: 'weather:state-updated',
USER_UPDATED_EVENT: 'user:updated',
}));
vi.mock("../../src/services/owmApiService", () => ({
getCurrentWeather: vi.fn(),
}));
describe("WeatherPollingService", () => {
let mockedAppEventBus: Mocked<typeof appEventBus>;
const mockedGetCurrentWeather = vi.mocked(getCurrentWeather);
let pollingService: WeatherPollingService;
const mockUser: IUser = {
uuid: "user-123",
location: "Berlin",
} as any;
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
mockedAppEventBus = appEventBus as Mocked<typeof appEventBus>;
pollingService = new WeatherPollingService();
});
afterEach(() => {
vi.useRealTimers();
});
describe("Subscription Management", () => {
it("should start a new poll when the first user subscribes to a location", async () => {
mockedGetCurrentWeather.mockResolvedValue({ temp: 10 } as any);
pollingService.subscribeUser(mockUser.uuid, mockUser.location);
await vi.advanceTimersByTimeAsync(0);
expect(vi.getTimerCount()).toBe(1);
expect(mockedGetCurrentWeather).toHaveBeenCalledOnce();
});
it("should NOT start a new poll if another user subscribes to the same location", async () => {
pollingService.subscribeUser("user-1", "Berlin");
pollingService.subscribeUser("user-2", "Berlin");
await vi.advanceTimersByTimeAsync(0);
expect(vi.getTimerCount()).toBe(1);
expect(mockedGetCurrentWeather).toHaveBeenCalledTimes(1);
});
it("should stop the poll when the last user unsubscribes from a location", async () => {
pollingService.subscribeUser("user-1", "Berlin");
pollingService.subscribeUser("user-2", "Berlin");
await vi.advanceTimersByTimeAsync(0);
expect(vi.getTimerCount()).toBe(1);
pollingService.unsubscribeUser("user-1", "Berlin");
expect(vi.getTimerCount()).toBe(1);
pollingService.unsubscribeUser("user-2", "Berlin");
expect(vi.getTimerCount()).toBe(0);
});
});
describe("Polling and Event Emission", () => {
it("should periodically poll the API and emit an event for all subscribers of that location", async () => {
const weatherData = { temp: 12, city: "London" };
mockedGetCurrentWeather.mockResolvedValue(weatherData as any);
pollingService.subscribeUser("user-london-1", "London");
pollingService.subscribeUser("user-london-2", "London");
await vi.advanceTimersByTimeAsync(0);
expect(mockedAppEventBus.emit).toHaveBeenCalledTimes(1);
expect(mockedAppEventBus.emit).toHaveBeenCalledWith(WEATHER_STATE_UPDATED_EVENT, {
weatherData,
subscribers: ["user-london-1", "user-london-2"],
});
await vi.advanceTimersByTimeAsync(10 * 60 * 1000);
expect(mockedAppEventBus.emit).toHaveBeenCalledTimes(2);
await vi.advanceTimersByTimeAsync(10 * 60 * 1000);
expect(mockedAppEventBus.emit).toHaveBeenCalledTimes(3);
});
});
describe("Automatic Location Change Handling (via USER_UPDATED_EVENT)", () => {
let userUpdateListener: (user: IUser) => void;
beforeEach(() => {
const onCall = mockedAppEventBus.on.mock.calls.find(
call => call[0] === USER_UPDATED_EVENT
);
if (onCall) {
userUpdateListener = onCall[1];
}
});
it("should be listening for USER_UPDATED_EVENT", () => {
expect(userUpdateListener).toBeDefined();
expect(typeof userUpdateListener).toBe("function");
});
it("should automatically move a user's subscription when their location changes", () => {
const unsubscribeSpy = vi.spyOn(pollingService, 'unsubscribeUser');
const subscribeSpy = vi.spyOn(pollingService, 'subscribeUser');
pollingService.subscribeUser("user-moving", "Berlin");
const updatedUser = { uuid: "user-moving", location: "London" } as IUser;
userUpdateListener(updatedUser);
expect(unsubscribeSpy).toHaveBeenCalledOnce();
expect(unsubscribeSpy).toHaveBeenCalledWith("user-moving", "Berlin");
expect(subscribeSpy).toHaveBeenCalledTimes(2); // Once for Berlin, once for London
expect(subscribeSpy).toHaveBeenCalledWith("user-moving", "London");
});
it("should do nothing if the user's location has not changed", () => {
const unsubscribeSpy = vi.spyOn(pollingService, 'unsubscribeUser');
pollingService.subscribeUser("user-staying", "Berlin");
const updatedUser = { uuid: "user-staying", location: "Berlin", name: "New Name" } as IUser;
userUpdateListener(updatedUser);
expect(unsubscribeSpy).not.toHaveBeenCalled();
});
});
});
@@ -6,14 +6,12 @@ import {GetSpotifyUpdatesEvent} from "../../../../src/utils/websocket/websocketC
import {SpotifyPollingService} from "../../../../src/services/spotifyPollingService";
import {createMockSpotifyPollingService,} from "../../../helpers/testSetup";
import {StopSpotifyUpdatesEvent} from "../../../../src/utils/websocket/websocketCustomEvents/stopSpotifyUpdatesEvent";
import {
GetSingleWeatherUpdateEvent,
GetWeatherUpdatesEvent
import {GetWeatherUpdatesEvent
} from "../../../../src/utils/websocket/websocketCustomEvents/getWeatherUpdatesEvent";
import {ErrorEvent} from "../../../../src/utils/websocket/websocketCustomEvents/errorEvent";
import {UpdateUserSingleEvent} from "../../../../src/utils/websocket/websocketCustomEvents/updateUserEvent";
import {StopWeatherUpdatesEvent} from "../../../../src/utils/websocket/websocketCustomEvents/stopWeatherUpdatesEvent";
import {getCurrentWeather} from "../../../../src/services/owmApiService";
import {WeatherPollingService} from "../../../../src/services/weatherPollingService";
const createMockWebSocket = (userPayload: any = {}): ExtendedWebSocket => {
return {
@@ -33,15 +31,18 @@ vi.mock("../../../../src/services/owmApiService", () => ({
getCurrentWeather: vi.fn(),
}));
vi.mock("../../../../src/services/weatherPollingService");
describe("WebSocket Custom Event Handlers", () => {
let mockSpotifyPollingService: Mocked<SpotifyPollingService>;
const mockedGetCurrentWeather = vi.mocked(getCurrentWeather);
let mockWeatherPollingService: Mocked<WeatherPollingService>;
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
mockSpotifyPollingService = createMockSpotifyPollingService() as any;
mockWeatherPollingService = new WeatherPollingService() as Mocked<WeatherPollingService>;
})
afterEach(() => {
@@ -106,86 +107,55 @@ describe("WebSocket Custom Event Handlers", () => {
});
describe("GetWeatherUpdatesEvent", () => {
it("should emit a single weather update immediately", async () => {
const mockWs = createMockWebSocket();
const event = new GetWeatherUpdatesEvent(mockWs);
it("should subscribe the user to the WeatherPollingService using their location", async () => {
const mockWs = createMockWebSocket({
uuid: "user-uuid-weather",
location: "London"
});
const event = new GetWeatherUpdatesEvent(mockWs, mockWeatherPollingService);
await event.handler();
expect(mockWs.emit).toHaveBeenCalledWith("GET_SINGLE_WEATHER_UPDATE");
expect(mockWeatherPollingService.subscribeUser).toHaveBeenCalledOnce();
expect(mockWeatherPollingService.subscribeUser).toHaveBeenCalledWith("user-uuid-weather", "London");
});
it("should set up an interval to emit weather updates periodically", async () => {
const mockWs = createMockWebSocket();
const event = new GetWeatherUpdatesEvent(mockWs);
it("should do nothing if the user has no location", async () => {
const mockWs = createMockWebSocket({ location: undefined });
const event = new GetWeatherUpdatesEvent(mockWs, mockWeatherPollingService);
await event.handler();
expect(mockWs.emit).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(60 * 1000);
expect(mockWs.emit).toHaveBeenCalledTimes(2);
await vi.advanceTimersByTimeAsync(60 * 1000);
expect(mockWs.emit).toHaveBeenCalledTimes(3);
});
it("should not set up a new interval if one is already running", async () => {
const mockWs = createMockWebSocket();
const intervalId = setInterval(() => {}, 1000); // Simuliere einen laufenden Timer
mockWs.asyncUpdates.set("WEATHER_UPDATE", intervalId);
const event = new GetWeatherUpdatesEvent(mockWs);
await event.handler();
expect(mockWs.asyncUpdates.get("WEATHER_UPDATE")).toBe(intervalId);
expect(mockWs.emit).toHaveBeenCalledOnce();
});
});
describe("GetSingleWeatherUpdateEvent", () => {
it("should fetch weather and send an update to the client", async () => {
const mockWs = createMockWebSocket({ location: "London" });
const weatherData = { temp: 15, city: "London" };
mockedGetCurrentWeather.mockResolvedValue(weatherData as any);
const event = new GetSingleWeatherUpdateEvent(mockWs);
await event.handler();
expect(mockedGetCurrentWeather).toHaveBeenCalledOnce();
expect(mockedGetCurrentWeather).toHaveBeenCalledWith("London");
expect(mockWs.send).toHaveBeenCalledOnce();
const expectedMessage = JSON.stringify({ type: "WEATHER_UPDATE", payload: weatherData });
expect(mockWs.send).toHaveBeenCalledWith(expectedMessage, { binary: false });
expect(mockWeatherPollingService.subscribeUser).not.toHaveBeenCalled();
});
});
describe("StopWeatherUpdatesEvent", () => {
it("should clear the weather update interval if it exists", async () => {
const mockWs = createMockWebSocket();
const intervalId = setInterval(() => {}, 1000);
mockWs.asyncUpdates.set("WEATHER_UPDATE", intervalId);
it("should unsubscribe the user from the WeatherPollingService using their location", async () => {
const mockWs = createMockWebSocket({
uuid: "user-uuid-weather",
location: "Paris"
});
const event = new StopWeatherUpdatesEvent(mockWs);
const event = new StopWeatherUpdatesEvent(mockWs, mockWeatherPollingService);
await event.handler();
expect(mockWs.asyncUpdates.has("WEATHER_UPDATE")).toBe(false);
expect(vi.getTimerCount()).toBe(0);
expect(mockWeatherPollingService.unsubscribeUser).toHaveBeenCalledOnce();
expect(mockWeatherPollingService.unsubscribeUser).toHaveBeenCalledWith("user-uuid-weather", "Paris");
});
it("should do nothing if no weather update interval is running", async () => {
const mockWs = createMockWebSocket();
it("should do nothing if the user has no location", async () => {
const mockWs = createMockWebSocket({ location: undefined });
const event = new StopWeatherUpdatesEvent(mockWs);
const event = new StopWeatherUpdatesEvent(mockWs, mockWeatherPollingService);
await event.handler();
expect(mockWs.asyncUpdates.size).toBe(0);
expect(mockWeatherPollingService.unsubscribeUser).not.toHaveBeenCalled();
});
});
describe("UpdateUserSingleEvent", () => {
it("should update the user property on the websocket object", async () => {
const mockWs = createMockWebSocket();
@@ -3,11 +3,13 @@ import { WebsocketEventHandler } from "../../../src/utils/websocket/websocketEve
import { ExtendedWebSocket } from "../../../src/interfaces/extendedWebsocket";
import { CustomWebsocketEvent } from "../../../src/utils/websocket/websocketCustomEvents/customWebsocketEvent";
import {SpotifyPollingService} from "../../../src/services/spotifyPollingService";
import {WeatherPollingService} from "../../../src/services/weatherPollingService";
describe("WebsocketEventHandler", () => {
let mockWebSocket: Mocked<ExtendedWebSocket>;
let websocketEventHandler: WebsocketEventHandler;
let mockSpotifyPollingService: Mocked<SpotifyPollingService>
let mockWeatherPollingService: Mocked<WeatherPollingService>
let registeredHandlers: Map<string, (...args: any[]) => void>;
beforeEach(() => {
@@ -31,8 +33,9 @@ describe("WebsocketEventHandler", () => {
// not used in this test
mockSpotifyPollingService = {} as Mocked<SpotifyPollingService>;
mockWeatherPollingService = {} as Mocked<WeatherPollingService>;
websocketEventHandler = new WebsocketEventHandler(mockWebSocket, mockSpotifyPollingService);
websocketEventHandler = new WebsocketEventHandler(mockWebSocket, mockSpotifyPollingService, mockWeatherPollingService);
});
afterEach(() => {
+6 -2
View File
@@ -10,6 +10,7 @@ import {UserService} from "../src/services/db/UserService";
import {SpotifyPollingService} from "../src/services/spotifyPollingService";
import { USER_UPDATED_EVENT, SPOTIFY_STATE_UPDATED_EVENT } from "../src/utils/eventBus";
import { WebsocketEventType } from "../src/utils/websocket/websocketCustomEvents/websocketEventType";
import {WeatherPollingService} from "../src/services/weatherPollingService";
let mockWssInstance: Mocked<WebSocketServer>;
let mockServerEventHandler: Mocked<WebsocketServerEventHandler>;
@@ -25,6 +26,7 @@ vi.mock("../src/utils/eventBus", () => ({
},
USER_UPDATED_EVENT: 'user:updated',
SPOTIFY_STATE_UPDATED_EVENT: 'spotify:state-updated',
WEATHER_STATE_UPDATED_EVENT: 'weather-state:updated',
}));
vi.mock("ws", () => ({
@@ -44,6 +46,7 @@ describe("ExtendedWebSocketServer", () => {
let extendedWss: ExtendedWebSocketServer;
let mockUserService: Mocked<UserService>;
let mockSpotifyPollingService: Mocked<SpotifyPollingService>
let mockWeatherPollingService: Mocked<WeatherPollingService>;
beforeEach(() => {
vi.clearAllMocks();
@@ -63,10 +66,11 @@ describe("ExtendedWebSocketServer", () => {
} as unknown as Mocked<WebSocketServer>;
mockSpotifyPollingService = {} as any;
mockWeatherPollingService = {} as any;
mockUserService = createMockUserService();
extendedWss = new ExtendedWebSocketServer(mockHttpServer, mockUserService, mockSpotifyPollingService);
extendedWss = new ExtendedWebSocketServer(mockHttpServer, mockUserService, mockSpotifyPollingService, mockWeatherPollingService);
});
describe("Constructor and Setup", () => {
@@ -136,7 +140,7 @@ describe("ExtendedWebSocketServer", () => {
it("should create and configure a WebsocketEventHandler for new clients", () => {
connectionHandler(mockWsClient, {});
expect(vi.mocked(WebsocketEventHandler)).toHaveBeenCalledWith(mockWsClient, mockSpotifyPollingService);
expect(vi.mocked(WebsocketEventHandler)).toHaveBeenCalledWith(mockWsClient, mockSpotifyPollingService, mockWeatherPollingService);
expect(mockClientEventHandler.enableErrorEvent).toHaveBeenCalled();
expect(mockClientEventHandler.enablePongEvent).toHaveBeenCalled();
expect(mockClientEventHandler.enableMessageEvent).toHaveBeenCalled();