add weather polling service
This commit is contained in:
+4
-1
@@ -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();
|
||||
|
||||
|
||||
@@ -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,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)
|
||||
];
|
||||
|
||||
@@ -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
@@ -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(() => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user