refactoring, added s3Service.ts, add more depenedncy injection, fix tests as always i geuss
This commit is contained in:
Generated
+2232
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.895.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "^3.895.0",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jsonwebtoken": "^9.0.5",
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {appEventBus, USER_UPDATED_EVENT} from "../../utils/eventBus";
|
|||||||
export function watchUserChanges() {
|
export function watchUserChanges() {
|
||||||
const changeStream = UserModel.watch([], { fullDocument: 'updateLookup' });
|
const changeStream = UserModel.watch([], { fullDocument: 'updateLookup' });
|
||||||
|
|
||||||
changeStream.on('change', (change) => {
|
changeStream.on('change', (change: any) => {
|
||||||
if (change.operationType === 'update' && change.fullDocument) {
|
if (change.operationType === 'update' && change.fullDocument) {
|
||||||
const updatedUser = change.fullDocument;
|
const updatedUser = change.fullDocument;
|
||||||
|
|
||||||
|
|||||||
+73
-6
@@ -1,9 +1,27 @@
|
|||||||
|
import {Server} from "./server";
|
||||||
import { Server } from "./server";
|
import {config as baseConfig} from "./config/config";
|
||||||
import { config as baseConfig} from "./config";
|
import {S3Service} from "./services/s3Service";
|
||||||
|
import {UserService} from "./services/db/UserService";
|
||||||
|
import {SpotifyTokenService} from "./services/spotifyTokenService";
|
||||||
|
import {connectToDatabase} from "./services/db/database.service";
|
||||||
|
import {SpotifyApiService} from "./services/spotifyApiService";
|
||||||
|
import {SpotifyPollingService} from "./services/spotifyPollingService";
|
||||||
|
import {WeatherPollingService} from "./services/weatherPollingService";
|
||||||
|
import {JwtAuthenticator} from "./utils/jwtAuthenticator";
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const { SECRET_KEY, SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET } = process.env;
|
const {
|
||||||
|
SECRET_KEY,
|
||||||
|
SPOTIFY_CLIENT_ID,
|
||||||
|
SPOTIFY_CLIENT_SECRET,
|
||||||
|
MINIO_ENDPOINT,
|
||||||
|
MINIO_PORT,
|
||||||
|
MINIO_BUCKET_NAME,
|
||||||
|
MINIO_ROOT_USER,
|
||||||
|
MINIO_ROOT_PASSWORD,
|
||||||
|
DB_NAME,
|
||||||
|
DB_CONN_STRING
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
if (!SECRET_KEY || SECRET_KEY.length < 32) {
|
if (!SECRET_KEY || SECRET_KEY.length < 32) {
|
||||||
throw new Error("CRITICAL ERROR: SECRET_KEY environment variable is not set or too short.");
|
throw new Error("CRITICAL ERROR: SECRET_KEY environment variable is not set or too short.");
|
||||||
@@ -15,12 +33,61 @@ async function bootstrap() {
|
|||||||
throw new Error("CRITICAL ERROR: SPOTIFY_CLIENT_SECRET environment variable is not set.");
|
throw new Error("CRITICAL ERROR: SPOTIFY_CLIENT_SECRET environment variable is not set.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!MINIO_ENDPOINT || !MINIO_PORT) {
|
||||||
|
throw new Error("MINIO_ENDPOINT and/or MINIO_PORT environment variable is not set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MINIO_ROOT_USER || !MINIO_ROOT_PASSWORD) {
|
||||||
|
throw new Error("MINIO_ROOT_USER and/or MINIO_ROOT_PASSWORD environment variable is not set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MINIO_BUCKET_NAME) {
|
||||||
|
throw new Error("MINIO_BUCKET_NAME environment variable is not set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DB_NAME || !DB_CONN_STRING) {
|
||||||
|
throw new Error("DB_NAME and/or DB_CONN_STRING environment variable is not set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const s3ClientConfig = {
|
||||||
|
endpoint: MINIO_ENDPOINT,
|
||||||
|
port: parseInt(MINIO_PORT),
|
||||||
|
accessKey: MINIO_ROOT_USER,
|
||||||
|
secretAccessKey: MINIO_ROOT_PASSWORD,
|
||||||
|
bucket: MINIO_BUCKET_NAME
|
||||||
|
};
|
||||||
|
|
||||||
|
const dbConfig = {
|
||||||
|
dbName: DB_NAME,
|
||||||
|
dbConnString: DB_CONN_STRING
|
||||||
|
}
|
||||||
|
|
||||||
|
await connectToDatabase(dbConfig.dbName, dbConfig.dbConnString);
|
||||||
|
|
||||||
|
const s3Service = S3Service.getInstance(s3ClientConfig);
|
||||||
|
const userService = await UserService.create();
|
||||||
|
const spotifyTokenService = new SpotifyTokenService(
|
||||||
|
SPOTIFY_CLIENT_ID!,
|
||||||
|
SPOTIFY_CLIENT_SECRET!
|
||||||
|
);
|
||||||
|
|
||||||
|
const spotifyApiService = new SpotifyApiService();
|
||||||
|
const spotifyPollingService = new SpotifyPollingService(userService, spotifyApiService, spotifyTokenService);
|
||||||
|
const weatherPollingService = new WeatherPollingService();
|
||||||
|
|
||||||
|
const jwtAuthenticator = new JwtAuthenticator(SECRET_KEY);
|
||||||
|
|
||||||
const server = new Server({
|
const server = new Server({
|
||||||
port: baseConfig.port,
|
port: baseConfig.port,
|
||||||
jwtSecret: SECRET_KEY,
|
jwtSecret: SECRET_KEY,
|
||||||
spotifyClientId: SPOTIFY_CLIENT_ID,
|
|
||||||
spotifyClientSecret: SPOTIFY_CLIENT_SECRET,
|
|
||||||
cors: baseConfig.cors,
|
cors: baseConfig.cors,
|
||||||
|
}, {
|
||||||
|
s3Service,
|
||||||
|
userService,
|
||||||
|
spotifyTokenService,
|
||||||
|
spotifyPollingService,
|
||||||
|
weatherPollingService,
|
||||||
|
jwtAuthenticator
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.start();
|
await server.start();
|
||||||
|
|||||||
+4
-3
@@ -10,9 +10,11 @@ import {UserService} from "../services/db/UserService";
|
|||||||
|
|
||||||
export class RestAuth {
|
export class RestAuth {
|
||||||
private readonly userService: UserService;
|
private readonly userService: UserService;
|
||||||
|
private readonly jwtAuthenticator: JwtAuthenticator;
|
||||||
|
|
||||||
constructor(userService: UserService) {
|
constructor(userService: UserService, jwtAuthenticator: JwtAuthenticator) {
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
|
this.jwtAuthenticator = jwtAuthenticator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public createRouter() {
|
public createRouter() {
|
||||||
@@ -81,8 +83,7 @@ export class RestAuth {
|
|||||||
return unauthorized(res, "Invalid password", {field: "password", code: "INVALID_PASSWORD"});
|
return unauthorized(res, "Invalid password", {field: "password", code: "INVALID_PASSWORD"});
|
||||||
}
|
}
|
||||||
|
|
||||||
const jwtToken = new JwtAuthenticator(process.env.SECRET_KEY!)
|
const jwtToken = this.jwtAuthenticator.generateToken({
|
||||||
.generateToken({
|
|
||||||
username: user.name,
|
username: user.name,
|
||||||
id: user.id,
|
id: user.id,
|
||||||
uuid: user.uuid
|
uuid: user.uuid
|
||||||
|
|||||||
+44
-35
@@ -1,32 +1,39 @@
|
|||||||
import express, { Express, Request, Response, NextFunction } from "express";
|
import express, {Express, Request, Response, NextFunction} from "express";
|
||||||
import { Server as HttpServer } from "http";
|
import {Server as HttpServer} from "http";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import { randomUUID } from "crypto";
|
import {randomUUID} from "crypto";
|
||||||
|
|
||||||
import { ExtendedWebSocketServer } from "./websocket";
|
import {ExtendedWebSocketServer} from "./websocket";
|
||||||
import { RestWebSocket } from "./rest/restWebSocket";
|
import {RestWebSocket} from "./rest/restWebSocket";
|
||||||
import { RestUser } from "./rest/restUser";
|
import {RestUser} from "./rest/restUser";
|
||||||
import { JwtTokenPropertiesExtractor } from "./rest/jwtTokenPropertiesExtractor";
|
import {JwtTokenPropertiesExtractor} from "./rest/jwtTokenPropertiesExtractor";
|
||||||
import { SpotifyTokenGenerator } from "./rest/spotifyTokenGenerator";
|
import {SpotifyTokenGenerator} from "./rest/spotifyTokenGenerator";
|
||||||
import { RestAuth } from "./rest/auth";
|
import {RestAuth} from "./rest/auth";
|
||||||
import { authLimiter, spotifyLimiter } from "./rest/middleware/rateLimit";
|
import {authLimiter, spotifyLimiter} from "./rest/middleware/rateLimit";
|
||||||
import { extractTokenFromCookie } from "./rest/middleware/extractTokenFromCookie";
|
import {extractTokenFromCookie} from "./rest/middleware/extractTokenFromCookie";
|
||||||
import { JwtAuthenticator } from "./utils/jwtAuthenticator";
|
import {JwtAuthenticator} from "./utils/jwtAuthenticator";
|
||||||
import { authenticateJwt } from "./rest/middleware/authenticateJwt";
|
import {authenticateJwt} from "./rest/middleware/authenticateJwt";
|
||||||
import {watchUserChanges} from "./db/models/userWatch";
|
import {watchUserChanges} from "./db/models/userWatch";
|
||||||
import {SpotifyPollingService} from "./services/spotifyPollingService";
|
import {SpotifyPollingService} from "./services/spotifyPollingService";
|
||||||
import {SpotifyApiService} from "./services/spotifyApiService";
|
|
||||||
import {UserService} from "./services/db/UserService";
|
import {UserService} from "./services/db/UserService";
|
||||||
import {connectToDatabase, disconnectFromDatabase} from "./services/db/database.service";
|
import {disconnectFromDatabase} from "./services/db/database.service";
|
||||||
import {SpotifyTokenService} from "./services/spotifyTokenService";
|
import {SpotifyTokenService} from "./services/spotifyTokenService";
|
||||||
import {WeatherPollingService} from "./services/weatherPollingService";
|
import {WeatherPollingService} from "./services/weatherPollingService";
|
||||||
|
import {S3Service} from "./services/s3Service";
|
||||||
|
|
||||||
|
interface ServerDependencies {
|
||||||
|
userService: UserService;
|
||||||
|
s3Service: S3Service;
|
||||||
|
spotifyTokenService: SpotifyTokenService;
|
||||||
|
spotifyPollingService: SpotifyPollingService;
|
||||||
|
weatherPollingService: WeatherPollingService;
|
||||||
|
jwtAuthenticator: JwtAuthenticator;
|
||||||
|
}
|
||||||
|
|
||||||
interface ServerConfig {
|
interface ServerConfig {
|
||||||
port: number;
|
port: number;
|
||||||
jwtSecret: string;
|
jwtSecret: string;
|
||||||
spotifyClientId: string;
|
|
||||||
spotifyClientSecret: string;
|
|
||||||
cors: {
|
cors: {
|
||||||
origin: string | string[];
|
origin: string | string[];
|
||||||
credentials: boolean;
|
credentials: boolean;
|
||||||
@@ -36,34 +43,36 @@ interface ServerConfig {
|
|||||||
export class Server {
|
export class Server {
|
||||||
public readonly app: Express;
|
public readonly app: Express;
|
||||||
private httpServer: HttpServer | null = null;
|
private httpServer: HttpServer | null = null;
|
||||||
private userService: UserService | null = null;
|
|
||||||
private webSocketServer: ExtendedWebSocketServer | null = null;
|
private webSocketServer: ExtendedWebSocketServer | null = null;
|
||||||
|
|
||||||
constructor(private readonly config: ServerConfig) {
|
constructor(private readonly config: ServerConfig,
|
||||||
|
private readonly dependencies: ServerDependencies) {
|
||||||
this.app = express();
|
this.app = express();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start(): Promise<HttpServer> {
|
public async start(): Promise<HttpServer> {
|
||||||
await connectToDatabase();
|
const {
|
||||||
|
userService,
|
||||||
|
s3Service,
|
||||||
|
spotifyTokenService,
|
||||||
|
spotifyPollingService,
|
||||||
|
weatherPollingService,
|
||||||
|
jwtAuthenticator
|
||||||
|
} = this.dependencies;
|
||||||
|
|
||||||
|
await s3Service.ensureBucketExists()
|
||||||
|
|
||||||
watchUserChanges();
|
watchUserChanges();
|
||||||
|
|
||||||
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._setupMiddleware();
|
||||||
this._setupRoutes(this.userService, spotifyTokenService);
|
this._setupRoutes(userService, spotifyTokenService, jwtAuthenticator);
|
||||||
this._setupErrorHandling();
|
this._setupErrorHandling();
|
||||||
|
|
||||||
this.httpServer = this.app.listen(this.config.port, () => {
|
this.httpServer = this.app.listen(this.config.port, () => {
|
||||||
console.log(`Server is running on port ${this.config.port}`);
|
console.log(`Server is running on port ${this.config.port}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.webSocketServer = new ExtendedWebSocketServer(this.httpServer, this.userService, spotifyPollingService, weatherPollingService);
|
this.webSocketServer = new ExtendedWebSocketServer(this.httpServer, userService, spotifyPollingService, weatherPollingService, jwtAuthenticator);
|
||||||
|
|
||||||
this._setupGracefulShutdown();
|
this._setupGracefulShutdown();
|
||||||
|
|
||||||
@@ -88,18 +97,18 @@ export class Server {
|
|||||||
credentials: this.config.cors.credentials,
|
credentials: this.config.cors.credentials,
|
||||||
}));
|
}));
|
||||||
this.app.use(this._securityHeaders);
|
this.app.use(this._securityHeaders);
|
||||||
this.app.use(express.json({ limit: "2mb" }));
|
this.app.use(express.json({limit: "2mb"}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setupRoutes(userService: UserService, spotifyTokenService: SpotifyTokenService): void {
|
private _setupRoutes(userService: UserService, spotifyTokenService: SpotifyTokenService, jwtAuthenticator: JwtAuthenticator): void {
|
||||||
const _authenticateJwt = authenticateJwt(new JwtAuthenticator(this.config.jwtSecret));
|
const _authenticateJwt = authenticateJwt(jwtAuthenticator);
|
||||||
|
|
||||||
const restAuth = new RestAuth(userService);
|
const restAuth = new RestAuth(userService, jwtAuthenticator);
|
||||||
const restUser = new RestUser(userService);
|
const restUser = new RestUser(userService);
|
||||||
const spotifyTokenGenerator = new SpotifyTokenGenerator(spotifyTokenService);
|
const spotifyTokenGenerator = new SpotifyTokenGenerator(spotifyTokenService);
|
||||||
const jwtTokenExtractor = new JwtTokenPropertiesExtractor();
|
const jwtTokenExtractor = new JwtTokenPropertiesExtractor();
|
||||||
|
|
||||||
this.app.get("/api/healthz", (_req, res) => res.status(200).send({ status: "ok" }));
|
this.app.get("/api/healthz", (_req, res) => res.status(200).send({status: "ok"}));
|
||||||
|
|
||||||
this.app.use("/api/auth", authLimiter, restAuth.createRouter());
|
this.app.use("/api/auth", authLimiter, restAuth.createRouter());
|
||||||
|
|
||||||
@@ -148,7 +157,7 @@ export class Server {
|
|||||||
ok: false,
|
ok: false,
|
||||||
data: {
|
data: {
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
...(statusCode >= 500 && { errorId: errorId }),
|
...(statusCode >= 500 && {errorId: errorId}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,17 +4,7 @@ import mongoose, { ConnectOptions } from "mongoose";
|
|||||||
let isConnected: boolean = false;
|
let isConnected: boolean = false;
|
||||||
let connectionPromise: Promise<void> | null = null;
|
let connectionPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
const connectWithRetry = async (): Promise<void> => {
|
const connectWithRetry = async (dbName: string, dbConnString: string): Promise<void> => {
|
||||||
const dbConnString: string | undefined = process.env.DB_CONN_STRING;
|
|
||||||
const dbName: string | undefined = process.env.DB_NAME;
|
|
||||||
|
|
||||||
if (!dbConnString) {
|
|
||||||
throw new Error("Missing environment variable: DB_CONN_STRING is required for database connection.");
|
|
||||||
}
|
|
||||||
if (!dbName) {
|
|
||||||
throw new Error("Missing environment variable: DB_NAME is required for database connection.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: ConnectOptions = {
|
const options: ConnectOptions = {
|
||||||
dbName: dbName,
|
dbName: dbName,
|
||||||
serverSelectionTimeoutMS: 5000,
|
serverSelectionTimeoutMS: 5000,
|
||||||
@@ -29,11 +19,11 @@ const connectWithRetry = async (): Promise<void> => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to connect to MongoDB. Retrying in 5 seconds...", error);
|
console.error("Failed to connect to MongoDB. Retrying in 5 seconds...", error);
|
||||||
await new Promise<void>(resolve => setTimeout(resolve, 5000));
|
await new Promise<void>(resolve => setTimeout(resolve, 5000));
|
||||||
return connectWithRetry();
|
return connectWithRetry(dbName, dbConnString);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function connectToDatabase(): Promise<void> {
|
export async function connectToDatabase(dbName: string, dbConnString: string): Promise<void> {
|
||||||
if (connectionPromise) {
|
if (connectionPromise) {
|
||||||
return connectionPromise;
|
return connectionPromise;
|
||||||
}
|
}
|
||||||
@@ -59,7 +49,7 @@ export async function connectToDatabase(): Promise<void> {
|
|||||||
console.error('Mongoose connection error:', err);
|
console.error('Mongoose connection error:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
await connectWithRetry();
|
await connectWithRetry(dbName, dbConnString);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return connectionPromise;
|
return connectionPromise;
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import {
|
||||||
|
S3Client,
|
||||||
|
CreateBucketCommand,
|
||||||
|
PutObjectCommand,
|
||||||
|
GetObjectCommand
|
||||||
|
} from "@aws-sdk/client-s3";
|
||||||
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
|
export interface S3ClientConfig {
|
||||||
|
endpoint: string;
|
||||||
|
port: number;
|
||||||
|
accessKey: string;
|
||||||
|
secretAccessKey: string;
|
||||||
|
bucket: string;
|
||||||
|
region?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class S3Service {
|
||||||
|
private static instance: S3Service;
|
||||||
|
|
||||||
|
private readonly client: S3Client;
|
||||||
|
private readonly bucketName: string;
|
||||||
|
|
||||||
|
private constructor(clientConfig: S3ClientConfig) {
|
||||||
|
this.client = new S3Client({
|
||||||
|
endpoint: `${clientConfig.endpoint}:${clientConfig.port}`,
|
||||||
|
forcePathStyle: true,
|
||||||
|
region: clientConfig.region || "us-east-1",
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: clientConfig.accessKey,
|
||||||
|
secretAccessKey: clientConfig.secretAccessKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bucketName = clientConfig.bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(config?: S3ClientConfig): S3Service {
|
||||||
|
if (!this.instance) {
|
||||||
|
if (!config) {
|
||||||
|
throw new Error("S3Service must be initialized with a config on first use.");
|
||||||
|
}
|
||||||
|
this.instance = new S3Service(config);
|
||||||
|
}
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureBucketExists(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.client.send(new CreateBucketCommand({ Bucket: this.bucketName }));
|
||||||
|
console.log(`Bucket "${this.bucketName}" created successfully or already existed.`);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.name === 'BucketAlreadyOwnedByYou' || err.name === 'BucketAlreadyExists') {
|
||||||
|
console.log(`Bucket "${this.bucketName}" already exists.`);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFile(file: Express.Multer.File, userId: string): Promise<string> {
|
||||||
|
const fileExtension = file.originalname.split('.').pop();
|
||||||
|
const objectKey = `user-${userId}/${randomUUID()}.${fileExtension}`;
|
||||||
|
|
||||||
|
const command = new PutObjectCommand({
|
||||||
|
Bucket: this.bucketName,
|
||||||
|
Key: objectKey,
|
||||||
|
Body: file.buffer,
|
||||||
|
ContentType: file.mimetype,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.client.send(command);
|
||||||
|
return objectKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSignedDownloadUrl(objectKey: string, expiresIn: number = 60): Promise<string> {
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: this.bucketName,
|
||||||
|
Key: objectKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await getSignedUrl(this.client, command, { expiresIn });
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+20
-5
@@ -1,9 +1,24 @@
|
|||||||
import { DecodedToken } from "../interfaces/decodedToken";
|
import {DecodedToken} from "../interfaces/decodedToken";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
declare namespace Express {
|
declare namespace Express {
|
||||||
export interface Request {
|
export interface Request {
|
||||||
payload: DecodedToken;
|
payload: DecodedToken;
|
||||||
|
file?: Multer.File;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Multer {
|
||||||
|
export interface File {
|
||||||
|
fieldname: string;
|
||||||
|
originalname: string;
|
||||||
|
encoding: string;
|
||||||
|
mimetype: string;
|
||||||
|
size: number;
|
||||||
|
destination: string;
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
buffer: Buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,9 @@ import {JwtAuthenticator} from "./jwtAuthenticator";
|
|||||||
|
|
||||||
export function verifyClient(
|
export function verifyClient(
|
||||||
request: IncomingMessage,
|
request: IncomingMessage,
|
||||||
|
jwtAuthenticator: JwtAuthenticator,
|
||||||
callback: (res: boolean, code?: number, message?: string) => void,
|
callback: (res: boolean, code?: number, message?: string) => void,
|
||||||
) {
|
) {
|
||||||
const jwtAuthenticator = new JwtAuthenticator(
|
|
||||||
process.env.SECRET_KEY as string,
|
|
||||||
);
|
|
||||||
|
|
||||||
const token = jwtAuthenticator.verifyToken(request.headers["authorization"]?.slice("Bearer ".length));
|
const token = jwtAuthenticator.verifyToken(request.headers["authorization"]?.slice("Bearer ".length));
|
||||||
if (!token) {
|
if (!token) {
|
||||||
reject(request, callback);
|
reject(request, callback);
|
||||||
|
|||||||
+4
-2
@@ -15,6 +15,7 @@ import {IUser} from "./db/models/user";
|
|||||||
import {SpotifyPollingService} from "./services/spotifyPollingService";
|
import {SpotifyPollingService} from "./services/spotifyPollingService";
|
||||||
import {UserService} from "./services/db/UserService";
|
import {UserService} from "./services/db/UserService";
|
||||||
import {WeatherPollingService} from "./services/weatherPollingService";
|
import {WeatherPollingService} from "./services/weatherPollingService";
|
||||||
|
import {JwtAuthenticator} from "./utils/jwtAuthenticator";
|
||||||
|
|
||||||
export class ExtendedWebSocketServer {
|
export class ExtendedWebSocketServer {
|
||||||
private readonly _wss: WebSocketServer;
|
private readonly _wss: WebSocketServer;
|
||||||
@@ -22,14 +23,15 @@ export class ExtendedWebSocketServer {
|
|||||||
private readonly spotifyPollingService: SpotifyPollingService;
|
private readonly spotifyPollingService: SpotifyPollingService;
|
||||||
private readonly weatherPollingService: WeatherPollingService;
|
private readonly weatherPollingService: WeatherPollingService;
|
||||||
|
|
||||||
constructor(server: Server, userService: UserService, spotifyPollingService: SpotifyPollingService, weatherPollingService: WeatherPollingService) {
|
constructor(server: Server, userService: UserService, spotifyPollingService: SpotifyPollingService,
|
||||||
|
weatherPollingService: WeatherPollingService, jwtAuthenticator: JwtAuthenticator) {
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.spotifyPollingService = spotifyPollingService;
|
this.spotifyPollingService = spotifyPollingService;
|
||||||
this.weatherPollingService = weatherPollingService;
|
this.weatherPollingService = weatherPollingService;
|
||||||
|
|
||||||
this._wss = new WebSocketServer({
|
this._wss = new WebSocketServer({
|
||||||
server,
|
server,
|
||||||
verifyClient: (info, callback) => verifyClient(info.req, callback),
|
verifyClient: (info, callback) => verifyClient(info.req, jwtAuthenticator, callback),
|
||||||
});
|
});
|
||||||
|
|
||||||
this._setupConnectionHandling();
|
this._setupConnectionHandling();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const loadConfigWithEnv = async (envVars: Record<string, string | undefined>) =>
|
|||||||
for (const key in envVars) {
|
for (const key in envVars) {
|
||||||
vi.stubEnv(key, envVars[key] as string);
|
vi.stubEnv(key, envVars[key] as string);
|
||||||
}
|
}
|
||||||
const { config } = await import("../src/config");
|
const { config } = await import("../src/config/config");
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe("RestAuth", () => {
|
|||||||
let mockJwtAuthenticator: any;
|
let mockJwtAuthenticator: any;
|
||||||
let mockCrypto: any;
|
let mockCrypto: any;
|
||||||
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ describe("RestAuth", () => {
|
|||||||
mockJwtAuthenticator = createMockJwtAuthenticator();
|
mockJwtAuthenticator = createMockJwtAuthenticator();
|
||||||
vi.mocked(JwtAuthenticator).mockImplementation(() => mockJwtAuthenticator);
|
vi.mocked(JwtAuthenticator).mockImplementation(() => mockJwtAuthenticator);
|
||||||
|
|
||||||
const restAuth = new RestAuth(mockUserService);
|
const restAuth = new RestAuth(mockUserService, mockJwtAuthenticator);
|
||||||
app = createPublicTestApp(restAuth.createRouter(), "/auth");
|
app = createPublicTestApp(restAuth.createRouter(), "/auth");
|
||||||
|
|
||||||
process.env.SECRET_KEY = "test-secret-key";
|
process.env.SECRET_KEY = "test-secret-key";
|
||||||
|
|||||||
+37
-13
@@ -4,15 +4,33 @@ import { Server } from "../src/server";
|
|||||||
import { Router, type Request, type Response, type NextFunction } from "express"; // Import Express types
|
import { Router, type Request, type Response, type NextFunction } from "express"; // Import Express types
|
||||||
import type { Express } from "express";
|
import type { Express } from "express";
|
||||||
import { authLimiter } from "../src/rest/middleware/rateLimit";
|
import { authLimiter } from "../src/rest/middleware/rateLimit";
|
||||||
|
import {
|
||||||
|
createMockJwtAuthenticator,
|
||||||
|
createMockSpotifyPollingService,
|
||||||
|
createMockSpotifyTokenService,
|
||||||
|
createMockUserService
|
||||||
|
} from "./helpers/testSetup";
|
||||||
|
|
||||||
|
|
||||||
|
const mockS3Service = {
|
||||||
|
ensureBucketExists: vi.fn().mockResolvedValue(undefined),
|
||||||
|
uploadFile: vi.fn(),
|
||||||
|
getSignedDownloadUrl: vi.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const mockUserService = createMockUserService() as any;
|
||||||
|
const mockSpotifyTokenService = createMockSpotifyTokenService() as any;
|
||||||
|
const mockSpotifyPollingService = createMockSpotifyPollingService() as any;
|
||||||
|
const mockWeatherPollingService = {
|
||||||
|
subscribeUser: vi.fn(),
|
||||||
|
unsubscribeUser: vi.fn()
|
||||||
|
} as any;
|
||||||
|
const mockJwtAuthenticator = createMockJwtAuthenticator() as any;
|
||||||
|
|
||||||
vi.mock("../src/services/db/database.service", () => ({
|
vi.mock("../src/services/db/database.service", () => ({
|
||||||
connectToDatabase: vi.fn().mockResolvedValue(undefined),
|
connectToDatabase: vi.fn().mockResolvedValue(undefined),
|
||||||
disconnectFromDatabase: vi.fn().mockResolvedValue(undefined),
|
disconnectFromDatabase: vi.fn().mockResolvedValue(undefined),
|
||||||
}));
|
}));
|
||||||
vi.mock("../src/services/db/UserService", () => ({
|
|
||||||
UserService: { create: vi.fn().mockResolvedValue({}) },
|
|
||||||
}));
|
|
||||||
vi.mock("../src/services/spotifyTokenService", () => ({ SpotifyTokenService: vi.fn() }));
|
|
||||||
|
|
||||||
vi.mock("../src/websocket", () => ({
|
vi.mock("../src/websocket", () => ({
|
||||||
ExtendedWebSocketServer: vi.fn().mockImplementation(() => {
|
ExtendedWebSocketServer: vi.fn().mockImplementation(() => {
|
||||||
@@ -29,12 +47,6 @@ vi.mock("../src/rest/middleware/rateLimit", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("../src/rest/middleware/authenticateJwt", () => ({
|
|
||||||
authenticateJwt: vi.fn(() => (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
res.status(401).json({ error: "Unauthorized" });
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../src/rest/auth", () => {
|
vi.mock("../src/rest/auth", () => {
|
||||||
const MockRestAuth = vi.fn().mockImplementation(() => {
|
const MockRestAuth = vi.fn().mockImplementation(() => {
|
||||||
return {
|
return {
|
||||||
@@ -59,8 +71,6 @@ vi.mock("../src/rest/auth", () => {
|
|||||||
const mockServerConfig = {
|
const mockServerConfig = {
|
||||||
port: 8888,
|
port: 8888,
|
||||||
jwtSecret: "a-very-secure-test-secret-that-is-at-least-32-chars-long",
|
jwtSecret: "a-very-secure-test-secret-that-is-at-least-32-chars-long",
|
||||||
spotifyClientId: "test-id",
|
|
||||||
spotifyClientSecret: "test-secret",
|
|
||||||
cors: {
|
cors: {
|
||||||
origin: "http://test-origin.com",
|
origin: "http://test-origin.com",
|
||||||
credentials: true,
|
credentials: true,
|
||||||
@@ -73,7 +83,14 @@ describe("Server Class Integration Tests", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
server = new Server(mockServerConfig);
|
server = new Server(mockServerConfig, {
|
||||||
|
s3Service: mockS3Service,
|
||||||
|
userService: mockUserService,
|
||||||
|
spotifyTokenService: mockSpotifyTokenService,
|
||||||
|
spotifyPollingService: mockSpotifyPollingService,
|
||||||
|
weatherPollingService: mockWeatherPollingService,
|
||||||
|
jwtAuthenticator: mockJwtAuthenticator,
|
||||||
|
});
|
||||||
await server.start();
|
await server.start();
|
||||||
app = server.app;
|
app = server.app;
|
||||||
});
|
});
|
||||||
@@ -82,6 +99,13 @@ describe("Server Class Integration Tests", () => {
|
|||||||
await server.stop();
|
await server.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Server Startup', () => {
|
||||||
|
it('should call ensureBucketExists on S3Service during startup', () => {
|
||||||
|
expect(mockS3Service.ensureBucketExists).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
describe("Server Setup and Middleware", () => {
|
describe("Server Setup and Middleware", () => {
|
||||||
it("should start and respond to the healthz endpoint", async () => {
|
it("should start and respond to the healthz endpoint", async () => {
|
||||||
const response = await request(app).get("/api/healthz").expect(200);
|
const response = await request(app).get("/api/healthz").expect(200);
|
||||||
|
|||||||
@@ -1,110 +1,84 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import mongoose from "mongoose";
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
const MODULE_PATH = "../../../src/services/db/database.service";
|
vi.mock('mongoose', async (importOriginal) => {
|
||||||
|
|
||||||
type SpyInstance<T extends (...args: any) => any> = ReturnType<typeof vi.spyOn<any, Parameters<T>[0]>>;
|
|
||||||
|
|
||||||
vi.mock("mongoose", async (importOriginal) => {
|
|
||||||
const originalMongoose = await importOriginal<typeof mongoose>();
|
const originalMongoose = await importOriginal<typeof mongoose>();
|
||||||
const mockConnection = {
|
|
||||||
on: vi.fn(),
|
|
||||||
};
|
|
||||||
return {
|
return {
|
||||||
...originalMongoose,
|
|
||||||
default: {
|
default: {
|
||||||
...originalMongoose.default,
|
...originalMongoose.default,
|
||||||
connect: vi.fn(),
|
connect: vi.fn(),
|
||||||
disconnect: vi.fn(),
|
disconnect: vi.fn(),
|
||||||
connection: mockConnection,
|
connection: {
|
||||||
|
on: vi.fn(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("dotenv/config", () => ({}));
|
|
||||||
|
|
||||||
const mockedMongooseConnect = vi.mocked(mongoose.connect);
|
const mockedMongooseConnect = vi.mocked(mongoose.connect);
|
||||||
const mockedMongooseDisconnect = vi.mocked(mongoose.disconnect);
|
const mockedMongooseDisconnect = vi.mocked(mongoose.disconnect);
|
||||||
const mockedConnectionOn = vi.mocked(mongoose.connection.on);
|
const mockedConnectionOn = vi.mocked(mongoose.connection.on);
|
||||||
|
|
||||||
describe("database.service", () => {
|
|
||||||
let consoleLogSpy: SpyInstance<typeof console.log>;
|
|
||||||
let consoleErrorSpy: SpyInstance<typeof console.error>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
describe('database.service', () => {
|
||||||
|
|
||||||
|
let connectToDatabase: any;
|
||||||
|
let disconnectFromDatabase: any;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
vi.unstubAllEnvs();
|
|
||||||
|
|
||||||
vi.stubEnv('DB_CONN_STRING', 'mongodb://test-host/testdb');
|
const dbService = await import('../../../src/services/db/database.service');
|
||||||
vi.stubEnv('DB_NAME', 'testdb');
|
connectToDatabase = dbService.connectToDatabase;
|
||||||
|
disconnectFromDatabase = dbService.disconnectFromDatabase;
|
||||||
consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
||||||
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("connectToDatabase", () => {
|
const TEST_DB_NAME = 'testdb';
|
||||||
it("should throw error,when DB_CONN_STRING is not set", async () => {
|
const TEST_DB_CONN_STRING = 'mongodb://test-host/testdb';
|
||||||
vi.unstubAllEnvs();
|
|
||||||
vi.stubEnv('DB_NAME', 'testdb');
|
|
||||||
const { connectToDatabase } = await import(MODULE_PATH);
|
|
||||||
|
|
||||||
await expect(connectToDatabase()).rejects.toThrow(
|
describe('connectToDatabase', () => {
|
||||||
"Missing environment variable: DB_CONN_STRING is required for database connection."
|
it('should attempt to connect to MongoDB with correct options', async () => {
|
||||||
);
|
mockedMongooseConnect.mockResolvedValue(undefined as any);
|
||||||
|
|
||||||
|
await connectToDatabase(TEST_DB_NAME, TEST_DB_CONN_STRING);
|
||||||
|
|
||||||
|
expect(mockedMongooseConnect).toHaveBeenCalledOnce();
|
||||||
|
expect(mockedMongooseConnect).toHaveBeenCalledWith(TEST_DB_CONN_STRING, expect.objectContaining({
|
||||||
|
dbName: TEST_DB_NAME,
|
||||||
|
family: 4,
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw error, when DB_NAME is not set", async () => {
|
it('should correctly set up event listeners on the connection', async () => {
|
||||||
vi.unstubAllEnvs();
|
mockedMongooseConnect.mockResolvedValue(undefined as any);
|
||||||
vi.stubEnv('DB_CONN_STRING', 'mongodb://test-host/testdb');
|
|
||||||
const { connectToDatabase } = await import(MODULE_PATH);
|
|
||||||
|
|
||||||
await expect(connectToDatabase()).rejects.toThrow(
|
await connectToDatabase(TEST_DB_NAME, TEST_DB_CONN_STRING);
|
||||||
"Missing environment variable: DB_NAME is required for database connection."
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should connect successfully first try", async () => {
|
|
||||||
mockedMongooseConnect.mockResolvedValueOnce(undefined as any);
|
|
||||||
const { connectToDatabase } = await import(MODULE_PATH);
|
|
||||||
|
|
||||||
await connectToDatabase();
|
|
||||||
|
|
||||||
expect(mockedMongooseConnect).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockedMongooseConnect).toHaveBeenCalledWith('mongodb://test-host/testdb', expect.any(Object));
|
|
||||||
expect(consoleLogSpy).toHaveBeenCalledWith("Attempting to connect to MongoDB...");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should configure event-listeners", async () => {
|
|
||||||
mockedMongooseConnect.mockResolvedValueOnce(undefined as any);
|
|
||||||
const { connectToDatabase } = await import(MODULE_PATH);
|
|
||||||
|
|
||||||
await connectToDatabase();
|
|
||||||
|
|
||||||
expect(mockedConnectionOn).toHaveBeenCalledWith('connected', expect.any(Function));
|
expect(mockedConnectionOn).toHaveBeenCalledWith('connected', expect.any(Function));
|
||||||
expect(mockedConnectionOn).toHaveBeenCalledWith('disconnected', expect.any(Function));
|
expect(mockedConnectionOn).toHaveBeenCalledWith('disconnected', expect.any(Function));
|
||||||
expect(mockedConnectionOn).toHaveBeenCalledWith('error', expect.any(Function));
|
expect(mockedConnectionOn).toHaveBeenCalledWith('error', expect.any(Function));
|
||||||
|
expect(mockedConnectionOn).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Singleton", () => {
|
it('should only attempt to connect once when called multiple times (singleton pattern)', async () => {
|
||||||
it("should try to connect once, even if called multiple times", async () => {
|
mockedMongooseConnect.mockResolvedValue(undefined as any);
|
||||||
mockedMongooseConnect.mockResolvedValue(undefined as any);
|
|
||||||
const { connectToDatabase } = await import(MODULE_PATH);
|
|
||||||
|
|
||||||
const promise1 = connectToDatabase();
|
// Rufe die Funktion mehrmals parallel auf
|
||||||
const promise2 = connectToDatabase();
|
const promise1 = connectToDatabase(TEST_DB_NAME, TEST_DB_CONN_STRING);
|
||||||
|
const promise2 = connectToDatabase(TEST_DB_NAME, TEST_DB_CONN_STRING);
|
||||||
|
|
||||||
await Promise.all([promise1, promise2]);
|
await Promise.all([promise1, promise2]);
|
||||||
|
|
||||||
expect(mockedMongooseConnect).toHaveBeenCalledTimes(1);
|
expect(mockedMongooseConnect).toHaveBeenCalledOnce();
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Retry Logic", () => {
|
describe('Retry Logic', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
});
|
});
|
||||||
@@ -112,18 +86,19 @@ describe("database.service", () => {
|
|||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should retry after 5 seconds when first time fails", async () => {
|
it('should retry connecting after a 5-second delay if the first attempt fails', async () => {
|
||||||
const connectionError = new Error("DB not ready");
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
const connectionError = new Error('Database unavailable');
|
||||||
|
|
||||||
mockedMongooseConnect
|
mockedMongooseConnect
|
||||||
.mockRejectedValueOnce(connectionError)
|
.mockRejectedValueOnce(connectionError)
|
||||||
.mockResolvedValueOnce(undefined as any);
|
.mockResolvedValueOnce(undefined as any);
|
||||||
|
|
||||||
const { connectToDatabase } = await import(MODULE_PATH);
|
const connectionPromise = connectToDatabase(TEST_DB_NAME, TEST_DB_CONN_STRING);
|
||||||
const connectionPromise = connectToDatabase();
|
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(1);
|
await vi.runAllTicks();
|
||||||
|
|
||||||
expect(mockedMongooseConnect).toHaveBeenCalledTimes(1);
|
expect(mockedMongooseConnect).toHaveBeenCalledOnce();
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to connect to MongoDB. Retrying in 5 seconds...", connectionError);
|
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to connect to MongoDB. Retrying in 5 seconds...", connectionError);
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(5000);
|
await vi.advanceTimersByTimeAsync(5000);
|
||||||
@@ -131,32 +106,31 @@ describe("database.service", () => {
|
|||||||
expect(mockedMongooseConnect).toHaveBeenCalledTimes(2);
|
expect(mockedMongooseConnect).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
await expect(connectionPromise).resolves.toBeUndefined();
|
await expect(connectionPromise).resolves.toBeUndefined();
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("disconnectFromDatabase", () => {
|
describe('disconnectFromDatabase', () => {
|
||||||
it("should call mongoose.disconnect, when connection is established", async () => {
|
it('should call mongoose.disconnect if the connection is established', async () => {
|
||||||
mockedMongooseConnect.mockResolvedValue(undefined as any);
|
mockedMongooseConnect.mockResolvedValue(undefined as any);
|
||||||
mockedMongooseDisconnect.mockResolvedValue(undefined as any);
|
mockedMongooseDisconnect.mockResolvedValue(undefined as any);
|
||||||
const { connectToDatabase, disconnectFromDatabase } = await import(MODULE_PATH);
|
|
||||||
|
|
||||||
await connectToDatabase();
|
await connectToDatabase(TEST_DB_NAME, TEST_DB_CONN_STRING);
|
||||||
|
|
||||||
const connectedCallback = mockedConnectionOn.mock.calls.find(call => call[0] === 'connected')?.[1];
|
const connectedCallback = mockedConnectionOn.mock.calls.find(call => call[0] === 'connected')?.[1];
|
||||||
if (connectedCallback) {
|
if (typeof connectedCallback === 'function') {
|
||||||
connectedCallback();
|
connectedCallback();
|
||||||
|
} else {
|
||||||
|
throw new Error("Connected callback was not found or is not a function");
|
||||||
}
|
}
|
||||||
|
|
||||||
await disconnectFromDatabase();
|
await disconnectFromDatabase();
|
||||||
|
|
||||||
expect(mockedMongooseDisconnect).toHaveBeenCalledTimes(1);
|
expect(mockedMongooseDisconnect).toHaveBeenCalledOnce();
|
||||||
expect(consoleLogSpy).toHaveBeenCalledWith("Disconnected from MongoDB.");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should NOT call.disconnect NICHT, when no connection is established", async () => {
|
it('should not call mongoose.disconnect if the connection was never established', async () => {
|
||||||
const { disconnectFromDatabase } = await import(MODULE_PATH);
|
|
||||||
|
|
||||||
await disconnectFromDatabase();
|
await disconnectFromDatabase();
|
||||||
|
|
||||||
expect(mockedMongooseDisconnect).not.toHaveBeenCalled();
|
expect(mockedMongooseDisconnect).not.toHaveBeenCalled();
|
||||||
|
|||||||
@@ -1,34 +1,29 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import {describe, it, expect, vi, beforeEach, afterEach, Mocked} from "vitest";
|
||||||
|
|
||||||
vi.mock("../../src/utils/jwtAuthenticator", () => {
|
|
||||||
return {
|
|
||||||
JwtAuthenticator: vi.fn().mockImplementation(() => ({
|
|
||||||
verifyToken: mockVerifyToken,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockVerifyToken = vi.fn();
|
|
||||||
|
|
||||||
import type { IncomingMessage } from "node:http";
|
import type { IncomingMessage } from "node:http";
|
||||||
import { verifyClient } from "../../src/utils/verifyClient";
|
import { verifyClient } from "../../src/utils/verifyClient";
|
||||||
|
import {JwtAuthenticator} from "../../src/utils/jwtAuthenticator";
|
||||||
|
// @ts-ignore
|
||||||
|
import {createMockJwtAuthenticator} from "../helpers/testSetup";
|
||||||
|
|
||||||
describe("verifyClient", () => {
|
describe("verifyClient", () => {
|
||||||
|
const payload = { id: "user-1", username: "hi", uuid: "1234" }
|
||||||
const cb = vi.fn();
|
const cb = vi.fn();
|
||||||
|
|
||||||
|
let mockJwtAuthenticator: Mocked<JwtAuthenticator>
|
||||||
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
||||||
|
|
||||||
function makeReq(authHeader?: string) {
|
function makeReq(authHeader?: string) {
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
if (authHeader) headers["authorization"] = authHeader;
|
if (authHeader) headers["authorization"] = authHeader;
|
||||||
|
|
||||||
// socket infos just for log
|
|
||||||
const socket: any = { remoteAddress: "127.0.0.1", remotePort: 12345 };
|
const socket: any = { remoteAddress: "127.0.0.1", remotePort: 12345 };
|
||||||
return { headers, socket } as unknown as IncomingMessage & { [k: string]: any };
|
return { headers, socket } as unknown as IncomingMessage & { [k: string]: any };
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cb.mockReset();
|
cb.mockReset();
|
||||||
mockVerifyToken.mockReset();
|
mockJwtAuthenticator = createMockJwtAuthenticator() as any;
|
||||||
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,20 +33,20 @@ describe("verifyClient", () => {
|
|||||||
|
|
||||||
it("accepts connections with valid token and sets payload", () => {
|
it("accepts connections with valid token and sets payload", () => {
|
||||||
const req = makeReq("Bearer valid.jwt");
|
const req = makeReq("Bearer valid.jwt");
|
||||||
mockVerifyToken.mockReturnValue({ sub: "user-1" });
|
mockJwtAuthenticator.verifyToken.mockReturnValue(payload);
|
||||||
|
|
||||||
verifyClient(req, cb);
|
verifyClient(req, mockJwtAuthenticator ,cb);
|
||||||
|
|
||||||
expect(mockVerifyToken).toHaveBeenCalledWith("valid.jwt");
|
expect(mockJwtAuthenticator.verifyToken).toHaveBeenCalledWith("valid.jwt");
|
||||||
expect(cb).toHaveBeenCalledWith(true);
|
expect(cb).toHaveBeenCalledWith(true);
|
||||||
expect((req as any).payload).toEqual({ sub: "user-1" });
|
expect((req as any).payload).toEqual(payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Rejects connection if no Authorization header is set", () => {
|
it("Rejects connection if no Authorization header is set", () => {
|
||||||
const req = makeReq(undefined);
|
const req = makeReq(undefined);
|
||||||
mockVerifyToken.mockReturnValue(null);
|
mockJwtAuthenticator.verifyToken.mockReturnValue(null);
|
||||||
|
|
||||||
verifyClient(req, cb);
|
verifyClient(req, mockJwtAuthenticator, cb);
|
||||||
|
|
||||||
expect(cb).toHaveBeenCalledWith(false, 401, "Unauthorized");
|
expect(cb).toHaveBeenCalledWith(false, 401, "Unauthorized");
|
||||||
expect(consoleSpy).toHaveBeenCalled();
|
expect(consoleSpy).toHaveBeenCalled();
|
||||||
@@ -59,22 +54,22 @@ describe("verifyClient", () => {
|
|||||||
|
|
||||||
it("rejects connection, if token is invalid", () => {
|
it("rejects connection, if token is invalid", () => {
|
||||||
const req = makeReq("Bearer bad.jwt");
|
const req = makeReq("Bearer bad.jwt");
|
||||||
mockVerifyToken.mockReturnValue(null);
|
mockJwtAuthenticator.verifyToken.mockReturnValue(null);
|
||||||
|
|
||||||
verifyClient(req, cb);
|
verifyClient(req, mockJwtAuthenticator, cb);
|
||||||
|
|
||||||
expect(mockVerifyToken).toHaveBeenCalledWith("bad.jwt");
|
expect(mockJwtAuthenticator.verifyToken).toHaveBeenCalledWith("bad.jwt");
|
||||||
expect(cb).toHaveBeenCalledWith(false, 401, "Unauthorized");
|
expect(cb).toHaveBeenCalledWith(false, 401, "Unauthorized");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("extracts token correctly after 'Bearer ' prefix", () => {
|
it("extracts token correctly after 'Bearer ' prefix", () => {
|
||||||
const expectedToken = " fancy.token.with.spaces ";
|
const expectedToken = " fancy.token.with.spaces ";
|
||||||
const req = makeReq(`Bearer ${expectedToken}`);
|
const req = makeReq(`Bearer ${expectedToken}`);
|
||||||
mockVerifyToken.mockReturnValue({ ok: true });
|
mockJwtAuthenticator.verifyToken.mockReturnValue(payload);
|
||||||
|
|
||||||
verifyClient(req, cb);
|
verifyClient(req,mockJwtAuthenticator, cb);
|
||||||
|
|
||||||
expect(mockVerifyToken).toHaveBeenCalledWith(expectedToken);
|
expect(mockJwtAuthenticator.verifyToken).toHaveBeenCalledWith(expectedToken);
|
||||||
expect(cb).toHaveBeenCalledWith(true);
|
expect(cb).toHaveBeenCalledWith(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+6
-3
@@ -1,9 +1,12 @@
|
|||||||
import { defineConfig } from 'vitest/config';
|
import {defineConfig} from 'vitest/config';
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tsconfigPaths()],
|
plugins: [
|
||||||
test: {
|
tsconfigPaths({
|
||||||
|
projects: ['./tsconfig.test.json']
|
||||||
|
})
|
||||||
|
], test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
exclude: [
|
exclude: [
|
||||||
|
|||||||
Reference in New Issue
Block a user