From 22b5d7a4e4cc98abd573fd74bd255a5c39174a43 Mon Sep 17 00:00:00 2001 From: StarAppeal Date: Sat, 20 Sep 2025 20:37:47 +0200 Subject: [PATCH] major refactoring and use spotify polling service instead of update every second --- src/db/services/spotifyApiService.ts | 45 --- src/interfaces/CurrentlyPlaying.ts | 21 ++ src/rest/auth.ts | 5 +- src/rest/middleware/isAdmin.ts | 2 +- src/rest/restUser.ts | 2 +- src/rest/spotifyTokenGenerator.ts | 2 +- src/server.ts | 12 +- src/{db => }/services/db/UserService.ts | 10 +- src/{db => }/services/db/database.service.ts | 0 src/{db => }/services/owmApiService.ts | 0 src/services/spotifyApiService.ts | 31 ++ src/services/spotifyPollingService.ts | 105 ++++++ src/{db => }/services/spotifyTokenService.ts | 2 +- src/utils/eventBus.ts | 3 +- .../customWebsocketEventUserService.ts | 13 - .../getSpotifyUpdatesEvent.ts | 85 +---- .../getWeatherUpdatesEvent.ts | 2 +- .../stopSpotifyUpdatesEvent.ts | 23 +- .../websocketEventType.ts | 6 +- .../websocketEventUtils.ts | 12 +- src/utils/websocket/websocketEventHandler.ts | 7 +- .../websocket/websocketServerEventHandler.ts | 2 +- src/websocket.ts | 54 +-- tests/db/services/spotifyApiService.test.ts | 339 ------------------ tests/helpers/testSetup.ts | 9 +- tests/rest/restUser.test.ts | 2 +- tests/rest/spotifyTokenGenerator.test.ts | 1 - tests/server.test.ts | 6 +- .../{db => }/services/db/UserService.test.ts | 8 +- .../services/db/database.service.test.ts | 2 +- tests/{db => }/services/owmApiService.test.ts | 2 +- tests/services/spotifyApiService.test.ts | 334 +++++++++++++++++ tests/services/spotifyPollingService.test.ts | 195 ++++++++++ .../services/spotifyTokenService.test.ts | 8 +- .../websocketEventUtils.test.ts | 110 ++---- .../websocket/websocketEventHandler.test.ts | 12 +- .../websocketServerEventHandler.test.ts | 2 +- tests/websocket.test.ts | 16 +- 38 files changed, 843 insertions(+), 647 deletions(-) delete mode 100644 src/db/services/spotifyApiService.ts create mode 100644 src/interfaces/CurrentlyPlaying.ts rename src/{db => }/services/db/UserService.ts (91%) rename src/{db => }/services/db/database.service.ts (100%) rename src/{db => }/services/owmApiService.ts (100%) create mode 100644 src/services/spotifyApiService.ts create mode 100644 src/services/spotifyPollingService.ts rename src/{db => }/services/spotifyTokenService.ts (95%) delete mode 100644 src/utils/websocket/websocketCustomEvents/customWebsocketEventUserService.ts delete mode 100644 tests/db/services/spotifyApiService.test.ts rename tests/{db => }/services/db/UserService.test.ts (96%) rename tests/{db => }/services/db/database.service.test.ts (98%) rename tests/{db => }/services/owmApiService.test.ts (99%) create mode 100644 tests/services/spotifyApiService.test.ts create mode 100644 tests/services/spotifyPollingService.test.ts rename tests/{db => }/services/spotifyTokenService.test.ts (90%) diff --git a/src/db/services/spotifyApiService.ts b/src/db/services/spotifyApiService.ts deleted file mode 100644 index bbeda7f..0000000 --- a/src/db/services/spotifyApiService.ts +++ /dev/null @@ -1,45 +0,0 @@ -import axios from "axios"; - -export interface CurrentlyPlaying { - timestamp: number; - context?: { - type: string; - uri: string; - }; - progress_ms?: number; - item?: { - name: string; - artists: { - name: string; - uri: string; - }[]; - album: { - name: string; - uri: string; - }; - duration_ms: number; - }; - is_playing: boolean; -} - - -export async function getCurrentlyPlaying(accessToken: string) { - try { - const response = await axios.get("https://api.spotify.com/v1/me/player/currently-playing", { - headers: { - Authorization: `Bearer ${accessToken}` - }, params: { - additional_types: "episode" - } - }); - - if (response.status === 204) { - console.log("Es wird gerade nichts abgespielt."); - return null; - } - - return response.data; - } catch (error: any) { - console.error("Fehler bei der Anfrage:", error.response?.status, error.response?.data); - } -} diff --git a/src/interfaces/CurrentlyPlaying.ts b/src/interfaces/CurrentlyPlaying.ts new file mode 100644 index 0000000..62e0f64 --- /dev/null +++ b/src/interfaces/CurrentlyPlaying.ts @@ -0,0 +1,21 @@ +export interface CurrentlyPlaying { + timestamp: number; + context?: { + type: string; + uri: string; + }; + progress_ms?: number; + item?: { + name: string; + artists: { + name: string; + uri: string; + }[]; + album: { + name: string; + uri: string; + }; + duration_ms: number; + }; + is_playing: boolean; +} \ No newline at end of file diff --git a/src/rest/auth.ts b/src/rest/auth.ts index 25a1dac..fd979a5 100644 --- a/src/rest/auth.ts +++ b/src/rest/auth.ts @@ -1,13 +1,12 @@ import express from "express"; -import {UserService} from "../db/services/db/UserService"; -import {CreateUserPayload, IUser} from "../db/models/user"; +import {CreateUserPayload} from "../db/models/user"; import {JwtAuthenticator} from "../utils/jwtAuthenticator"; import crypto from "crypto"; import {PasswordUtils} from "../utils/passwordUtils"; import {asyncHandler} from "./middleware/asyncHandler"; import {validateBody, v} from "./middleware/validate"; import {ok, badRequest, unauthorized, created, conflict, notFound} from "./utils/responses"; -import {Document} from "mongoose"; +import {UserService} from "../services/db/UserService"; export class RestAuth { private readonly userService: UserService; diff --git a/src/rest/middleware/isAdmin.ts b/src/rest/middleware/isAdmin.ts index 6fa0bcf..47d70bb 100644 --- a/src/rest/middleware/isAdmin.ts +++ b/src/rest/middleware/isAdmin.ts @@ -1,6 +1,6 @@ import type { NextFunction, Request, Response } from "express"; -import type { UserService } from "../../db/services/db/UserService"; import { notFound } from "../utils/responses"; +import {UserService} from "../../services/db/UserService"; export function isAdmin(userService: UserService) { return async (req: Request, res: Response, next: NextFunction) => { diff --git a/src/rest/restUser.ts b/src/rest/restUser.ts index 2b01e0a..20c78d3 100644 --- a/src/rest/restUser.ts +++ b/src/rest/restUser.ts @@ -1,10 +1,10 @@ import express from "express"; -import {UserService} from "../db/services/db/UserService"; import {PasswordUtils} from "../utils/passwordUtils"; import {asyncHandler} from "./middleware/asyncHandler"; import {v, validateBody, validateParams} from "./middleware/validate"; import {badRequest, ok} from "./utils/responses"; import {isAdmin} from "./middleware/isAdmin"; +import {UserService} from "../services/db/UserService"; export class RestUser { diff --git a/src/rest/spotifyTokenGenerator.ts b/src/rest/spotifyTokenGenerator.ts index 259f93b..de334d0 100644 --- a/src/rest/spotifyTokenGenerator.ts +++ b/src/rest/spotifyTokenGenerator.ts @@ -1,8 +1,8 @@ import express from "express"; -import {SpotifyTokenService} from "../db/services/spotifyTokenService"; import {asyncHandler} from "./middleware/asyncHandler"; import {validateBody, v} from "./middleware/validate"; import {ok, internalError} from "./utils/responses"; +import {SpotifyTokenService} from "../services/spotifyTokenService"; export class SpotifyTokenGenerator { diff --git a/src/server.ts b/src/server.ts index 1644503..f5cb752 100644 --- a/src/server.ts +++ b/src/server.ts @@ -12,12 +12,14 @@ import { SpotifyTokenGenerator } from "./rest/spotifyTokenGenerator"; import { RestAuth } from "./rest/auth"; import { authLimiter, spotifyLimiter } from "./rest/middleware/rateLimit"; import { extractTokenFromCookie } from "./rest/middleware/extractTokenFromCookie"; -import { UserService } from "./db/services/db/UserService"; import { JwtAuthenticator } from "./utils/jwtAuthenticator"; import { authenticateJwt } from "./rest/middleware/authenticateJwt"; -import { connectToDatabase, disconnectFromDatabase } from "./db/services/db/database.service"; -import { SpotifyTokenService } from "./db/services/spotifyTokenService"; import {watchUserChanges} from "./db/models/userWatch"; +import {SpotifyPollingService} from "./services/spotifyPollingService"; +import {SpotifyApiService} from "./services/spotifyApiService"; +import {UserService} from "./services/db/UserService"; +import {connectToDatabase, disconnectFromDatabase} from "./services/db/database.service"; +import {SpotifyTokenService} from "./services/spotifyTokenService"; interface ServerConfig { port: number; @@ -47,6 +49,8 @@ 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); this._setupMiddleware(); this._setupRoutes(this.userService, spotifyTokenService); @@ -56,7 +60,7 @@ export class Server { console.log(`Server is running on port ${this.config.port}`); }); - this.webSocketServer = new ExtendedWebSocketServer(this.httpServer, this.userService, spotifyTokenService); + this.webSocketServer = new ExtendedWebSocketServer(this.httpServer, this.userService, spotifyPollingService); this._setupGracefulShutdown(); diff --git a/src/db/services/db/UserService.ts b/src/services/db/UserService.ts similarity index 91% rename from src/db/services/db/UserService.ts rename to src/services/db/UserService.ts index f59f5ef..f126b80 100644 --- a/src/db/services/db/UserService.ts +++ b/src/services/db/UserService.ts @@ -1,6 +1,6 @@ -import {CreateUserPayload, IUser, SpotifyConfig, UserModel} from "../../models/user"; import {connectToDatabase} from "./database.service"; import { UpdateQuery} from "mongoose"; +import {CreateUserPayload, IUser, SpotifyConfig, UserModel} from "../../db/models/user"; export class UserService { private static _instance: UserService; @@ -22,6 +22,14 @@ export class UserService { }).exec(); } + public async updateUserByUUID(uuid: string, updates: Partial): Promise { + return await UserModel.findOneAndUpdate( + { uuid: uuid }, + { $set: updates }, + { new: true } + ).exec(); + } + public async getAllUsers(): Promise { return await UserModel.find({}, {spotifyConfig: 0, lastState: 0}).exec(); } diff --git a/src/db/services/db/database.service.ts b/src/services/db/database.service.ts similarity index 100% rename from src/db/services/db/database.service.ts rename to src/services/db/database.service.ts diff --git a/src/db/services/owmApiService.ts b/src/services/owmApiService.ts similarity index 100% rename from src/db/services/owmApiService.ts rename to src/services/owmApiService.ts diff --git a/src/services/spotifyApiService.ts b/src/services/spotifyApiService.ts new file mode 100644 index 0000000..b756847 --- /dev/null +++ b/src/services/spotifyApiService.ts @@ -0,0 +1,31 @@ +import axios, { AxiosError } from "axios"; +import {CurrentlyPlaying} from "../interfaces/CurrentlyPlaying"; + +export class SpotifyApiService { + private readonly apiUrl = "https://api.spotify.com/v1"; + + public async getCurrentlyPlaying(accessToken: string): Promise { + try { + const response = await axios.get(`${this.apiUrl}/me/player/currently-playing`, { + headers: { + Authorization: `Bearer ${accessToken}` + }, + params: { + additional_types: "episode" + } + }); + + if (response.status === 204) { + return null; + } + + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + console.error("Spotify API Error:", error.response?.status, error.response?.data); + throw error; + } + throw error; + } + } +} \ No newline at end of file diff --git a/src/services/spotifyPollingService.ts b/src/services/spotifyPollingService.ts new file mode 100644 index 0000000..dfe01e2 --- /dev/null +++ b/src/services/spotifyPollingService.ts @@ -0,0 +1,105 @@ +import { appEventBus, SPOTIFY_STATE_UPDATED_EVENT } from "../utils/eventBus"; +import { SpotifyApiService } from "./spotifyApiService"; +import { IUser } from "../db/models/user"; +import { AxiosError } from "axios"; +import {UserService} from "./db/UserService"; +import {SpotifyTokenService} from "./spotifyTokenService"; + +const userStateCache = new Map(); +const activePolls = new Map(); + +export class SpotifyPollingService { + constructor( + private readonly userService: UserService, + private readonly spotifyApiService: SpotifyApiService, + private readonly spotifyTokenService: SpotifyTokenService, + ) {} + + public startPollingForUser(user: IUser): void { + const uuid = user.uuid; + if (activePolls.has(uuid)) return; + + console.log(`[SpotifyPolling] Starting polling for user ${uuid}`); + const intervalId = setInterval(() => this._pollUser(uuid), 3000); // Sicherer 3-Sekunden-Intervall + activePolls.set(uuid, intervalId); + + this._pollUser(uuid); + } + + public stopPollingForUser(uuid: string): void { + if (activePolls.has(uuid)) { + console.log(`[SpotifyPolling] Stopping polling for user ${uuid}`); + clearInterval(activePolls.get(uuid)!); + activePolls.delete(uuid); + userStateCache.delete(uuid); + } + } + + private async _pollUser(uuid: string): Promise { + let user = await this.userService.getUserByUUID(uuid); + if (!user || !user.spotifyConfig) { + this.stopPollingForUser(uuid); + return; + } + + try { + if (Date.now() > user.spotifyConfig.expirationDate.getTime()) { + console.log(`[SpotifyPolling] Token for ${uuid} expired, refreshing...`); + const token = await this.spotifyTokenService.refreshToken(user.spotifyConfig.refreshToken); + const newConfig = { + refreshToken: user.spotifyConfig.refreshToken, + accessToken: token.access_token, + expirationDate: new Date(Date.now() + token.expires_in * 1000), + scope: token.scope, + }; + user = await this.userService.updateUserByUUID(uuid, { spotifyConfig: newConfig }); + + console.log(`[SpotifyPolling] Token for ${uuid} refreshed.`); + } + + const currentState = await this.spotifyApiService.getCurrentlyPlaying(user!.spotifyConfig!.accessToken); + const lastState = userStateCache.get(uuid); + + if (this._hasStateChanged(lastState, currentState)) { + console.log(`[SpotifyPolling] State change for ${uuid}. Emitting event.`); + userStateCache.set(uuid, currentState); + appEventBus.emit(SPOTIFY_STATE_UPDATED_EVENT, { uuid, state: currentState }); + } + + } catch (error) { + if (error instanceof AxiosError && error.response) { + if (error.response.status === 429) { + const retryAfter = Number(error.response.headers['retry-after'] || 5); + console.warn(`[SpotifyPolling] Rate limit for ${uuid}. Pausing for ${retryAfter}s.`); + this._pausePolling(uuid, retryAfter * 1000); + } else if (error.response.status === 401) { + console.error(`[SpotifyPolling] Bad token for ${uuid}. Stopping poll.`); + this.stopPollingForUser(uuid); + } + } else { + console.error(`[SpotifyPolling] Unknown error for ${uuid}:`, error); + } + } + } + + private _hasStateChanged(lastState: any, currentState: any): boolean { + if (!currentState && !lastState) return false; + if (!currentState || !lastState) return true; + + return lastState.item?.id !== currentState.item?.id || + lastState.is_playing !== currentState.is_playing; + } + + private _pausePolling(uuid: string, durationMs: number): void { + if (activePolls.has(uuid)) { + clearInterval(activePolls.get(uuid)!); + activePolls.delete(uuid); + setTimeout(() => { + console.log(`[SpotifyPolling] Resuming polling for ${uuid}.`); + this.userService.getUserByUUID(uuid).then(user => { + if (user) this.startPollingForUser(user); + }); + }, durationMs); + } + } +} \ No newline at end of file diff --git a/src/db/services/spotifyTokenService.ts b/src/services/spotifyTokenService.ts similarity index 95% rename from src/db/services/spotifyTokenService.ts rename to src/services/spotifyTokenService.ts index 8fb5f54..af38e16 100644 --- a/src/db/services/spotifyTokenService.ts +++ b/src/services/spotifyTokenService.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import {OAuthTokenResponse} from "../../interfaces/OAuthTokenResponse"; +import {OAuthTokenResponse} from "../interfaces/OAuthTokenResponse"; const url = "https://accounts.spotify.com/api/token"; diff --git a/src/utils/eventBus.ts b/src/utils/eventBus.ts index b4c354f..aeaa7e0 100644 --- a/src/utils/eventBus.ts +++ b/src/utils/eventBus.ts @@ -1,4 +1,5 @@ import { EventEmitter } from 'events'; export const appEventBus = new EventEmitter(); -export const USER_UPDATED_EVENT = 'user:updated'; \ No newline at end of file +export const USER_UPDATED_EVENT = 'user:updated'; +export const SPOTIFY_STATE_UPDATED_EVENT = 'spotify:updated'; \ No newline at end of file diff --git a/src/utils/websocket/websocketCustomEvents/customWebsocketEventUserService.ts b/src/utils/websocket/websocketCustomEvents/customWebsocketEventUserService.ts deleted file mode 100644 index beb4b54..0000000 --- a/src/utils/websocket/websocketCustomEvents/customWebsocketEventUserService.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; -import {CustomWebsocketEvent} from "./customWebsocketEvent"; -import {UserService} from "../../../db/services/db/UserService"; - -export abstract class CustomWebsocketEventUserService extends CustomWebsocketEvent { - protected readonly userService: UserService; - - public constructor(ws: ExtendedWebSocket, userService: UserService) { - super(ws); - this.userService = userService; - } - -} diff --git a/src/utils/websocket/websocketCustomEvents/getSpotifyUpdatesEvent.ts b/src/utils/websocket/websocketCustomEvents/getSpotifyUpdatesEvent.ts index 660b4a6..31267a4 100644 --- a/src/utils/websocket/websocketCustomEvents/getSpotifyUpdatesEvent.ts +++ b/src/utils/websocket/websocketCustomEvents/getSpotifyUpdatesEvent.ts @@ -1,88 +1,21 @@ -import {CustomWebsocketEvent} from "./customWebsocketEvent"; +import { SpotifyPollingService } from "../../../services/spotifyPollingService"; import {WebsocketEventType} from "./websocketEventType"; -import {SpotifyTokenService} from "../../../db/services/spotifyTokenService"; -import {getCurrentlyPlaying} from "../../../db/services/spotifyApiService"; -import {CustomWebsocketEventUserService} from "./customWebsocketEventUserService"; import {NoData} from "./NoData"; -import {UserService} from "../../../db/services/db/UserService"; import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; +import {CustomWebsocketEvent} from "./customWebsocketEvent"; -export const SpotifyAsyncUpdateEvent = "SPOTIFY_UPDATE"; export class GetSpotifyUpdatesEvent extends CustomWebsocketEvent { + event = WebsocketEventType.GET_SPOTIFY_UPDATE; - event = WebsocketEventType.GET_SPOTIFY_UPDATES; - - handler = async () => { - console.log("Starting Spotify updates"); - this.ws.emit(WebsocketEventType.GET_SINGLE_SPOTIFY_UPDATE, {}); - - if (this.ws.asyncUpdates.has(SpotifyAsyncUpdateEvent)) { - console.log("Spotify updates already running"); - return; - } - - this.ws.asyncUpdates.set(SpotifyAsyncUpdateEvent, setInterval(() => { - this.ws.emit(WebsocketEventType.GET_SINGLE_SPOTIFY_UPDATE, {}); - }, 1000)); - - } -} - -export class GetSingleSpotifyUpdateEvent extends CustomWebsocketEventUserService { - - private readonly spotifyTokenService: SpotifyTokenService; - - event = WebsocketEventType.GET_SINGLE_SPOTIFY_UPDATE; - - constructor(ws: ExtendedWebSocket, userService: UserService, spotifyTokenService: SpotifyTokenService) { - super(ws, userService); - this.spotifyTokenService = spotifyTokenService; + constructor(ws: ExtendedWebSocket, private spotifyPollingService: SpotifyPollingService) { + super(ws); } handler = async () => { - console.log("Getting single Spotify update event"); - await this.spotifyUpdates(); + console.log("Client requests Spotify updates. Starting polling."); + if (this.ws.user) { + this.spotifyPollingService.startPollingForUser(this.ws.user); + } } - - private async spotifyUpdates() { - console.log("Checking Spotify") - const user = this.ws.user; - const spotifyConfig = user.spotifyConfig; - if (!spotifyConfig) { - console.log("No Spotify config found"); - // stop the interval - this.ws.emit(WebsocketEventType.STOP_SPOTIFY_UPDATES, {}); - return; - } - if (Date.now() > spotifyConfig.expirationDate.getTime()) { - console.log("Token expired"); - - const token = await this.spotifyTokenService.refreshToken(spotifyConfig.refreshToken); - const newSpotifyConfig = { - // use old refresh token because you don't get a new one - refreshToken: user.spotifyConfig!.refreshToken, - accessToken: token.access_token, - expirationDate: new Date(Date.now() + token.expires_in * 1000), - scope: token.scope, - }; - await this.userService.updateUserById(user.id, {spotifyConfig: newSpotifyConfig}); - this.ws.user.spotifyConfig = newSpotifyConfig; - console.log("Token refreshed and database updated"); - } - const musicData = await getCurrentlyPlaying(this.ws.user.spotifyConfig!.accessToken); - if (!musicData) { - console.log("No music data found, maybe error from spotify, skipping this update"); - return; - } - console.log("Sending Spotify update"); - console.log(musicData); - - this.ws.send(JSON.stringify({ - type: "SPOTIFY_UPDATE", - payload: musicData, - }), {binary: false}); - } - } - diff --git a/src/utils/websocket/websocketCustomEvents/getWeatherUpdatesEvent.ts b/src/utils/websocket/websocketCustomEvents/getWeatherUpdatesEvent.ts index c909e23..59a601b 100644 --- a/src/utils/websocket/websocketCustomEvents/getWeatherUpdatesEvent.ts +++ b/src/utils/websocket/websocketCustomEvents/getWeatherUpdatesEvent.ts @@ -1,7 +1,7 @@ import {CustomWebsocketEvent} from "./customWebsocketEvent"; import {WebsocketEventType} from "./websocketEventType"; -import {getCurrentWeather} from "../../../db/services/owmApiService"; import {NoData} from "./NoData"; +import {getCurrentWeather} from "../../../services/owmApiService"; export const WeatherAsyncUpdateEvent = "WEATHER_UPDATE"; diff --git a/src/utils/websocket/websocketCustomEvents/stopSpotifyUpdatesEvent.ts b/src/utils/websocket/websocketCustomEvents/stopSpotifyUpdatesEvent.ts index 1907136..d9d1292 100644 --- a/src/utils/websocket/websocketCustomEvents/stopSpotifyUpdatesEvent.ts +++ b/src/utils/websocket/websocketCustomEvents/stopSpotifyUpdatesEvent.ts @@ -1,19 +1,30 @@ import {CustomWebsocketEvent} from "./customWebsocketEvent"; import {WebsocketEventType} from "./websocketEventType"; -import {SpotifyAsyncUpdateEvent} from "./getSpotifyUpdatesEvent"; import {NoData} from "./NoData"; +import {SpotifyPollingService} from "../../../services/spotifyPollingService"; +import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; export class StopSpotifyUpdatesEvent extends CustomWebsocketEvent { event = WebsocketEventType.STOP_SPOTIFY_UPDATES; + private readonly spotifyPollingService: SpotifyPollingService; + + constructor(ws: ExtendedWebSocket, spotifyPollingService: SpotifyPollingService) { + super(ws); + this.spotifyPollingService = spotifyPollingService; + } + handler = async () => { - if (this.ws.asyncUpdates.has(SpotifyAsyncUpdateEvent)) { - clearInterval(this.ws.asyncUpdates.get(SpotifyAsyncUpdateEvent)); - this.ws.asyncUpdates.delete(SpotifyAsyncUpdateEvent); - console.log("Spotify updates stopped"); + console.log("Client requests to stop Spotify updates. Stopping polling."); + + const uuid = this.ws.payload?.uuid; + + if (uuid) { + this.spotifyPollingService.stopPollingForUser(uuid); + } else { + console.warn("Could not stop Spotify polling: No UUID found on WebSocket payload."); } } - } diff --git a/src/utils/websocket/websocketCustomEvents/websocketEventType.ts b/src/utils/websocket/websocketCustomEvents/websocketEventType.ts index 4ab68a7..115f350 100644 --- a/src/utils/websocket/websocketCustomEvents/websocketEventType.ts +++ b/src/utils/websocket/websocketCustomEvents/websocketEventType.ts @@ -1,15 +1,11 @@ export enum WebsocketEventType { GET_SETTINGS = "GET_SETTINGS", GET_STATE = "GET_STATE", - GET_SINGLE_SPOTIFY_UPDATE = "GET_SINGLE_SPOTIFY_UPDATE", - GET_SPOTIFY_UPDATES = "GET_SPOTIFY_UPDATES", + GET_SPOTIFY_UPDATE = "GET_SPOTIFY_UPDATES", GET_SINGLE_WEATHER_UPDATE = "GET_SINGLE_WEATHER_UPDATE", GET_WEATHER_UPDATES = "GET_WEATHER_UPDATES", STOP_SPOTIFY_UPDATES = "STOP_SPOTIFY_UPDATES", STOP_WEATHER_UPDATES = "STOP_WEATHER_UPDATES", ERROR = "ERROR", - UPDATE_USER = "UPDATE_USER", UPDATE_USER_SINGLE = "UPDATE_USER_SINGLE", - STOP_UPDATE_USER = "STOP_UPDATE_USER", - } diff --git a/src/utils/websocket/websocketCustomEvents/websocketEventUtils.ts b/src/utils/websocket/websocketCustomEvents/websocketEventUtils.ts index f106703..b9acf4c 100644 --- a/src/utils/websocket/websocketCustomEvents/websocketEventUtils.ts +++ b/src/utils/websocket/websocketCustomEvents/websocketEventUtils.ts @@ -1,23 +1,21 @@ import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket"; import {GetSettingsEvent} from "./getSettingsEvent"; import {ErrorEvent} from "./errorEvent"; -import {GetSingleSpotifyUpdateEvent, GetSpotifyUpdatesEvent} from "./getSpotifyUpdatesEvent"; +import {GetSpotifyUpdatesEvent} from "./getSpotifyUpdatesEvent"; import {GetStateEvent} from "./getStateEvent"; import {GetSingleWeatherUpdateEvent, GetWeatherUpdatesEvent} from "./getWeatherUpdatesEvent"; import {StopSpotifyUpdatesEvent} from "./stopSpotifyUpdatesEvent"; import {StopWeatherUpdatesEvent} from "./stopWeatherUpdatesEvent"; import { UpdateUserSingleEvent} from "./updateUserEvent"; import {CustomWebsocketEvent} from "./customWebsocketEvent"; -import {UserService} from "../../../db/services/db/UserService"; -import {SpotifyTokenService} from "../../../db/services/spotifyTokenService"; +import {SpotifyPollingService} from "../../../services/spotifyPollingService"; -export function getEventListeners(ws: ExtendedWebSocket, userService: UserService, spotifyTokenService: SpotifyTokenService): CustomWebsocketEvent[] { +export function getEventListeners(ws: ExtendedWebSocket, spotifyPollingService: SpotifyPollingService): CustomWebsocketEvent[] { return [ new GetStateEvent(ws), new GetSettingsEvent(ws), - new GetSingleSpotifyUpdateEvent(ws, userService, spotifyTokenService), - new GetSpotifyUpdatesEvent(ws), - new StopSpotifyUpdatesEvent(ws), + new GetSpotifyUpdatesEvent(ws, spotifyPollingService), + new StopSpotifyUpdatesEvent(ws, spotifyPollingService), new GetSingleWeatherUpdateEvent(ws), new GetWeatherUpdatesEvent(ws), new StopWeatherUpdatesEvent(ws), diff --git a/src/utils/websocket/websocketEventHandler.ts b/src/utils/websocket/websocketEventHandler.ts index 7c3a593..8052e91 100644 --- a/src/utils/websocket/websocketEventHandler.ts +++ b/src/utils/websocket/websocketEventHandler.ts @@ -1,11 +1,10 @@ import {ExtendedWebSocket} from "../../interfaces/extendedWebsocket"; import {CustomWebsocketEvent} from "./websocketCustomEvents/customWebsocketEvent"; -import {UserService} from "../../db/services/db/UserService"; import {getEventListeners} from "./websocketCustomEvents/websocketEventUtils"; -import {SpotifyTokenService} from "../../db/services/spotifyTokenService"; +import {SpotifyPollingService} from "../../services/spotifyPollingService"; export class WebsocketEventHandler { - constructor(private webSocket: ExtendedWebSocket, private userService: UserService, private spotifyTokenService: SpotifyTokenService) { + constructor(private webSocket: ExtendedWebSocket, private spotifyPollingService: SpotifyPollingService) { } public enableErrorEvent() { @@ -46,7 +45,7 @@ export class WebsocketEventHandler { } public registerCustomEvents() { - const events = getEventListeners(this.webSocket, this.userService, this.spotifyTokenService); + const events = getEventListeners(this.webSocket, this.spotifyPollingService); events.forEach(this.registerCustomEvent, this); } diff --git a/src/utils/websocket/websocketServerEventHandler.ts b/src/utils/websocket/websocketServerEventHandler.ts index ac01cfd..2bd5643 100644 --- a/src/utils/websocket/websocketServerEventHandler.ts +++ b/src/utils/websocket/websocketServerEventHandler.ts @@ -2,7 +2,7 @@ import {ExtendedWebSocket} from "../../interfaces/extendedWebsocket"; import {ExtendedIncomingMessage} from "../../interfaces/extendedIncomingMessage"; import {Server as WebSocketServer} from "ws"; import {heartbeat} from "./websocketServerHeartbeatInterval"; -import {UserService} from "../../db/services/db/UserService"; +import {UserService} from "../../services/db/UserService"; export class WebsocketServerEventHandler { private readonly heartbeat: () => void; diff --git a/src/websocket.ts b/src/websocket.ts index 611e1d3..839de36 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -1,23 +1,23 @@ -import { Server } from "http"; -import { Server as WebSocketServer, WebSocket } from "ws"; -import { verifyClient } from "./utils/verifyClient"; -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 { UserService } from "./db/services/db/UserService"; -import { SpotifyTokenService } from "./db/services/spotifyTokenService"; -import { appEventBus, USER_UPDATED_EVENT } from "./utils/eventBus"; -import { IUser } from "./db/models/user"; +import {Server} from "http"; +import {Server as WebSocketServer, WebSocket} from "ws"; +import {verifyClient} from "./utils/verifyClient"; +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 {IUser} from "./db/models/user"; +import {SpotifyPollingService} from "./services/spotifyPollingService"; +import {UserService} from "./services/db/UserService"; export class ExtendedWebSocketServer { private readonly _wss: WebSocketServer; private readonly userService: UserService; - private readonly spotifyTokenService: SpotifyTokenService; + private readonly spotifyPollingService: SpotifyPollingService; - constructor(server: Server, userService: UserService, spotifyTokenService: SpotifyTokenService) { + constructor(server: Server, userService: UserService, spotifyPollingService: SpotifyPollingService) { this.userService = userService; - this.spotifyTokenService = spotifyTokenService; + this.spotifyPollingService = spotifyPollingService; this._wss = new WebSocketServer({ server, @@ -31,7 +31,7 @@ export class ExtendedWebSocketServer { public broadcast(message: string): void { this.getConnectedClients().forEach((client) => { if (client.readyState === WebSocket.OPEN) { - client.send(message, { binary: false }); + client.send(message, {binary: false}); } }); } @@ -39,7 +39,7 @@ export class ExtendedWebSocketServer { public sendMessageToUser(uuid: string, message: string): void { const client = this._findClientByUUID(uuid); if (client && client.readyState === WebSocket.OPEN) { - client.send(message, { binary: false }); + client.send(message, {binary: false}); } } @@ -63,7 +63,7 @@ export class ExtendedWebSocketServer { private _onNewClientReady(ws: ExtendedWebSocket): void { console.log("WebSocket client connected and authenticated"); - const socketEventHandler = new WebsocketEventHandler(ws, this.userService, this.spotifyTokenService); + const socketEventHandler = new WebsocketEventHandler(ws, this.spotifyPollingService); socketEventHandler.enableErrorEvent(); socketEventHandler.enablePongEvent(); @@ -76,14 +76,6 @@ export class ExtendedWebSocketServer { // send initial state and settings ws.emit(WebsocketEventType.GET_SETTINGS, {}); ws.emit(WebsocketEventType.GET_STATE, {}); - - const mode = ws.user.lastState?.global.mode; - if (mode === "clock") { - ws.emit(WebsocketEventType.GET_WEATHER_UPDATES, {}); - } - if (mode === "music") { - ws.emit(WebsocketEventType.GET_SPOTIFY_UPDATES, {}); - } } private _listenForAppEvents(): void { @@ -95,6 +87,18 @@ export class ExtendedWebSocketServer { client.emit(WebsocketEventType.UPDATE_USER_SINGLE, user); } }); + + appEventBus.on(SPOTIFY_STATE_UPDATED_EVENT, ({uuid, state}) => { + const client = this._findClientByUUID(uuid); + console.log(`Received update for user ${uuid}`); + if (client) { + client.send(JSON.stringify({ + type: "SPOTIFY_UPDATE", + payload: state, + }), {binary: false}); + } + }); + } private _findClientByUUID(uuid: string): ExtendedWebSocket | undefined { diff --git a/tests/db/services/spotifyApiService.test.ts b/tests/db/services/spotifyApiService.test.ts deleted file mode 100644 index ef99b6e..0000000 --- a/tests/db/services/spotifyApiService.test.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import axios from "axios"; -import { getCurrentlyPlaying, CurrentlyPlaying } from "../../../src/db/services/spotifyApiService"; - -vi.mock("axios", () => ({ - default: { - get: vi.fn(), - }, -})); - -const mockedAxios = vi.mocked(axios, true); - -describe("spotifyApiService", () => { - let consoleSpy: any; - let consoleErrorSpy: any; - - beforeEach(() => { - vi.clearAllMocks(); - consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - }); - - describe("getCurrentlyPlaying", () => { - it("should return currently playing track data", async () => { - const accessToken = "test-access-token"; - const mockCurrentlyPlaying: CurrentlyPlaying = { - timestamp: 1640995200000, - context: { - type: "playlist", - uri: "spotify:playlist:37i9dQZF1DXcBWIGoYBM5M", - }, - progress_ms: 45000, - item: { - name: "Test Song", - artists: [ - { - name: "Test Artist", - uri: "spotify:artist:test123", - }, - ], - album: { - name: "Test Album", - uri: "spotify:album:test456", - }, - duration_ms: 180000, - }, - is_playing: true, - }; - - mockedAxios.get.mockResolvedValue({ - status: 200, - data: mockCurrentlyPlaying, - }); - - const result = await getCurrentlyPlaying(accessToken); - - expect(mockedAxios.get).toHaveBeenCalledWith( - "https://api.spotify.com/v1/me/player/currently-playing", - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - params: { - additional_types: "episode", - }, - } - ); - - expect(result).toEqual(mockCurrentlyPlaying); - }); - - it("should return null when nothing is playing (204 status)", async () => { - const accessToken = "test-access-token"; - - mockedAxios.get.mockResolvedValue({ - status: 204, - data: null, - }); - - const result = await getCurrentlyPlaying(accessToken); - - expect(result).toBeNull(); - expect(consoleSpy).toHaveBeenCalledWith("Es wird gerade nichts abgespielt."); - }); - - it("should handle track with minimal data", async () => { - const accessToken = "test-access-token"; - const mockMinimalTrack: CurrentlyPlaying = { - timestamp: 1640995200000, - is_playing: false, - }; - - mockedAxios.get.mockResolvedValue({ - status: 200, - data: mockMinimalTrack, - }); - - const result = await getCurrentlyPlaying(accessToken); - - expect(result).toEqual(mockMinimalTrack); - expect(result?.item).toBeUndefined(); - expect(result?.progress_ms).toBeUndefined(); - }); - - it("should handle track with multiple artists", async () => { - const accessToken = "test-access-token"; - const mockTrackWithMultipleArtists: CurrentlyPlaying = { - timestamp: 1640995200000, - item: { - name: "Collaboration Song", - artists: [ - { - name: "Artist One", - uri: "spotify:artist:artist1", - }, - { - name: "Artist Two", - uri: "spotify:artist:artist2", - }, - { - name: "Artist Three", - uri: "spotify:artist:artist3", - }, - ], - album: { - name: "Collaboration Album", - uri: "spotify:album:collab123", - }, - duration_ms: 240000, - }, - is_playing: true, - }; - - mockedAxios.get.mockResolvedValue({ - status: 200, - data: mockTrackWithMultipleArtists, - }); - - const result = await getCurrentlyPlaying(accessToken); - - expect(result?.item?.artists).toHaveLength(3); - expect(result?.item?.artists[0].name).toBe("Artist One"); - expect(result?.item?.artists[2].name).toBe("Artist Three"); - }); - - it("should handle episodes (podcasts)", async () => { - const accessToken = "test-access-token"; - const mockEpisode: CurrentlyPlaying = { - timestamp: 1640995200000, - context: { - type: "show", - uri: "spotify:show:test123", - }, - progress_ms: 600000, - item: { - name: "Test Podcast Episode", - artists: [ - { - name: "Podcast Host", - uri: "spotify:artist:host123", - }, - ], - album: { - name: "Test Podcast Show", - uri: "spotify:show:test123", - }, - duration_ms: 3600000, - }, - is_playing: true, - }; - - mockedAxios.get.mockResolvedValue({ - status: 200, - data: mockEpisode, - }); - - const result = await getCurrentlyPlaying(accessToken); - - expect(result).toEqual(mockEpisode); - expect(result?.context?.type).toBe("show"); - }); - - it("should handle 401 unauthorized error", async () => { - const accessToken = "invalid-token"; - const unauthorizedError = { - response: { - status: 401, - data: { - error: { - status: 401, - message: "Invalid access token", - }, - }, - }, - }; - - mockedAxios.get.mockRejectedValue(unauthorizedError); - - const result = await getCurrentlyPlaying(accessToken); - - expect(result).toBeUndefined(); - expect(consoleErrorSpy).toHaveBeenCalledWith( - "Fehler bei der Anfrage:", - 401, - unauthorizedError.response.data - ); - }); - - it("should handle 403 forbidden error (premium required)", async () => { - const accessToken = "valid-but-non-premium-token"; - const forbiddenError = { - response: { - status: 403, - data: { - error: { - status: 403, - message: "Player command failed: Premium required", - }, - }, - }, - }; - - mockedAxios.get.mockRejectedValue(forbiddenError); - - const result = await getCurrentlyPlaying(accessToken); - - expect(result).toBeUndefined(); - expect(consoleErrorSpy).toHaveBeenCalledWith( - "Fehler bei der Anfrage:", - 403, - forbiddenError.response.data - ); - }); - - it("should handle 429 rate limit error", async () => { - const accessToken = "test-access-token"; - const rateLimitError = { - response: { - status: 429, - data: { - error: { - status: 429, - message: "API rate limit exceeded", - }, - }, - headers: { - "retry-after": "30", - }, - }, - }; - - mockedAxios.get.mockRejectedValue(rateLimitError); - - const result = await getCurrentlyPlaying(accessToken); - - expect(result).toBeUndefined(); - expect(consoleErrorSpy).toHaveBeenCalledWith( - "Fehler bei der Anfrage:", - 429, - rateLimitError.response.data - ); - }); - - it("should handle network errors", async () => { - const accessToken = "test-access-token"; - const networkError = { - code: "ECONNREFUSED", - message: "Network Error", - }; - - mockedAxios.get.mockRejectedValue(networkError); - - const result = await getCurrentlyPlaying(accessToken); - - expect(result).toBeUndefined(); - expect(consoleErrorSpy).toHaveBeenCalledWith( - "Fehler bei der Anfrage:", - undefined, - undefined - ); - }); - - it("should include additional_types parameter for episodes", async () => { - const accessToken = "test-access-token"; - - mockedAxios.get.mockResolvedValue({ - status: 204, - data: null, - }); - - await getCurrentlyPlaying(accessToken); - - const [, config] = mockedAxios.get.mock.calls[0]; - expect(config?.params?.additional_types).toBe("episode"); - }); - - it("should use correct Spotify API endpoint", async () => { - const accessToken = "test-access-token"; - - mockedAxios.get.mockResolvedValue({ - status: 204, - data: null, - }); - - await getCurrentlyPlaying(accessToken); - - const [url] = mockedAxios.get.mock.calls[0]; - expect(url).toBe("https://api.spotify.com/v1/me/player/currently-playing"); - }); - - it("should format authorization header correctly", async () => { - const accessToken = "test-access-token-123"; - - mockedAxios.get.mockResolvedValue({ - status: 204, - data: null, - }); - - await getCurrentlyPlaying(accessToken); - - const [, config] = mockedAxios.get.mock.calls[0]; - expect(config?.headers?.Authorization).toBe(`Bearer ${accessToken}`); - }); - - it("should handle empty access token", async () => { - const accessToken = ""; - - mockedAxios.get.mockResolvedValue({ - status: 204, - data: null, - }); - - await getCurrentlyPlaying(accessToken); - - const [, config] = mockedAxios.get.mock.calls[0]; - expect(config?.headers?.Authorization).toBe("Bearer "); - }); - }); -}); \ No newline at end of file diff --git a/tests/helpers/testSetup.ts b/tests/helpers/testSetup.ts index 21e6f1a..e244405 100644 --- a/tests/helpers/testSetup.ts +++ b/tests/helpers/testSetup.ts @@ -1,6 +1,6 @@ import express, { Router } from "express"; import { vi, type Mocked } from "vitest"; -import { UserService } from "../../src/db/services/db/UserService"; +import { UserService } from "../../src/services/db/UserService"; import { PasswordUtils } from "../../src/utils/passwordUtils"; export const defaultMockPayload = { @@ -56,6 +56,7 @@ export const createMockUserService = () => ({ existsUserByName: vi.fn(), createUser: vi.fn(), getUserAuthByName: vi.fn(), + updateUserByUUID: vi.fn(), }); /** @@ -114,4 +115,8 @@ export const createMockJwtAuthenticator = () => ({ export const createMockSpotifyTokenService = () => ({ refreshToken: vi.fn(), generateToken: vi.fn(), -}); \ No newline at end of file +}); + +export const createMockSpotifyApiService = () => ({ + getCurrentlyPlaying: vi.fn(), +}) \ No newline at end of file diff --git a/tests/rest/restUser.test.ts b/tests/rest/restUser.test.ts index 8a555e2..5f2dc19 100644 --- a/tests/rest/restUser.test.ts +++ b/tests/rest/restUser.test.ts @@ -4,7 +4,7 @@ import request from "supertest"; import {RestUser} from "../../src/rest/restUser"; import {createMockUserService, setupTestEnvironment, type TestEnvironment} from "../helpers/testSetup"; -vi.mock("../../src/db/services/db/UserService", () => ({ +vi.mock("../../src/services/db/UserService", () => ({ UserService: { create: vi.fn() } diff --git a/tests/rest/spotifyTokenGenerator.test.ts b/tests/rest/spotifyTokenGenerator.test.ts index e903528..96102fd 100644 --- a/tests/rest/spotifyTokenGenerator.test.ts +++ b/tests/rest/spotifyTokenGenerator.test.ts @@ -3,7 +3,6 @@ import request from "supertest"; import express from "express"; import { SpotifyTokenGenerator } from "../../src/rest/spotifyTokenGenerator"; -import { SpotifyTokenService } from "../../src/db/services/spotifyTokenService"; import { createTestApp, createMockSpotifyTokenService } from "../helpers/testSetup"; vi.mock("../../src/db/services/spotifyTokenService"); diff --git a/tests/server.test.ts b/tests/server.test.ts index c350e5d..bfd5c08 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -5,14 +5,14 @@ import { Router, type Request, type Response, type NextFunction } from "express" import type { Express } from "express"; import { authLimiter } from "../src/rest/middleware/rateLimit"; -vi.mock("../src/db/services/db/database.service", () => ({ +vi.mock("../src/services/db/database.service", () => ({ connectToDatabase: vi.fn().mockResolvedValue(undefined), disconnectFromDatabase: vi.fn().mockResolvedValue(undefined), })); -vi.mock("../src/db/services/db/UserService", () => ({ +vi.mock("../src/services/db/UserService", () => ({ UserService: { create: vi.fn().mockResolvedValue({}) }, })); -vi.mock("../src/db/services/spotifyTokenService", () => ({ SpotifyTokenService: vi.fn() })); +vi.mock("../src/services/spotifyTokenService", () => ({ SpotifyTokenService: vi.fn() })); vi.mock("../src/websocket", () => ({ ExtendedWebSocketServer: vi.fn().mockImplementation(() => { diff --git a/tests/db/services/db/UserService.test.ts b/tests/services/db/UserService.test.ts similarity index 96% rename from tests/db/services/db/UserService.test.ts rename to tests/services/db/UserService.test.ts index c6328f8..d232fc9 100644 --- a/tests/db/services/db/UserService.test.ts +++ b/tests/services/db/UserService.test.ts @@ -1,9 +1,9 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { UserService } from "../../../../src/db/services/db/UserService"; -import {UserModel} from "../../../../src/db/models/user"; -import { connectToDatabase } from "../../../../src/db/services/db/database.service"; +import {UserModel} from "../../../src/db/models/user"; +import {UserService} from "../../../src/services/db/UserService"; +import {connectToDatabase} from "../../../src/services/db/database.service"; -vi.mock("../../../../src/db/services/db/database.service", () => ({ +vi.mock("../../../../src/services/db/database.service", () => ({ connectToDatabase: vi.fn(), })); diff --git a/tests/db/services/db/database.service.test.ts b/tests/services/db/database.service.test.ts similarity index 98% rename from tests/db/services/db/database.service.test.ts rename to tests/services/db/database.service.test.ts index 86a2214..1143f35 100644 --- a/tests/db/services/db/database.service.test.ts +++ b/tests/services/db/database.service.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import mongoose from "mongoose"; -const MODULE_PATH = "../../../../src/db/services/db/database.service"; +const MODULE_PATH = "../../../../src/services/db/database.service"; type SpyInstance any> = ReturnType[0]>>; diff --git a/tests/db/services/owmApiService.test.ts b/tests/services/owmApiService.test.ts similarity index 99% rename from tests/db/services/owmApiService.test.ts rename to tests/services/owmApiService.test.ts index 4b6d67a..08a123b 100644 --- a/tests/db/services/owmApiService.test.ts +++ b/tests/services/owmApiService.test.ts @@ -1,6 +1,6 @@ import {describe, it, expect, vi, beforeEach} from "vitest"; import OpenWeatherAPI from "openweather-api-node"; -import {getCurrentWeather} from "../../../src/db/services/owmApiService"; +import {getCurrentWeather} from "../../src/services/owmApiService"; vi.mock("openweather-api-node", () => { return { diff --git a/tests/services/spotifyApiService.test.ts b/tests/services/spotifyApiService.test.ts new file mode 100644 index 0000000..af08484 --- /dev/null +++ b/tests/services/spotifyApiService.test.ts @@ -0,0 +1,334 @@ +import {describe, it, expect, vi, beforeEach} from "vitest"; +import axios from "axios"; +import {CurrentlyPlaying} from "../../src/interfaces/CurrentlyPlaying"; +import {SpotifyApiService} from "../../src/services/spotifyApiService"; + +vi.mock("axios", () => ({ + default: { + get: vi.fn(), + isAxiosError: vi.fn(), + }, +})); + +const mockedAxios = vi.mocked(axios, true); + +describe("spotifyApiService", () => { + let consoleErrorSpy: any; + + let spotifyApiService: SpotifyApiService; + + beforeEach(() => { + vi.clearAllMocks(); + consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { + }); + spotifyApiService = new SpotifyApiService(); + }); + + describe("spotifyApiService.getCurrentlyPlaying", () => { + it("should return currently playing track data", async () => { + const accessToken = "test-access-token"; + const mockCurrentlyPlaying: CurrentlyPlaying = { + timestamp: 1640995200000, + context: { + type: "playlist", + uri: "spotify:playlist:37i9dQZF1DXcBWIGoYBM5M", + }, + progress_ms: 45000, + item: { + name: "Test Song", + artists: [ + { + name: "Test Artist", + uri: "spotify:artist:test123", + }, + ], + album: { + name: "Test Album", + uri: "spotify:album:test456", + }, + duration_ms: 180000, + }, + is_playing: true, + }; + + mockedAxios.get.mockResolvedValue({ + status: 200, + data: mockCurrentlyPlaying, + }); + + const result = await spotifyApiService.getCurrentlyPlaying(accessToken); + + expect(mockedAxios.get).toHaveBeenCalledWith( + "https://api.spotify.com/v1/me/player/currently-playing", + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + params: { + additional_types: "episode", + }, + } + ); + + expect(result).toEqual(mockCurrentlyPlaying); + }); + + it("should return null when nothing is playing (204 status)", async () => { + const accessToken = "test-access-token"; + + mockedAxios.get.mockResolvedValue({ + status: 204, + data: null, + }); + + const result = await spotifyApiService.getCurrentlyPlaying(accessToken); + + expect(result).toBeNull(); + }); + + it("should handle track with minimal data", async () => { + const accessToken = "test-access-token"; + const mockMinimalTrack: CurrentlyPlaying = { + timestamp: 1640995200000, + is_playing: false, + }; + + mockedAxios.get.mockResolvedValue({ + status: 200, + data: mockMinimalTrack, + }); + + const result = await spotifyApiService.getCurrentlyPlaying(accessToken); + + expect(result).toEqual(mockMinimalTrack); + expect(result?.item).toBeUndefined(); + expect(result?.progress_ms).toBeUndefined(); + }); + + it("should handle track with multiple artists", async () => { + const accessToken = "test-access-token"; + const mockTrackWithMultipleArtists: CurrentlyPlaying = { + timestamp: 1640995200000, + item: { + name: "Collaboration Song", + artists: [ + { + name: "Artist One", + uri: "spotify:artist:artist1", + }, + { + name: "Artist Two", + uri: "spotify:artist:artist2", + }, + { + name: "Artist Three", + uri: "spotify:artist:artist3", + }, + ], + album: { + name: "Collaboration Album", + uri: "spotify:album:collab123", + }, + duration_ms: 240000, + }, + is_playing: true, + }; + + mockedAxios.get.mockResolvedValue({ + status: 200, + data: mockTrackWithMultipleArtists, + }); + + const result = await spotifyApiService.getCurrentlyPlaying(accessToken); + + expect(result?.item?.artists).toHaveLength(3); + expect(result?.item?.artists[0].name).toBe("Artist One"); + expect(result?.item?.artists[2].name).toBe("Artist Three"); + }); + + it("should handle episodes (podcasts)", async () => { + const accessToken = "test-access-token"; + const mockEpisode: CurrentlyPlaying = { + timestamp: 1640995200000, + context: { + type: "show", + uri: "spotify:show:test123", + }, + progress_ms: 600000, + item: { + name: "Test Podcast Episode", + artists: [ + { + name: "Podcast Host", + uri: "spotify:artist:host123", + }, + ], + album: { + name: "Test Podcast Show", + uri: "spotify:show:test123", + }, + duration_ms: 3600000, + }, + is_playing: true, + }; + + mockedAxios.get.mockResolvedValue({ + status: 200, + data: mockEpisode, + }); + + const result = await spotifyApiService.getCurrentlyPlaying(accessToken); + + expect(result).toEqual(mockEpisode); + expect(result?.context?.type).toBe("show"); + }); + + it("should handle 401 unauthorized error", async () => { + const accessToken = "invalid-token"; + const errorData = { error: { status: 401, message: "Invalid access token" } }; + const unauthorizedError = new Error("Request failed with status code 401"); + Object.assign(unauthorizedError, { + isAxiosError: true, + response: { + status: 401, + data: errorData, + } + }); + + mockedAxios.get.mockRejectedValue(unauthorizedError); + mockedAxios.isAxiosError.mockReturnValue(true); + + + await expect(spotifyApiService.getCurrentlyPlaying(accessToken)) + .rejects.toThrow(unauthorizedError); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Spotify API Error:", + 401, + errorData + ); + }); + + it("should handle 403 forbidden error (premium required)", async () => { + const accessToken = "valid-but-non-premium-token"; + const errorData = { error: { status: 403, message: "Player command failed: Premium required" } }; + const forbiddenError = new Error("Request failed with status code 403"); + Object.assign(forbiddenError, { + isAxiosError: true, + response: { + status: 403, + data: errorData, + } + }); + + mockedAxios.get.mockRejectedValue(forbiddenError); + mockedAxios.isAxiosError.mockReturnValue(true) + + await expect(spotifyApiService.getCurrentlyPlaying(accessToken)) + .rejects.toThrow(forbiddenError); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Spotify API Error:", + 403, + errorData + ); + }); + + it("should handle 429 rate limit error", async () => { + const accessToken = "test-access-token"; + const errorData = { error: { status: 429, message: "API rate limit exceeded" } }; + const rateLimitError = new Error("Request failed with status code 429"); + Object.assign(rateLimitError, { + isAxiosError: true, + response: { + status: 429, + data: errorData, + headers: { + "retry-after": "30", + }, + } + }); + + mockedAxios.get.mockRejectedValue(rateLimitError); + mockedAxios.isAxiosError.mockReturnValue(true) + + await expect(spotifyApiService.getCurrentlyPlaying(accessToken)) + .rejects.toThrow(rateLimitError); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Spotify API Error:", + 429, + errorData + ); + }); + + it("should throw and NOT log a specific message for generic network errors", async () => { + const accessToken = "test-access-token"; + const networkError = new Error("Network Error"); + + mockedAxios.isAxiosError.mockReturnValue(false); + mockedAxios.get.mockRejectedValue(networkError); + + await expect(spotifyApiService.getCurrentlyPlaying(accessToken)) + .rejects.toThrow("Network Error"); + + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + it("should include additional_types parameter for episodes", async () => { + const accessToken = "test-access-token"; + + mockedAxios.get.mockResolvedValue({ + status: 204, + data: null, + }); + + await spotifyApiService.getCurrentlyPlaying(accessToken); + + const [, config] = mockedAxios.get.mock.calls[0]; + expect(config?.params?.additional_types).toBe("episode"); + }); + + it("should use correct Spotify API endpoint", async () => { + const accessToken = "test-access-token"; + + mockedAxios.get.mockResolvedValue({ + status: 204, + data: null, + }); + + await spotifyApiService.getCurrentlyPlaying(accessToken); + + const [url] = mockedAxios.get.mock.calls[0]; + expect(url).toBe("https://api.spotify.com/v1/me/player/currently-playing"); + }); + + it("should format authorization header correctly", async () => { + const accessToken = "test-access-token-123"; + + mockedAxios.get.mockResolvedValue({ + status: 204, + data: null, + }); + + await spotifyApiService.getCurrentlyPlaying(accessToken); + + const [, config] = mockedAxios.get.mock.calls[0]; + expect(config?.headers?.Authorization).toBe(`Bearer ${accessToken}`); + }); + + it("should handle empty access token", async () => { + const accessToken = ""; + + mockedAxios.get.mockResolvedValue({ + status: 204, + data: null, + }); + + await spotifyApiService.getCurrentlyPlaying(accessToken); + + const [, config] = mockedAxios.get.mock.calls[0]; + expect(config?.headers?.Authorization).toBe("Bearer "); + }); + }); +}); \ No newline at end of file diff --git a/tests/services/spotifyPollingService.test.ts b/tests/services/spotifyPollingService.test.ts new file mode 100644 index 0000000..e7e7ce4 --- /dev/null +++ b/tests/services/spotifyPollingService.test.ts @@ -0,0 +1,195 @@ +import {describe, it, expect, vi, beforeEach, afterEach, Mocked} from "vitest"; +import { AxiosError } from "axios"; +import { UserService } from "../../src/services/db/UserService"; +import { SpotifyApiService } from "../../src/services/spotifyApiService"; +import { SpotifyTokenService } from "../../src/services/spotifyTokenService"; +import { appEventBus, SPOTIFY_STATE_UPDATED_EVENT } from "../../src/utils/eventBus"; +import { SpotifyPollingService } from "../../src/services/spotifyPollingService"; +import { IUser } from "../../src/db/models/user"; +import { createMockSpotifyApiService, createMockSpotifyTokenService, createMockUserService } from "../helpers/testSetup"; + +vi.mock("../../src/services/db/UserService"); +vi.mock("../../src/services/spotifyApiService"); +vi.mock("../../src/services/spotifyTokenService"); +vi.mock("../../src/utils/eventBus", () => ({ + appEventBus: { emit: vi.fn() }, + SPOTIFY_STATE_UPDATED_EVENT: 'spotify:state-updated', +})); + +describe("SpotifyPollingService", () => { + let mockedUserService: Mocked; + let mockedApiService: Mocked; + let mockedTokenService: Mocked; + let mockedAppEventBus: Mocked; + + let pollingService: SpotifyPollingService; + + const mockUser: IUser = { + uuid: "user-123", + spotifyConfig: { + accessToken: "valid-access-token", + refreshToken: "valid-refresh-token", + expirationDate: new Date(Date.now() + 3600 * 1000), + }, + } as any; + + beforeEach(async () => { + vi.resetModules(); + vi.clearAllMocks(); + vi.useFakeTimers(); + + // Recreate mocks + mockedUserService = createMockUserService(); + mockedApiService = createMockSpotifyApiService() as any; + mockedTokenService = createMockSpotifyTokenService() as any; + mockedAppEventBus = appEventBus as Mocked; + + const { SpotifyPollingService: FreshSpotifyPollingService } = await import('../../src/services/spotifyPollingService'); + + pollingService = new FreshSpotifyPollingService( + mockedUserService, + mockedApiService, + mockedTokenService + ); + + mockedUserService.getUserByUUID.mockResolvedValue(mockUser); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe("startPollingForUser", () => { + it("should immediately poll and then periodically every 3 seconds", async () => { + mockedApiService.getCurrentlyPlaying.mockResolvedValue({ item: { id: "song-a" }, is_playing: true } as any); + + pollingService.startPollingForUser(mockUser); + + await vi.advanceTimersByTimeAsync(0); + + expect(mockedApiService.getCurrentlyPlaying).toHaveBeenCalledOnce(); + expect(mockedApiService.getCurrentlyPlaying).toHaveBeenCalledWith(mockUser.spotifyConfig!.accessToken); + + await vi.advanceTimersByTimeAsync(3000); + expect(mockedApiService.getCurrentlyPlaying).toHaveBeenCalledTimes(2); + + await vi.advanceTimersByTimeAsync(3000); + expect(mockedApiService.getCurrentlyPlaying).toHaveBeenCalledTimes(3); + }); + + it("should not start a new polling interval if one is already running for the user", async () => { + pollingService.startPollingForUser(mockUser); + await vi.advanceTimersByTimeAsync(0); + + expect(vi.getTimerCount()).toBe(1); + expect(mockedApiService.getCurrentlyPlaying).toHaveBeenCalledTimes(1); + + pollingService.startPollingForUser(mockUser); + expect(vi.getTimerCount()).toBe(1); // Still only one timer + expect(mockedApiService.getCurrentlyPlaying).toHaveBeenCalledTimes(1); // No new immediate poll + }); + }); + + describe("stopPollingForUser", () => { + it("should clear the active interval for the user", () => { + pollingService.startPollingForUser(mockUser); + expect(vi.getTimerCount()).toBe(1); + + pollingService.stopPollingForUser(mockUser.uuid); + expect(vi.getTimerCount()).toBe(0); + }); + }); + + describe("Polling Logic and Event Emission", () => { + it("should emit a state update event when the song changes", async () => { + const initialState = { item: { id: "song-a" }, is_playing: true }; + const nextState = { item: { id: "song-b" }, is_playing: true }; + + mockedApiService.getCurrentlyPlaying + .mockResolvedValueOnce(initialState as any) + .mockResolvedValueOnce(nextState as any); + + pollingService.startPollingForUser(mockUser); + + await vi.advanceTimersByTimeAsync(0); + expect(mockedAppEventBus.emit).toHaveBeenCalledWith(SPOTIFY_STATE_UPDATED_EVENT, { uuid: mockUser.uuid, state: initialState }); + expect(mockedAppEventBus.emit).toHaveBeenCalledTimes(1); + + + await vi.advanceTimersByTimeAsync(3000); + expect(mockedAppEventBus.emit).toHaveBeenCalledWith(SPOTIFY_STATE_UPDATED_EVENT, { uuid: mockUser.uuid, state: nextState }); + + expect(mockedAppEventBus.emit).toHaveBeenCalledTimes(2); + }); + + it("should NOT emit a state update event if the state is unchanged", async () => { + const state = { item: { id: "song-a" }, is_playing: true }; + mockedApiService.getCurrentlyPlaying.mockResolvedValue(state as any); + + pollingService.startPollingForUser(mockUser); + + await vi.advanceTimersByTimeAsync(0); + + await vi.advanceTimersByTimeAsync(3000); + + expect(mockedAppEventBus.emit).toHaveBeenCalledTimes(1); + }); + }); + + describe("Token Refresh and Error Handling", () => { + it("should refresh the token if it is expired and then call the API with the new token", async () => { + const expiredUser = { + ...mockUser, + spotifyConfig: { ...mockUser.spotifyConfig!, expirationDate: new Date(Date.now() - 1000) } + }; + mockedUserService.getUserByUUID.mockResolvedValue(expiredUser as any); + + const refreshedToken = {access_token: "new-refreshed-token", expires_in: 3600, scope: "some-scope"} as any; + mockedTokenService.refreshToken.mockResolvedValue(refreshedToken); + + mockedUserService.updateUserByUUID.mockImplementation(async (uuid, updates) => ({ + ...expiredUser, ...updates + } as any)); + + pollingService.startPollingForUser(expiredUser as IUser); + + await vi.advanceTimersByTimeAsync(0); + + expect(mockedTokenService.refreshToken).toHaveBeenCalledWith(expiredUser.spotifyConfig.refreshToken); + expect(mockedApiService.getCurrentlyPlaying).toHaveBeenCalledWith(refreshedToken.access_token); + }); + + it("should stop polling if a 401 Unauthorized error occurs", async () => { + const error = new AxiosError("Unauthorized"); + (error as any).response = { status: 401 }; + mockedApiService.getCurrentlyPlaying.mockRejectedValue(error); + + pollingService.startPollingForUser(mockUser); + + await vi.advanceTimersByTimeAsync(0); + + expect(vi.getTimerCount()).toBe(0); + }); + + it("should pause and automatically resume polling after a 429 Rate Limit error", async () => { + const error = new AxiosError("Rate Limit"); + (error as any).response = { status: 429, headers: { "retry-after": "5" } }; + + mockedApiService.getCurrentlyPlaying + .mockRejectedValueOnce(error) + .mockResolvedValue({ item: { id: "song-a" }, is_playing: true } as any); + + pollingService.startPollingForUser(mockUser); + + await vi.advanceTimersByTimeAsync(0); + + expect(mockedApiService.getCurrentlyPlaying).toHaveBeenCalledTimes(1); + expect(vi.getTimerCount()).toBe(1); + + await vi.advanceTimersByTimeAsync(5000); + + expect(mockedApiService.getCurrentlyPlaying).toHaveBeenCalledTimes(2); + expect(vi.getTimerCount()).toBe(1); + }); + }); +}); \ No newline at end of file diff --git a/tests/db/services/spotifyTokenService.test.ts b/tests/services/spotifyTokenService.test.ts similarity index 90% rename from tests/db/services/spotifyTokenService.test.ts rename to tests/services/spotifyTokenService.test.ts index 362642b..fbedc14 100644 --- a/tests/db/services/spotifyTokenService.test.ts +++ b/tests/services/spotifyTokenService.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import axios from "axios"; -import type { SpotifyTokenService as SpotifyTokenServiceType } from "../../../src/db/services/spotifyTokenService"; -import type { OAuthTokenResponse } from "../../../src/interfaces/OAuthTokenResponse"; +import type { OAuthTokenResponse } from "../../src/interfaces/OAuthTokenResponse"; +import {SpotifyTokenService} from "../../src/services/spotifyTokenService"; vi.mock("axios"); const mockedAxiosPost = vi.mocked(axios.post); @@ -14,10 +14,10 @@ afterEach(() => { }); describe("SpotifyTokenService - Successful Initialization", () => { - let spotifyTokenService: SpotifyTokenServiceType; + let spotifyTokenService: SpotifyTokenService; beforeEach(async () => { - const { SpotifyTokenService } = await import("../../../src/db/services/spotifyTokenService"); + const { SpotifyTokenService } = await import("../../src/services/spotifyTokenService"); spotifyTokenService = new SpotifyTokenService("test-client-id","test-client-secret"); }); diff --git a/tests/utils/websocket/websocketCustomEvents/websocketEventUtils.test.ts b/tests/utils/websocket/websocketCustomEvents/websocketEventUtils.test.ts index d6d86a8..49533a6 100644 --- a/tests/utils/websocket/websocketCustomEvents/websocketEventUtils.test.ts +++ b/tests/utils/websocket/websocketCustomEvents/websocketEventUtils.test.ts @@ -1,101 +1,55 @@ -import { describe, it, expect, vi, beforeEach, type Mocked } from "vitest"; -import { getEventListeners } from "../../../../src/utils/websocket/websocketCustomEvents/websocketEventUtils"; -import { WebsocketEventType } from "../../../../src/utils/websocket/websocketCustomEvents/websocketEventType"; -import { CustomWebsocketEvent } from "../../../../src/utils/websocket/websocketCustomEvents/customWebsocketEvent"; -import type { UserService } from "../../../../src/db/services/db/UserService"; -import { - CustomWebsocketEventUserService -} from "../../../../src/utils/websocket/websocketCustomEvents/customWebsocketEventUserService"; -import {createMockSpotifyTokenService} from "../../../helpers/testSetup"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { ExtendedWebSocket } from "../../../../src/interfaces/extendedWebsocket"; +import { GetStateEvent } from "../../../../src/utils/websocket/websocketCustomEvents/getStateEvent"; +import { GetSettingsEvent } from "../../../../src/utils/websocket/websocketCustomEvents/getSettingsEvent"; -type MockWs = { - user: { - timezone: string; - lastState: { global: { mode: string; brightness: number } }; - }; - send: Mocked<(data: any, options: { binary: boolean }) => void>; - on: Mocked<(event: string, listener: (...args: any[]) => void) => void>; - emit: Mocked<(event: string, ...args: any[]) => void>; +const createMockWebSocket = (userPayload: any = {}): ExtendedWebSocket => { + return { + send: vi.fn(), + emit: vi.fn(), + user: { + timezone: "Europe/Berlin", + lastState: { global: { mode: "idle", brightness: 42 } }, + ...userPayload, + }, + payload: { uuid: "test-uuid-123" }, + } as unknown as ExtendedWebSocket; }; -type MockUserService = Mocked; + +describe("WebSocket Custom Event Handlers", () => { -describe("websocketEventUtils.getEventListeners", () => { - let mockWs: MockWs; - let mockUserService: MockUserService; - let listeners: CustomWebsocketEvent[]; + describe("GetStateEvent", () => { + it("should send the user's lastState when its handler is called", async () => { + const mockLastState = { global: { mode: "music", brightness: 100 } }; + const mockWs = createMockWebSocket({ lastState: mockLastState }); - beforeEach(() => { - mockWs = { - user: { - timezone: "Europe/Berlin", - lastState: { global: { mode: "idle", brightness: 42 } }, - }, - send: vi.fn(), - on: vi.fn(), - emit: vi.fn(), - }; - - mockUserService = { - getUserByUUID: vi.fn(), - updateUser: vi.fn(), - } as any; - - - - listeners = getEventListeners(mockWs as any, mockUserService, createMockSpotifyTokenService() as any); - }); - - it("should return an array of event listener objects", () => { - expect(Array.isArray(listeners)).toBe(true); - expect(listeners.length).toBeGreaterThan(0); - - for (const listener of listeners) { - expect(listener).toHaveProperty("event"); - expect(listener).toHaveProperty("handler"); - expect(typeof listener.handler).toBe("function"); - if (typeof listener === typeof CustomWebsocketEventUserService){ - expect(listener).toHaveProperty("userService", mockUserService); - } - } - }); - - describe("GET_STATE event handler", () => { - it("should include a handler for GET_STATE", () => { - const getStateListener = listeners.find(l => l.event === WebsocketEventType.GET_STATE); - expect(getStateListener).toBeDefined(); - }); - - it("should send the user's last state when the handler is called", () => { - const getStateListener = listeners.find(l => l.event === WebsocketEventType.GET_STATE); - - getStateListener!.handler({}); + const event = new GetStateEvent(mockWs); + await event.handler(); expect(mockWs.send).toHaveBeenCalledOnce(); expect(mockWs.send).toHaveBeenCalledWith( - JSON.stringify({ type: "STATE", payload: mockWs.user.lastState }), + JSON.stringify({ type: "STATE", payload: mockLastState }), { binary: false } ); }); }); - describe("GET_SETTINGS event handler", () => { - it("should include a handler for GET_SETTINGS", () => { - const getSettingsListener = listeners.find(l => l.event === WebsocketEventType.GET_SETTINGS); - expect(getSettingsListener).toBeDefined(); - }); + describe("GetSettingsEvent", () => { + it("should send the user's settings when its handler is called", async () => { + const mockTimezone = "America/New_York"; + const mockWs = createMockWebSocket({ timezone: mockTimezone }); - it("should send the user's timezone when the handler is called", () => { - const getSettingsListener = listeners.find(l => l.event === WebsocketEventType.GET_SETTINGS); - - getSettingsListener!.handler({}); + const event = new GetSettingsEvent(mockWs); + await event.handler(); expect(mockWs.send).toHaveBeenCalledOnce(); expect(mockWs.send).toHaveBeenCalledWith( - JSON.stringify({ type: "SETTINGS", payload: { timezone: mockWs.user.timezone } }), + JSON.stringify({ type: "SETTINGS", payload: { timezone: mockTimezone } }), { binary: false } ); }); }); + }); \ No newline at end of file diff --git a/tests/utils/websocket/websocketEventHandler.test.ts b/tests/utils/websocket/websocketEventHandler.test.ts index 2f50dfd..f55f180 100644 --- a/tests/utils/websocket/websocketEventHandler.test.ts +++ b/tests/utils/websocket/websocketEventHandler.test.ts @@ -2,14 +2,12 @@ import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from "vi import { WebsocketEventHandler } from "../../../src/utils/websocket/websocketEventHandler"; import { ExtendedWebSocket } from "../../../src/interfaces/extendedWebsocket"; import { CustomWebsocketEvent } from "../../../src/utils/websocket/websocketCustomEvents/customWebsocketEvent"; -import {UserService} from "../../../src/db/services/db/UserService"; -import {SpotifyTokenService} from "../../../src/db/services/spotifyTokenService"; +import {SpotifyPollingService} from "../../../src/services/spotifyPollingService"; describe("WebsocketEventHandler", () => { let mockWebSocket: Mocked; let websocketEventHandler: WebsocketEventHandler; - let mockUserService: Mocked; - let mockSpotifyTokenService: Mocked + let mockSpotifyPollingService: Mocked let registeredHandlers: Map void>; beforeEach(() => { @@ -32,11 +30,9 @@ describe("WebsocketEventHandler", () => { } as unknown as Mocked; // not used in this test - mockUserService = {} as Mocked; - mockSpotifyTokenService = {} as Mocked; + mockSpotifyPollingService = {} as Mocked; - - websocketEventHandler = new WebsocketEventHandler(mockWebSocket, mockUserService, mockSpotifyTokenService); + websocketEventHandler = new WebsocketEventHandler(mockWebSocket, mockSpotifyPollingService); }); afterEach(() => { diff --git a/tests/utils/websocket/websocketServerEventHandler.test.ts b/tests/utils/websocket/websocketServerEventHandler.test.ts index 69d4089..1c455e4 100644 --- a/tests/utils/websocket/websocketServerEventHandler.test.ts +++ b/tests/utils/websocket/websocketServerEventHandler.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from "vitest"; import { WebsocketServerEventHandler } from "../../../src/utils/websocket/websocketServerEventHandler"; -import type { UserService } from "../../../src/db/services/db/UserService"; +import {UserService} from "../../../src/services/db/UserService"; const heartbeatSpy = vi.fn(); vi.mock("../../../src/utils/websocket/websocketServerHeartbeatInterval", () => ({ diff --git a/tests/websocket.test.ts b/tests/websocket.test.ts index f98e5d8..4b85029 100644 --- a/tests/websocket.test.ts +++ b/tests/websocket.test.ts @@ -5,9 +5,9 @@ import { ExtendedWebSocketServer } from "../src/websocket"; import { WebsocketServerEventHandler } from "../src/utils/websocket/websocketServerEventHandler"; import { WebsocketEventHandler } from "../src/utils/websocket/websocketEventHandler"; import { getEventListeners } from "../src/utils/websocket/websocketCustomEvents/websocketEventUtils"; -import {UserService} from "../src/db/services/db/UserService"; -import {createMockSpotifyTokenService, createMockUserService} from "./helpers/testSetup"; -import {SpotifyTokenService} from "../src/db/services/spotifyTokenService"; +import { createMockUserService} from "./helpers/testSetup"; +import {UserService} from "../src/services/db/UserService"; +import {SpotifyPollingService} from "../src/services/spotifyPollingService"; let mockWssInstance: Mocked; let mockServerEventHandler: Mocked; @@ -28,7 +28,7 @@ describe("ExtendedWebSocketServer", () => { let mockHttpServer: Mocked; let extendedWss: ExtendedWebSocketServer; let mockUserService: Mocked; - let mockSpotifyService : Mocked; + let mockSpotifyPollingService: Mocked beforeEach(() => { vi.clearAllMocks(); @@ -47,11 +47,11 @@ describe("ExtendedWebSocketServer", () => { close: vi.fn(), } as unknown as Mocked; + mockSpotifyPollingService = {} as any; + mockUserService = createMockUserService(); - mockSpotifyService = createMockSpotifyTokenService() as any; - - extendedWss = new ExtendedWebSocketServer(mockHttpServer, mockUserService, mockSpotifyService); + extendedWss = new ExtendedWebSocketServer(mockHttpServer, mockUserService, mockSpotifyPollingService); }); describe("Constructor and Setup", () => { @@ -121,7 +121,7 @@ describe("ExtendedWebSocketServer", () => { it("should create and configure a WebsocketEventHandler for new clients", () => { connectionHandler(mockWsClient, {}); - expect(vi.mocked(WebsocketEventHandler)).toHaveBeenCalledWith(mockWsClient, mockUserService, mockSpotifyService); + expect(vi.mocked(WebsocketEventHandler)).toHaveBeenCalledWith(mockWsClient, mockSpotifyPollingService); expect(mockClientEventHandler.enableErrorEvent).toHaveBeenCalled(); expect(mockClientEventHandler.enablePongEvent).toHaveBeenCalled(); expect(mockClientEventHandler.enableMessageEvent).toHaveBeenCalled();