From 5ec63a819cb5a5bc6ee0f68a09bc49b77c57f10a Mon Sep 17 00:00:00 2001 From: nulfrost Date: Sat, 19 Jul 2025 20:44:17 -0400 Subject: [PATCH 1/2] fix: address lint errors brought up by biome --- src/app.ts | 2 +- src/controllers/atproto.controller.ts | 48 ++- src/controllers/auth.controller.ts | 214 +++++------ src/controllers/moderation.controller.ts | 362 +++++++++--------- src/middleware/dev-only.middleware.ts | 16 +- src/routes/atproto.ts | 10 +- src/routes/clientMetadata.ts | 6 +- test/integration/auth/auth.routes.test.ts | 343 +++++++++-------- test/mocks/atproto.mocks.ts | 94 ++--- test/setup.ts | 82 ++-- test/unit/auth/auth.controller.test.ts | 209 +++++----- test/unit/logging/getLogs.test.ts | 145 ++++--- .../moderation/moderation.controller.test.ts | 356 ++++++++--------- test/unit/moderation/processReport.test.ts | 354 ++++++++--------- .../getModerationServicesConfig.test.ts | 147 ++++--- 15 files changed, 1207 insertions(+), 1181 deletions(-) diff --git a/src/app.ts b/src/app.ts index 3cbc616..a2f7805 100644 --- a/src/app.ts +++ b/src/app.ts @@ -30,7 +30,7 @@ app.use(morgan('dev')); app.use(express.json()); app.use(cookieParser()); -app.use((req, res, next) => { +app.use((req, _, next) => { if (process.env.NODE_ENV === 'development') { console.log(`Incoming request: ${req.method} ${req.url}`); } diff --git a/src/controllers/atproto.controller.ts b/src/controllers/atproto.controller.ts index 8cf0648..55812cc 100644 --- a/src/controllers/atproto.controller.ts +++ b/src/controllers/atproto.controller.ts @@ -1,6 +1,18 @@ import { Request, Response } from 'express'; import { AtprotoAgent } from '../repos/atproto'; +interface SearchParams { + q: string; + sort: string; + since: string; + until: string; + author: string; + mentions: string; + hashtags: string; + limit: number; + cursor: string; +} + /** * Search for posts using ATProto's searchPosts endpoint * Supports pagination via cursor parameter @@ -8,26 +20,34 @@ import { AtprotoAgent } from '../repos/atproto'; export const searchPosts = async (req: Request, res: Response): Promise => { try { const { q, sort, since, until, author, mentions, hashtags, limit, cursor } = req.query; - + // Debug logging for cursor value if (cursor) { console.log('Received cursor:', cursor, 'Type:', typeof cursor); - + // Log if we detect a numeric cursor (might be ATProto search API behavior) if (/^\d+$/.test(cursor as string)) { console.warn('Numeric cursor detected (ATProto search API behavior):', cursor); // Don't reject - let's see if ATProto accepts it } } - + if (!q || typeof q !== 'string') { res.status(400).json({ error: 'Query parameter "q" is required' }); return; } // Build search parameters - const searchParams: any = { - q: q as string, + const searchParams: SearchParams = { + q, + sort: "", + since: "", + until: "", + author: "", + mentions: "", + hashtags: "", + limit: 25, + cursor: "" }; // Add optional parameters if provided @@ -45,7 +65,10 @@ export const searchPosts = async (req: Request, res: Response): Promise => // Debug logging for response cursor console.log('ATProto response cursor:', response.data.cursor, 'Type:', typeof response.data.cursor); - const responseData: any = { + + // not sure what the correct type is for "response" but we can infer it + type ResponseData = typeof response.data + const responseData: ResponseData = { posts: response.data.posts, }; @@ -71,15 +94,22 @@ export const searchPosts = async (req: Request, res: Response): Promise => export const searchUsers = async (req: Request, res: Response): Promise => { try { const { q, limit, cursor } = req.query; - + if (!q || typeof q !== 'string') { res.status(400).json({ error: 'Query parameter "q" is required' }); return; } - const searchParams: any = { + const searchParams: SearchParams = { q: q as string, limit: limit ? parseInt(limit as string) : 25, + sort: "", + since: "", + until: "", + author: "", + mentions: "", + hashtags: "", + cursor: "" }; // Add cursor for pagination if provided @@ -103,7 +133,7 @@ export const searchUsers = async (req: Request, res: Response): Promise => export const getPosts = async (req: Request, res: Response): Promise => { try { const { uris } = req.query; - + if (!uris) { res.status(400).json({ error: 'Query parameter "uris" is required' }); return; diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 08d0eb1..31d475b 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -10,27 +10,27 @@ import { SessionPayload } from "../lib/types/session"; * Helper: Retrieve the user's Bluesky profile data by exchanging the OAuth callback parameters. */ const getUsersBlueskyProfileData = async ( - oAuthCallbackParams: URLSearchParams, + oAuthCallbackParams: URLSearchParams, ) => { - const { session } = await BlueskyOAuthClient.callback(oAuthCallbackParams); - - if (!session?.sub) { - throw new Error("Invalid session: No DID found."); - } - - try { - const response = await AtprotoAgent.getProfile({ - actor: session.sub, - }); - - if (!response.success || !response.data) { - throw new Error("Failed to fetch profile data"); - } - return response.data; - } catch (error) { - console.error("Error fetching profile data:", error); - throw new Error("Failed to fetch profile data"); - } + const { session } = await BlueskyOAuthClient.callback(oAuthCallbackParams); + + if (!session?.sub) { + throw new Error("Invalid session: No DID found."); + } + + try { + const response = await AtprotoAgent.getProfile({ + actor: session.sub, + }); + + if (!response.success || !response.data) { + throw new Error("Failed to fetch profile data"); + } + return response.data; + } catch (error) { + console.error("Error fetching profile data:", error); + throw new Error("Failed to fetch profile data"); + } }; /** @@ -38,102 +38,102 @@ const getUsersBlueskyProfileData = async ( * Expects a 'handle' query parameter and returns a JSON object containing the authorization URL. */ export const signin = async (req: Request, res: Response): Promise => { - try { - const { handle } = req.query; - if (!handle) { - res.status(400).json({ error: "Handle is required" }); - return; - } - - const url = await BlueskyOAuthClient.authorize(handle as string); - - res.json({ url: url.toString() }); - } catch (err) { - console.error("Error initiating Bluesky auth:", err); - res.status(500).json({ error: "Failed to initiate authentication" }); - } + try { + const { handle } = req.query; + if (!handle) { + res.status(400).json({ error: "Handle is required" }); + return; + } + + const url = await BlueskyOAuthClient.authorize(handle as string); + + res.json({ url: url.toString() }); + } catch (err) { + console.error("Error initiating Bluesky auth:", err); + res.status(500).json({ error: "Failed to initiate authentication" }); + } }; /** * Logs the user out by clearing the custom JWT session cookie. */ -export const logout = async (req: Request, res: Response): Promise => { - try { - res.clearCookie("session_token", { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - }); - - res.json({ success: true, message: "Logged out successfully" }); - } catch (err) { - console.error("Error in logout:", err); - res.status(500).json({ error: "Failed to log out" }); - } +export const logout = async (_: Request, res: Response): Promise => { + try { + res.clearCookie("session_token", { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + }); + + res.json({ success: true, message: "Logged out successfully" }); + } catch (err) { + console.error("Error in logout:", err); + res.status(500).json({ error: "Failed to log out" }); + } }; /** * Handles the OAuth callback from Bluesky. */ export const callback = async (req: Request, res: Response): Promise => { - try { - // 1. Obtain initial profile data from Bluesky using OAuth callback parameters. - const profileData = await getUsersBlueskyProfileData( - new URLSearchParams(req.query as Record), - ); - - // 2. Retrieve local feed permissions for the user. - const feedsResponse = await getActorFeeds(profileData.did); - const createdFeeds = feedsResponse?.feeds || []; - - // 3. Build the initial user object merging local feed roles. - const initialUser = { - ...profileData, - rolesByFeed: createdFeeds.map((feed) => ({ - role: "admin" as UserRole, - uri: feed.uri, - displayName: feed.displayName, - feed_name: feed.displayName, - })), - }; - - // 4. Upsert (save) the user profile along with feed permissions. - const upsertSuccess = await saveProfile(initialUser, createdFeeds); - if (!upsertSuccess) { - throw new Error("Failed to save profile data"); - } - - // 5. Retrieve the complete profile (including any feed role updates). - const completeProfile = await getProfile(profileData.did); - - if (!completeProfile) { - throw new Error("Failed to retrieve complete profile"); - } - - // 6. Create a session payload and sign a JWT. - const sessionPayload: SessionPayload = { - did: completeProfile.did, - handle: completeProfile.handle, - displayName: completeProfile.displayName, - rolesByFeed: initialUser.rolesByFeed || [], - }; - const secret = process.env.JWT_SECRET; - if (!secret) { - throw new Error("Missing JWT_SECRET environment variable"); - } - const token = jwt.sign(sessionPayload, process.env.JWT_SECRET!, { - expiresIn: "7d", - }); - // 7. Redirect the user back to the client. - res.redirect(`${process.env.CLIENT_URL}/oauth/callback?token=${token}`); - } catch (err) { - console.error("OAuth callback error:", err); - const errorMessage = - err instanceof Error ? err.message : "An unknown error occurred."; - res.redirect( - `${process.env.CLIENT_URL}/oauth/login?error=${encodeURIComponent( - errorMessage, - )}`, - ); - } + try { + // 1. Obtain initial profile data from Bluesky using OAuth callback parameters. + const profileData = await getUsersBlueskyProfileData( + new URLSearchParams(req.query as Record), + ); + + // 2. Retrieve local feed permissions for the user. + const feedsResponse = await getActorFeeds(profileData.did); + const createdFeeds = feedsResponse?.feeds || []; + + // 3. Build the initial user object merging local feed roles. + const initialUser = { + ...profileData, + rolesByFeed: createdFeeds.map((feed) => ({ + role: "admin" as UserRole, + uri: feed.uri, + displayName: feed.displayName, + feed_name: feed.displayName, + })), + }; + + // 4. Upsert (save) the user profile along with feed permissions. + const upsertSuccess = await saveProfile(initialUser, createdFeeds); + if (!upsertSuccess) { + throw new Error("Failed to save profile data"); + } + + // 5. Retrieve the complete profile (including any feed role updates). + const completeProfile = await getProfile(profileData.did); + + if (!completeProfile) { + throw new Error("Failed to retrieve complete profile"); + } + + // 6. Create a session payload and sign a JWT. + const sessionPayload: SessionPayload = { + did: completeProfile.did, + handle: completeProfile.handle, + displayName: completeProfile.displayName, + rolesByFeed: initialUser.rolesByFeed || [], + }; + const secret = process.env.JWT_SECRET; + if (!secret) { + throw new Error("Missing JWT_SECRET environment variable"); + } + const token = jwt.sign(sessionPayload, process.env.JWT_SECRET!, { + expiresIn: "7d", + }); + // 7. Redirect the user back to the client. + res.redirect(`${process.env.CLIENT_URL}/oauth/callback?token=${token}`); + } catch (err) { + console.error("OAuth callback error:", err); + const errorMessage = + err instanceof Error ? err.message : "An unknown error occurred."; + res.redirect( + `${process.env.CLIENT_URL}/oauth/login?error=${encodeURIComponent( + errorMessage, + )}`, + ); + } }; diff --git a/src/controllers/moderation.controller.ts b/src/controllers/moderation.controller.ts index fd1c6b1..6db334d 100644 --- a/src/controllers/moderation.controller.ts +++ b/src/controllers/moderation.controller.ts @@ -1,9 +1,9 @@ import { Request, Response } from "express"; import { - fetchReportOptions, - fetchModerationServices, - reportToBlacksky, - reportToOzone, + fetchReportOptions, + fetchModerationServices, + reportToBlacksky, + reportToOzone, } from "../repos/moderation"; import { customServiceGate } from "../repos/permissions"; @@ -11,42 +11,42 @@ import { Report } from "../lib/types/moderation"; import { createModerationLog } from "../repos/logs"; export const getReportOptions = async ( - req: Request, - res: Response, + _: Request, + res: Response, ): Promise => { - try { - const options = await fetchReportOptions(); - res.status(200).json({ options }); - return; - } catch (error) { - console.error("Error reporting post:", error); - res.status(500).json({ error: "Internal server error" }); - } + try { + const options = await fetchReportOptions(); + res.status(200).json({ options }); + return; + } catch (error) { + console.error("Error reporting post:", error); + res.status(500).json({ error: "Internal server error" }); + } }; export const getModerationServices = async ( - req: Request, - res: Response, + req: Request, + res: Response, ): Promise => { - try { - const actingUser = req.user; - if (!actingUser) { - res.status(401).json({ error: "Unauthorized: No valid session" }); - return; - } - - const { uri } = req.query; - if (!uri) { - res.status(400).json({ error: "Uri is required" }); - return; - } - const services = await fetchModerationServices(uri.toString()); - - res.status(200).json({ services }); - } catch (error) { - console.error("Error in getModerationServices:", error); - res.status(500).json({ error: "Internal server error" }); - } + try { + const actingUser = req.user; + if (!actingUser) { + res.status(401).json({ error: "Unauthorized: No valid session" }); + return; + } + + const { uri } = req.query; + if (!uri) { + res.status(400).json({ error: "Uri is required" }); + return; + } + const services = await fetchModerationServices(uri.toString()); + + res.status(200).json({ services }); + } catch (error) { + console.error("Error in getModerationServices:", error); + res.status(500).json({ error: "Internal server error" }); + } }; /** @@ -54,123 +54,123 @@ export const getModerationServices = async ( * Returns an object containing the report payload, status, and details from processing each service and logging. */ export const processReport = async ( - report: Report, - idx: number, - actingUser: { did: string }, + report: Report, + idx: number, + actingUser: { did: string }, ) => { - // Build the payload and override performed_by with actingUser.did - const payload = { - targetedPostUri: report.targetedPostUri, - reason: report.reason, - toServices: report.toServices, - targetedUserDid: report.targetedUserDid, - uri: report.uri, - feedName: report.feedName, - additionalInfo: report.additionalInfo || "", - action: report.action, // expected to be a valid ModAction - targetedPost: report.targetedPost, - targetedProfile: report.targetedProfile, - performed_by: actingUser.did, - }; - - const resultDetails: { - service: string; - result?: unknown; - error?: unknown; - }[] = []; - - // Helper to process a service - async function processService(serviceValue: string) { - if (serviceValue === "blacksky") { - // Only process Blacksky if the gate passes - const allowed = await customServiceGate("blacksky", payload.uri); - if (allowed) { - try { - const blackskyResult = await reportToBlacksky([ - { uri: payload.targetedPostUri }, - ]); - resultDetails.push({ - service: "blacksky", - result: blackskyResult, - }); - } catch (bsError: unknown) { - console.error(`Report ${idx}: Error reporting to Blacksky:`, bsError); - resultDetails.push({ - service: "blacksky", - error: - bsError instanceof Error - ? bsError.message - : "An unknown error occurred", - }); - } - } else { - console.warn(`Report ${idx}: Blacksky service gate not passed.`); - resultDetails.push({ - service: "blacksky", - error: "Service gate not passed", - }); - } - } else if (serviceValue === "ozone") { - // Process Ozone unconditionally - try { - const ozoneResult = await reportToOzone(); - resultDetails.push({ service: "ozone", result: ozoneResult }); - } catch (ozError: unknown) { - console.error(`Report ${idx}: Error reporting to Ozone:`, ozError); - resultDetails.push({ - service: "ozone", - error: - ozError instanceof Error - ? ozError.message - : "An unknown error occurred", - }); - } - } else { - // For any future services, default behavior (or add extra logic as needed) - resultDetails.push({ - service: serviceValue, - error: "Service not implemented", - }); - } - } - - // Process each requested service. - // Assume payload.toServices is an array of ModerationService objects having a "value" property. - for (const service of payload.toServices) { - await processService(service.value); - } - - // Attempt to create a moderation log entry. - try { - await createModerationLog({ - uri: payload.uri, - performed_by: actingUser.did, - action: payload.action, - target_user_did: payload.targetedUserDid, - metadata: { - reason: payload.reason, - feedName: payload.feedName, - additionalInfo: payload.additionalInfo, - targetedPost: payload.targetedPost, - targetedProfile: payload.targetedProfile, - toServices: payload.toServices, - }, - target_post_uri: payload.targetedPostUri, - }); - - resultDetails.push({ service: "log", result: "logged" }); - } catch (logError: unknown) { - console.error(`Report ${idx}: Error creating moderation log:`, logError); - resultDetails.push({ - service: "log", - error: - logError instanceof Error - ? logError.message - : "An unknown error occurred", - }); - } - - return { report: payload, status: "success", details: resultDetails }; + // Build the payload and override performed_by with actingUser.did + const payload = { + targetedPostUri: report.targetedPostUri, + reason: report.reason, + toServices: report.toServices, + targetedUserDid: report.targetedUserDid, + uri: report.uri, + feedName: report.feedName, + additionalInfo: report.additionalInfo || "", + action: report.action, // expected to be a valid ModAction + targetedPost: report.targetedPost, + targetedProfile: report.targetedProfile, + performed_by: actingUser.did, + }; + + const resultDetails: { + service: string; + result?: unknown; + error?: unknown; + }[] = []; + + // Helper to process a service + async function processService(serviceValue: string) { + if (serviceValue === "blacksky") { + // Only process Blacksky if the gate passes + const allowed = await customServiceGate("blacksky", payload.uri); + if (allowed) { + try { + const blackskyResult = await reportToBlacksky([ + { uri: payload.targetedPostUri }, + ]); + resultDetails.push({ + service: "blacksky", + result: blackskyResult, + }); + } catch (bsError: unknown) { + console.error(`Report ${idx}: Error reporting to Blacksky:`, bsError); + resultDetails.push({ + service: "blacksky", + error: + bsError instanceof Error + ? bsError.message + : "An unknown error occurred", + }); + } + } else { + console.warn(`Report ${idx}: Blacksky service gate not passed.`); + resultDetails.push({ + service: "blacksky", + error: "Service gate not passed", + }); + } + } else if (serviceValue === "ozone") { + // Process Ozone unconditionally + try { + const ozoneResult = await reportToOzone(); + resultDetails.push({ service: "ozone", result: ozoneResult }); + } catch (ozError: unknown) { + console.error(`Report ${idx}: Error reporting to Ozone:`, ozError); + resultDetails.push({ + service: "ozone", + error: + ozError instanceof Error + ? ozError.message + : "An unknown error occurred", + }); + } + } else { + // For any future services, default behavior (or add extra logic as needed) + resultDetails.push({ + service: serviceValue, + error: "Service not implemented", + }); + } + } + + // Process each requested service. + // Assume payload.toServices is an array of ModerationService objects having a "value" property. + for (const service of payload.toServices) { + await processService(service.value); + } + + // Attempt to create a moderation log entry. + try { + await createModerationLog({ + uri: payload.uri, + performed_by: actingUser.did, + action: payload.action, + target_user_did: payload.targetedUserDid, + metadata: { + reason: payload.reason, + feedName: payload.feedName, + additionalInfo: payload.additionalInfo, + targetedPost: payload.targetedPost, + targetedProfile: payload.targetedProfile, + toServices: payload.toServices, + }, + target_post_uri: payload.targetedPostUri, + }); + + resultDetails.push({ service: "log", result: "logged" }); + } catch (logError: unknown) { + console.error(`Report ${idx}: Error creating moderation log:`, logError); + resultDetails.push({ + service: "log", + error: + logError instanceof Error + ? logError.message + : "An unknown error occurred", + }); + } + + return { report: payload, status: "success", details: resultDetails }; }; /** @@ -181,35 +181,35 @@ export const processReport = async ( * - Returns a summary of processing for each report. */ export const reportModerationEvents = async ( - req: Request, - res: Response, + req: Request, + res: Response, ): Promise => { - try { - // Ensure the authenticated user is present. - const actingUser = req.user; - if (!actingUser) { - console.error("No acting user found in request."); - res.status(401).json({ error: "Unauthorized: No valid session" }); - return; - } - - // Ensure the request body is an array; if not, wrap it. - let reports = req.body; - if (!Array.isArray(reports)) { - console.warn("Request body is not an array. Wrapping in an array."); - reports = [reports]; - } - - // Process each report individually using the helper. - const summary = await Promise.all( - reports.map((report: Report, idx: number) => - processReport(report, idx, actingUser), - ), - ); - - res.json({ summary }); - } catch (error: unknown) { - console.error("Error reporting moderation events:", error); - res.status(500).json({ error: "Internal server error" }); - } + try { + // Ensure the authenticated user is present. + const actingUser = req.user; + if (!actingUser) { + console.error("No acting user found in request."); + res.status(401).json({ error: "Unauthorized: No valid session" }); + return; + } + + // Ensure the request body is an array; if not, wrap it. + let reports = req.body; + if (!Array.isArray(reports)) { + console.warn("Request body is not an array. Wrapping in an array."); + reports = [reports]; + } + + // Process each report individually using the helper. + const summary = await Promise.all( + reports.map((report: Report, idx: number) => + processReport(report, idx, actingUser), + ), + ); + + res.json({ summary }); + } catch (error: unknown) { + console.error("Error reporting moderation events:", error); + res.status(500).json({ error: "Internal server error" }); + } }; diff --git a/src/middleware/dev-only.middleware.ts b/src/middleware/dev-only.middleware.ts index 8f732b3..fe45163 100644 --- a/src/middleware/dev-only.middleware.ts +++ b/src/middleware/dev-only.middleware.ts @@ -1,13 +1,13 @@ import { Request, Response, NextFunction } from "express"; export const developmentOnly = ( - req: Request, - res: Response, - next: NextFunction, + _: Request, + res: Response, + next: NextFunction, ): void => { - if (process.env.NODE_ENV !== "development") { - res.status(404).json({ error: "Not found" }); - return; - } - next(); + if (process.env.NODE_ENV !== "development") { + res.status(404).json({ error: "Not found" }); + return; + } + next(); }; diff --git a/src/routes/atproto.ts b/src/routes/atproto.ts index 6308d01..0e0d44e 100644 --- a/src/routes/atproto.ts +++ b/src/routes/atproto.ts @@ -1,8 +1,8 @@ import express from 'express'; import { authenticateJWT } from '../middleware/auth.middleware'; -import { - searchPosts, - searchUsers, +import { + searchPosts, + searchUsers, getPosts } from '../controllers/atproto.controller'; @@ -10,9 +10,9 @@ const router = express.Router(); // Temporary test endpoint without auth for debugging (REMOVE IN PRODUCTION) if (process.env.NODE_ENV === 'development') { - router.get('/test/search/posts', (req, res, next) => { + router.get('/test/search/posts', (req, _, next) => { // Mock user for testing - replace with a real DID from your database - req.user = { + req.user = { did: 'did:plc:test123', // Replace with actual DID from your feed_permissions table handle: 'test.user' }; diff --git a/src/routes/clientMetadata.ts b/src/routes/clientMetadata.ts index cfd9dc6..820cc02 100644 --- a/src/routes/clientMetadata.ts +++ b/src/routes/clientMetadata.ts @@ -3,9 +3,9 @@ import { BLUE_SKY_CLIENT_META_DATA } from "../lib/constants/oauth-config"; const router = Router(); -router.get("/client-metadata.json", (req, res) => { - res.header("Content-Type", "application/json"); - res.json(BLUE_SKY_CLIENT_META_DATA); +router.get("/client-metadata.json", (_, res) => { + res.header("Content-Type", "application/json"); + res.json(BLUE_SKY_CLIENT_META_DATA); }); export default router; diff --git a/test/integration/auth/auth.routes.test.ts b/test/integration/auth/auth.routes.test.ts index 0b6a330..13475c6 100644 --- a/test/integration/auth/auth.routes.test.ts +++ b/test/integration/auth/auth.routes.test.ts @@ -4,195 +4,194 @@ import { setupAuthMocks } from "../../mocks/auth.mocks"; import { mockUser } from "../../fixtures/user.fixtures"; jest.mock("../../../src/repos/oauth-client", () => ({ - BlueskyOAuthClient: { - // For authorize, always return a fixed URL. - - authorize: jest.fn(async () => { - return new URL("http://example.com"); - }), - // For callback, expect URLSearchParams; return a valid session for valid params; otherwise, throw error. - callback: jest.fn(async (params: URLSearchParams) => { - const code = params.get("code"); - const state = params.get("state"); - if (code === "validCode" && state === "validState") { - return { session: { sub: mockUser.did } }; - } - throw new Error("Test error"); - }), - }, + BlueskyOAuthClient: { + // For authorize, always return a fixed URL. + + authorize: jest.fn(async () => { + return new URL("http://example.com"); + }), + // For callback, expect URLSearchParams; return a valid session for valid params; otherwise, throw error. + callback: jest.fn(async (params: URLSearchParams) => { + const code = params.get("code"); + const state = params.get("state"); + if (code === "validCode" && state === "validState") { + return { session: { sub: mockUser.did } }; + } + throw new Error("Test error"); + }), + }, })); jest.mock("../../../src/repos/profile", () => ({ - getProfile: jest.fn(async () => { - // If GET_PROFILE_FAIL is set, simulate failure by returning null. - return process.env.GET_PROFILE_FAIL === "true" ? null : mockUser; - }), - saveProfile: jest.fn(async () => { - // If SAVE_PROFILE_FAIL is set, simulate failure. - return process.env.SAVE_PROFILE_FAIL === "true" ? false : true; - }), + getProfile: jest.fn(async () => { + // If GET_PROFILE_FAIL is set, simulate failure by returning null. + return process.env.GET_PROFILE_FAIL === "true" ? null : mockUser; + }), + saveProfile: jest.fn(async () => { + // If SAVE_PROFILE_FAIL is set, simulate failure. + return process.env.SAVE_PROFILE_FAIL === "true" ? false : true; + }), })); setupAuthMocks(); jest.mock("../../../src/repos/atproto", () => ({ - AtprotoAgent: { - // Return a fixed profile based on the actor. - getProfile: jest.fn(async () => { - return { - success: true, - data: mockUser, - }; - }), - }, - // Return a fixed set of feeds. - getActorFeeds: jest.fn(async () => ({ - feeds: [ - { uri: "feed:1", displayName: "Feed One", creator: { did: "admin1" } }, - ], - })), + AtprotoAgent: { + // Return a fixed profile based on the actor. + getProfile: jest.fn(async () => { + return { + success: true, + data: mockUser, + }; + }), + }, + // Return a fixed set of feeds. + getActorFeeds: jest.fn(async () => ({ + feeds: [ + { uri: "feed:1", displayName: "Feed One", creator: { did: "admin1" } }, + ], + })), })); beforeEach(() => { - jest.clearAllMocks(); + jest.clearAllMocks(); }); afterAll(() => { - jest.restoreAllMocks(); + jest.restoreAllMocks(); }); describe("Auth Routes Integration", () => { - describe("GET /auth/signin", () => { - it("should return 400 when handle is missing", async () => { - const res = await request(app).get("/auth/signin"); - expect(res.status).toBe(400); - expect(res.body).toEqual({ error: "Handle is required" }); - }); - - it("should return an authorization URL when handle is provided", async () => { - const res = await request(app) - .get("/auth/signin") - .query({ handle: mockUser.handle }); - expect(res.status).toBe(200); - expect(typeof res.body.url).toBe("string"); - expect(res.body.url).toMatch(/^https?:\/\//); - }); - }); - - describe("GET /auth/callback", () => { - beforeAll(() => { - process.env.JWT_SECRET = "secret"; - process.env.CLIENT_URL = "http://client.com"; - }); - - it("should process a valid callback and redirect with a token", async () => { - const res = await request(app) - .get("/auth/callback") - .query({ code: "validCode", state: "validState" }); - expect(res.status).toBe(302); - expect(res.headers.location).toContain(process.env.CLIENT_URL); - expect(res.headers.location).toMatch(/token=.+/); - }); - - it("should redirect to login with an error on callback failure", async () => { - // Suppress expected error logs - const consoleErrorSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); - - const res = await request(app) - .get("/auth/callback") - .query({ code: "fail", state: "any" }); - expect(res.status).toBe(302); - expect(res.headers.location).toContain("/oauth/login"); - // Decode the error parameter for comparison. - const url = new URL(res.headers.location); - const errorParam = url.searchParams.get("error") || ""; - expect(decodeURIComponent(errorParam)).toMatch(/Test error/); - - consoleErrorSpy.mockRestore(); - }); - }); - - describe("POST /auth/logout", () => { - it("should clear the session token and return a success message", async () => { - const res = await request(app).post("/auth/logout"); - expect(res.status).toBe(200); - expect(res.body).toEqual({ - success: true, - message: "Logged out successfully", - }); - }); - }); + describe("GET /auth/signin", () => { + it("should return 400 when handle is missing", async () => { + const res = await request(app).get("/auth/signin"); + expect(res.status).toBe(400); + expect(res.body).toEqual({ error: "Handle is required" }); + }); + + it("should return an authorization URL when handle is provided", async () => { + const res = await request(app) + .get("/auth/signin") + .query({ handle: mockUser.handle }); + expect(res.status).toBe(200); + expect(typeof res.body.url).toBe("string"); + expect(res.body.url).toMatch(/^https?:\/\//); + }); + }); + + describe("GET /auth/callback", () => { + beforeAll(() => { + process.env.JWT_SECRET = "secret"; + process.env.CLIENT_URL = "http://client.com"; + }); + + it("should process a valid callback and redirect with a token", async () => { + const res = await request(app) + .get("/auth/callback") + .query({ code: "validCode", state: "validState" }); + expect(res.status).toBe(302); + expect(res.headers.location).toContain(process.env.CLIENT_URL); + expect(res.headers.location).toMatch(/token=.+/); + }); + + it("should redirect to login with an error on callback failure", async () => { + // Suppress expected error logs + const consoleErrorSpy = jest + .spyOn(console, "error") + + const res = await request(app) + .get("/auth/callback") + .query({ code: "fail", state: "any" }); + expect(res.status).toBe(302); + expect(res.headers.location).toContain("/oauth/login"); + // Decode the error parameter for comparison. + const url = new URL(res.headers.location); + const errorParam = url.searchParams.get("error") || ""; + expect(decodeURIComponent(errorParam)).toMatch(/Test error/); + + consoleErrorSpy.mockRestore(); + }); + }); + + describe("POST /auth/logout", () => { + it("should clear the session token and return a success message", async () => { + const res = await request(app).post("/auth/logout"); + expect(res.status).toBe(200); + expect(res.body).toEqual({ + success: true, + message: "Logged out successfully", + }); + }); + }); }); describe("Auth Routes Integration - Callback Failure Scenarios", () => { - beforeAll(() => { - process.env.JWT_SECRET = "secret"; - process.env.CLIENT_URL = "http://client.com"; - }); - let consoleErrorSpy: jest.SpyInstance; - beforeEach(() => { - consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); - }); - - afterEach(() => { - // Remove any failure flags after each test. - delete process.env.SAVE_PROFILE_FAIL; - delete process.env.GET_PROFILE_FAIL; - consoleErrorSpy.mockRestore(); - jest.restoreAllMocks(); - }); - - it("should redirect to login if saving profile data fails", async () => { - process.env.SAVE_PROFILE_FAIL = "true"; - - const res = await request(app) - .get("/auth/callback") - .query({ code: "validCode", state: "validState" }); - - expect(res.status).toBe(302); - expect(res.headers.location).toContain("/oauth/login"); - const url = new URL(res.headers.location); - const errorParam = url.searchParams.get("error") || ""; - expect(decodeURIComponent(errorParam)).toMatch( - /Failed to save profile data/, - ); - }); - - it("should redirect to login if complete profile is not retrieved", async () => { - // Simulate getProfile failure. - process.env.GET_PROFILE_FAIL = "true"; - - const res = await request(app) - .get("/auth/callback") - .query({ code: "validCode", state: "validState" }); - - expect(res.status).toBe(302); - expect(res.headers.location).toContain("/oauth/login"); - const url = new URL(res.headers.location); - const errorParam = url.searchParams.get("error") || ""; - expect(decodeURIComponent(errorParam)).toMatch( - /Failed to retrieve complete profile/, - ); - }); - - it("should redirect to login if JWT_SECRET is missing", async () => { - // Ensure other steps succeed. - const originalSecret = process.env.JWT_SECRET; - delete process.env.JWT_SECRET; - - const res = await request(app) - .get("/auth/callback") - .query({ code: "validCode", state: "validState" }); - - expect(res.status).toBe(302); - expect(res.headers.location).toContain("/oauth/login"); - const url = new URL(res.headers.location); - const errorParam = url.searchParams.get("error") || ""; - expect(decodeURIComponent(errorParam)).toMatch( - /Missing JWT_SECRET environment variable/, - ); - - process.env.JWT_SECRET = originalSecret; - }); + beforeAll(() => { + process.env.JWT_SECRET = "secret"; + process.env.CLIENT_URL = "http://client.com"; + }); + let consoleErrorSpy: jest.SpyInstance; + beforeEach(() => { + consoleErrorSpy = jest.spyOn(console, "error") + }); + + afterEach(() => { + // Remove any failure flags after each test. + delete process.env.SAVE_PROFILE_FAIL; + delete process.env.GET_PROFILE_FAIL; + consoleErrorSpy.mockRestore(); + jest.restoreAllMocks(); + }); + + it("should redirect to login if saving profile data fails", async () => { + process.env.SAVE_PROFILE_FAIL = "true"; + + const res = await request(app) + .get("/auth/callback") + .query({ code: "validCode", state: "validState" }); + + expect(res.status).toBe(302); + expect(res.headers.location).toContain("/oauth/login"); + const url = new URL(res.headers.location); + const errorParam = url.searchParams.get("error") || ""; + expect(decodeURIComponent(errorParam)).toMatch( + /Failed to save profile data/, + ); + }); + + it("should redirect to login if complete profile is not retrieved", async () => { + // Simulate getProfile failure. + process.env.GET_PROFILE_FAIL = "true"; + + const res = await request(app) + .get("/auth/callback") + .query({ code: "validCode", state: "validState" }); + + expect(res.status).toBe(302); + expect(res.headers.location).toContain("/oauth/login"); + const url = new URL(res.headers.location); + const errorParam = url.searchParams.get("error") || ""; + expect(decodeURIComponent(errorParam)).toMatch( + /Failed to retrieve complete profile/, + ); + }); + + it("should redirect to login if JWT_SECRET is missing", async () => { + // Ensure other steps succeed. + const originalSecret = process.env.JWT_SECRET; + delete process.env.JWT_SECRET; + + const res = await request(app) + .get("/auth/callback") + .query({ code: "validCode", state: "validState" }); + + expect(res.status).toBe(302); + expect(res.headers.location).toContain("/oauth/login"); + const url = new URL(res.headers.location); + const errorParam = url.searchParams.get("error") || ""; + expect(decodeURIComponent(errorParam)).toMatch( + /Missing JWT_SECRET environment variable/, + ); + + process.env.JWT_SECRET = originalSecret; + }); }); diff --git a/test/mocks/atproto.mocks.ts b/test/mocks/atproto.mocks.ts index 0724a7a..d067be7 100644 --- a/test/mocks/atproto.mocks.ts +++ b/test/mocks/atproto.mocks.ts @@ -1,69 +1,69 @@ import { mockFeedsByRole } from "../fixtures/feed.fixtures"; export const mockAtprotoAgent = { - getProfile: jest.fn().mockResolvedValue({ - success: true, - data: { - did: "did:example:123", - handle: "testHandle", - displayName: "Test User", - }, - }), + getProfile: jest.fn().mockResolvedValue({ + success: true, + data: { + did: "did:example:123", + handle: "testHandle", + displayName: "Test User", + }, + }), }; export const mockFeedGenerator = { - uri: "feed:1", - displayName: "Test Feed", - description: "Test Description", - did: "did:feed:1", + uri: "feed:1", + displayName: "Test Feed", + description: "Test Description", + did: "did:feed:1", }; export const mockActorFeeds = { - feeds: [ - { - uri: "feed:1", - displayName: "BlueSky Admin Feed 1", - description: "Admin description 1", - did: "did:feed:1", - creator: { did: "admin1" }, - }, - { - uri: "feed:2", - displayName: "Admin Feed 2", - description: "Admin description 2", - did: "did:feed:2", - creator: { did: "admin2" }, - }, - ], + feeds: [ + { + uri: "feed:1", + displayName: "BlueSky Admin Feed 1", + description: "Admin description 1", + did: "did:feed:1", + creator: { did: "admin1" }, + }, + { + uri: "feed:2", + displayName: "Admin Feed 2", + description: "Admin description 2", + did: "did:feed:2", + creator: { did: "admin2" }, + }, + ], }; -export const mockGetFeedsByRole = jest.fn().mockImplementation((did, role) => { - if (role === "admin") { - return Promise.resolve(mockFeedsByRole.admin); - } - if (role === "mod") { - return Promise.resolve(mockFeedsByRole.mod); - } - return Promise.resolve(mockFeedsByRole.user); +export const mockGetFeedsByRole = jest.fn().mockImplementation((_, role) => { + if (role === "admin") { + return Promise.resolve(mockFeedsByRole.admin); + } + if (role === "mod") { + return Promise.resolve(mockFeedsByRole.mod); + } + return Promise.resolve(mockFeedsByRole.user); }); export const mockGetActorFeeds = jest.fn().mockResolvedValue(mockActorFeeds); export const mockGetFeedGenerator = jest - .fn() - .mockResolvedValue(mockFeedGenerator); + .fn() + .mockResolvedValue(mockFeedGenerator); // Helper to setup all mocks at once export const setupAtprotoMocks = () => { - jest - .spyOn(require("../../src/repos/feed"), "getFeedsByRole") - .mockImplementation(mockGetFeedsByRole); + jest + .spyOn(require("../../src/repos/feed"), "getFeedsByRole") + .mockImplementation(mockGetFeedsByRole); - jest - .spyOn(require("../../src/repos/atproto"), "getActorFeeds") - .mockImplementation(mockGetActorFeeds); + jest + .spyOn(require("../../src/repos/atproto"), "getActorFeeds") + .mockImplementation(mockGetActorFeeds); - jest - .spyOn(require("../../src/repos/atproto"), "getFeedGenerator") - .mockImplementation(mockGetFeedGenerator); + jest + .spyOn(require("../../src/repos/atproto"), "getFeedGenerator") + .mockImplementation(mockGetFeedGenerator); }; diff --git a/test/setup.ts b/test/setup.ts index 18b2b70..f72f0c7 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,53 +1,53 @@ // Stub external modules for integration tests. jest.mock("../src/repos/atproto", () => ({ - AtprotoAgent: { - // Return a fixed profile based on the actor. - getProfile: jest.fn(async (params: { actor: string }) => { - return { - success: true, - data: { - did: params.actor, - handle: "testHandle", - displayName: "Test User", - }, - }; - }), - }, - // Return a fixed set of feeds. - getActorFeeds: jest.fn(async () => ({ - feeds: [ - { uri: "feed:1", displayName: "Feed One", creator: { did: "admin1" } }, - ], - })), - getFeedGenerator: jest.fn(async (feed: string) => { - // Here you define responses based on the feed URI. - if (feed === "feed:1") { - return { - displayName: "BlueSky Feed One", - description: "Updated Desc 1", - did: "did:example:456", - }; - } else if (feed === "feed:2") { - return { - displayName: "BlueSky Feed Two", - description: "Updated Desc 2", - did: "did:example:456", - }; - } - // For any other feed, simulate a not found scenario. - throw new Error("Feed not found"); - }), + AtprotoAgent: { + // Return a fixed profile based on the actor. + getProfile: jest.fn(async (params: { actor: string }) => { + return { + success: true, + data: { + did: params.actor, + handle: "testHandle", + displayName: "Test User", + }, + }; + }), + }, + // Return a fixed set of feeds. + getActorFeeds: jest.fn(async () => ({ + feeds: [ + { uri: "feed:1", displayName: "Feed One", creator: { did: "admin1" } }, + ], + })), + getFeedGenerator: jest.fn(async (feed: string) => { + // Here you define responses based on the feed URI. + if (feed === "feed:1") { + return { + displayName: "BlueSky Feed One", + description: "Updated Desc 1", + did: "did:example:456", + }; + } else if (feed === "feed:2") { + return { + displayName: "BlueSky Feed Two", + description: "Updated Desc 2", + did: "did:example:456", + }; + } + // For any other feed, simulate a not found scenario. + throw new Error("Feed not found"); + }), })); // Optionally, suppress known warnings. -jest.spyOn(console, "warn").mockImplementation(() => {}); -jest.spyOn(console, "error").mockImplementation(() => {}); +jest.spyOn(console, "warn") +jest.spyOn(console, "error") beforeEach(() => { - jest.clearAllMocks(); + jest.clearAllMocks(); }); afterAll(() => { - jest.restoreAllMocks(); + jest.restoreAllMocks(); }); diff --git a/test/unit/auth/auth.controller.test.ts b/test/unit/auth/auth.controller.test.ts index d442c80..a8253ca 100644 --- a/test/unit/auth/auth.controller.test.ts +++ b/test/unit/auth/auth.controller.test.ts @@ -3,117 +3,116 @@ import { mockBlueskyOAuthClient, setupAuthMocks } from "../../mocks/auth.mocks"; setupAuthMocks(); import { - signin, - logout, - callback, + signin, + logout, + callback, } from "../../../src/controllers/auth.controller"; import { - createMockRequest, - createMockResponse, + createMockRequest, + createMockResponse, } from "../../mocks/express.mock"; import { mockUser } from "../../fixtures/user.fixtures"; import { - mockOauthResponseParams, - mockToken, + mockOauthResponseParams, + mockToken, } from "../../fixtures/auth.fixtures"; describe("Auth Controller", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - afterAll(() => { - jest.restoreAllMocks(); - }); - - describe("signin", () => { - it("should return 400 if handle is missing", async () => { - const req = createMockRequest(); - const res = createMockResponse(); - - await signin(req, res); - - expect(res.status).toHaveBeenCalledWith(400); - expect(res.json).toHaveBeenCalledWith({ error: "Handle is required" }); - }); - - it("should call BlueskyOAuthClient.authorize and return the URL if handle is provided", async () => { - const req = createMockRequest({ query: { handle: mockUser.handle } }); - const res = createMockResponse(); - const fakeUrl = new URL("http://example.com"); - - mockBlueskyOAuthClient.authorize.mockResolvedValueOnce(fakeUrl); - - await signin(req, res); - - expect(mockBlueskyOAuthClient.authorize).toHaveBeenCalledWith( - mockUser.handle, - ); - expect(res.json).toHaveBeenCalledWith({ url: fakeUrl.toString() }); - }); - }); - - describe("logout", () => { - it("should clear the session_token cookie and return a success message", async () => { - const req = createMockRequest(); - const res = createMockResponse(); - process.env.NODE_ENV = "development"; - - await logout(req, res); - - expect(res.clearCookie).toHaveBeenCalledWith("session_token", { - httpOnly: true, - secure: false, - sameSite: "strict", - }); - expect(res.json).toHaveBeenCalledWith({ - success: true, - message: "Logged out successfully", - }); - }); - }); - - describe("callback", () => { - beforeEach(() => { - process.env.JWT_SECRET = "secret"; - process.env.CLIENT_URL = "http://client.com"; - }); - - it("should process a valid callback and redirect with a token", async () => { - const req = createMockRequest({ query: mockOauthResponseParams }); - const res = createMockResponse(); - - const fakeSession = { session: { sub: mockUser.did } }; - mockBlueskyOAuthClient.callback.mockResolvedValueOnce(fakeSession); - - await callback(req, res); - - expect(res.redirect).toHaveBeenCalled(); - const redirectUrl = (res.redirect as jest.Mock).mock.calls[0][0]; - expect(redirectUrl).toContain(process.env.CLIENT_URL); - expect(redirectUrl).toContain(`token=${mockToken}`); - }); - - it("should redirect to login with error on failure", async () => { - const req = createMockRequest({ query: mockOauthResponseParams }); - const res = createMockResponse(); - - const consoleSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); - - mockBlueskyOAuthClient.callback.mockRejectedValueOnce( - new Error("Test error"), - ); - - await callback(req, res); - - expect(res.redirect).toHaveBeenCalled(); - const redirectUrl = (res.redirect as jest.Mock).mock.calls[0][0]; - expect(redirectUrl).toContain("http://client.com/oauth/login"); - expect(redirectUrl).toContain(encodeURIComponent("Test error")); - - consoleSpy.mockRestore(); - }); - }); + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + describe("signin", () => { + it("should return 400 if handle is missing", async () => { + const req = createMockRequest(); + const res = createMockResponse(); + + await signin(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: "Handle is required" }); + }); + + it("should call BlueskyOAuthClient.authorize and return the URL if handle is provided", async () => { + const req = createMockRequest({ query: { handle: mockUser.handle } }); + const res = createMockResponse(); + const fakeUrl = new URL("http://example.com"); + + mockBlueskyOAuthClient.authorize.mockResolvedValueOnce(fakeUrl); + + await signin(req, res); + + expect(mockBlueskyOAuthClient.authorize).toHaveBeenCalledWith( + mockUser.handle, + ); + expect(res.json).toHaveBeenCalledWith({ url: fakeUrl.toString() }); + }); + }); + + describe("logout", () => { + it("should clear the session_token cookie and return a success message", async () => { + const req = createMockRequest(); + const res = createMockResponse(); + process.env.NODE_ENV = "development"; + + await logout(req, res); + + expect(res.clearCookie).toHaveBeenCalledWith("session_token", { + httpOnly: true, + secure: false, + sameSite: "strict", + }); + expect(res.json).toHaveBeenCalledWith({ + success: true, + message: "Logged out successfully", + }); + }); + }); + + describe("callback", () => { + beforeEach(() => { + process.env.JWT_SECRET = "secret"; + process.env.CLIENT_URL = "http://client.com"; + }); + + it("should process a valid callback and redirect with a token", async () => { + const req = createMockRequest({ query: mockOauthResponseParams }); + const res = createMockResponse(); + + const fakeSession = { session: { sub: mockUser.did } }; + mockBlueskyOAuthClient.callback.mockResolvedValueOnce(fakeSession); + + await callback(req, res); + + expect(res.redirect).toHaveBeenCalled(); + const redirectUrl = (res.redirect as jest.Mock).mock.calls[0][0]; + expect(redirectUrl).toContain(process.env.CLIENT_URL); + expect(redirectUrl).toContain(`token=${mockToken}`); + }); + + it("should redirect to login with error on failure", async () => { + const req = createMockRequest({ query: mockOauthResponseParams }); + const res = createMockResponse(); + + const consoleSpy = jest + .spyOn(console, "error") + + mockBlueskyOAuthClient.callback.mockRejectedValueOnce( + new Error("Test error"), + ); + + await callback(req, res); + + expect(res.redirect).toHaveBeenCalled(); + const redirectUrl = (res.redirect as jest.Mock).mock.calls[0][0]; + expect(redirectUrl).toContain("http://client.com/oauth/login"); + expect(redirectUrl).toContain(encodeURIComponent("Test error")); + + consoleSpy.mockRestore(); + }); + }); }); diff --git a/test/unit/logging/getLogs.test.ts b/test/unit/logging/getLogs.test.ts index 3711240..4448c80 100644 --- a/test/unit/logging/getLogs.test.ts +++ b/test/unit/logging/getLogs.test.ts @@ -1,8 +1,8 @@ import { tracker, setupDbMocks, cleanupDbMocks } from "../../mocks/db.mocks"; import { - mockCacheGet, - mockCacheSet, - setupNodeCacheMocks, + mockCacheGet, + mockCacheSet, + setupNodeCacheMocks, } from "../../mocks/cache.mocks"; import { mockModerationServices } from "../../fixtures/moderation.fixtures"; @@ -14,74 +14,73 @@ setupDbMocks(); import { getModerationServicesConfig } from "../../../src/repos/moderation"; describe("getModerationServicesConfig", () => { - // Keep track of query count with our own counter - let queryCount = 0; - - beforeEach(() => { - jest.clearAllMocks(); - queryCount = 0; - tracker.install(); - }); - - afterEach(() => { - tracker.uninstall(); - }); - - afterAll(() => { - cleanupDbMocks(); - }); - - it("should return cached value if available", async () => { - // Setup cache to return a value - mockCacheGet.mockReturnValue(mockModerationServices); - - const result = await getModerationServicesConfig(); - - expect(result).toEqual(mockModerationServices); - // We expect zero queries when cache is hit - expect(queryCount).toBe(0); - }); - - it("should query the database and cache the result if not in cache", async () => { - // Setup cache to not return a value - mockCacheGet.mockReturnValue(null); - - // Setup database to return sample data - tracker.on("query", function (query) { - queryCount++; - expect(query.method).toBe("select"); - query.response(mockModerationServices); - }); - - const result = await getModerationServicesConfig(); - - expect(result).toEqual(mockModerationServices); - expect(queryCount).toBe(1); - expect(mockCacheSet).toHaveBeenCalledWith( - "moderationServices", - mockModerationServices, - ); - }); - - it("should throw an error if the database query fails", async () => { - // Setup cache to not return a value - mockCacheGet.mockReturnValue(null); - - // Setup database to throw an error - const testError = new Error("DB Error"); - tracker.on("query", function (query) { - queryCount++; - query.reject(testError); - }); - - const consoleSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); - - await expect(getModerationServicesConfig()).rejects.toThrow("DB Error"); - expect(consoleSpy).toHaveBeenCalled(); - expect(queryCount).toBe(1); - - consoleSpy.mockRestore(); - }); + // Keep track of query count with our own counter + let queryCount = 0; + + beforeEach(() => { + jest.clearAllMocks(); + queryCount = 0; + tracker.install(); + }); + + afterEach(() => { + tracker.uninstall(); + }); + + afterAll(() => { + cleanupDbMocks(); + }); + + it("should return cached value if available", async () => { + // Setup cache to return a value + mockCacheGet.mockReturnValue(mockModerationServices); + + const result = await getModerationServicesConfig(); + + expect(result).toEqual(mockModerationServices); + // We expect zero queries when cache is hit + expect(queryCount).toBe(0); + }); + + it("should query the database and cache the result if not in cache", async () => { + // Setup cache to not return a value + mockCacheGet.mockReturnValue(null); + + // Setup database to return sample data + tracker.on("query", function (query) { + queryCount++; + expect(query.method).toBe("select"); + query.response(mockModerationServices); + }); + + const result = await getModerationServicesConfig(); + + expect(result).toEqual(mockModerationServices); + expect(queryCount).toBe(1); + expect(mockCacheSet).toHaveBeenCalledWith( + "moderationServices", + mockModerationServices, + ); + }); + + it("should throw an error if the database query fails", async () => { + // Setup cache to not return a value + mockCacheGet.mockReturnValue(null); + + // Setup database to throw an error + const testError = new Error("DB Error"); + tracker.on("query", function (query) { + queryCount++; + query.reject(testError); + }); + + const consoleSpy = jest + .spyOn(console, "error") + + await expect(getModerationServicesConfig()).rejects.toThrow("DB Error"); + expect(consoleSpy).toHaveBeenCalled(); + expect(queryCount).toBe(1); + + consoleSpy.mockRestore(); + }); }); diff --git a/test/unit/moderation/moderation.controller.test.ts b/test/unit/moderation/moderation.controller.test.ts index 71e779c..0407200 100644 --- a/test/unit/moderation/moderation.controller.test.ts +++ b/test/unit/moderation/moderation.controller.test.ts @@ -1,21 +1,21 @@ import { Request, Response } from "express"; import { - createMockRequest, - createMockResponse, + createMockRequest, + createMockResponse, } from "../../mocks/express.mock"; import { - getReportOptions, - getModerationServices, - reportModerationEvents, + getReportOptions, + getModerationServices, + reportModerationEvents, } from "../../../src/controllers/moderation.controller"; // Import fixtures import { - mockModerationServices, - mockReportOptions, - mockReport, - mockReports, + mockModerationServices, + mockReportOptions, + mockReport, + mockReports, } from "../../fixtures/moderation.fixtures"; // Import repository modules for mocking @@ -25,173 +25,173 @@ import * as logs from "../../../src/repos/logs"; import { mockUser } from "../../fixtures/user.fixtures"; describe("Moderation Controller", () => { - let req: Request; - let res: Response; - - beforeEach(() => { - jest.clearAllMocks(); - res = createMockResponse(); - }); - - describe("getReportOptions", () => { - it("should return available report options", async () => { - // Mock the repository function - jest - .spyOn(moderation, "fetchReportOptions") - .mockResolvedValue(mockReportOptions); - - await getReportOptions(req, res); - - expect(moderation.fetchReportOptions).toHaveBeenCalled(); - expect(res.status).toHaveBeenCalledWith(200); - expect(res.json).toHaveBeenCalledWith({ options: mockReportOptions }); - }); - - it("should handle errors gracefully", async () => { - // Mock an error response - jest - .spyOn(moderation, "fetchReportOptions") - .mockRejectedValue(new Error("Database error")); - jest.spyOn(console, "error").mockImplementation(() => {}); - - await getReportOptions(req, res); - - expect(console.error).toHaveBeenCalled(); - expect(res.status).toHaveBeenCalledWith(500); - expect(res.json).toHaveBeenCalledWith({ error: "Internal server error" }); - }); - }); - - describe("getModerationServices", () => { - it("should return available moderation services for a feed", async () => { - req = createMockRequest({ - user: { did: mockUser.did, handle: mockUser.handle }, - query: { uri: "feed:1" }, - }); - - jest - .spyOn(moderation, "fetchModerationServices") - .mockResolvedValue(mockModerationServices); - - await getModerationServices(req, res); - - expect(moderation.fetchModerationServices).toHaveBeenCalledWith("feed:1"); - expect(res.status).toHaveBeenCalledWith(200); - expect(res.json).toHaveBeenCalledWith({ - services: mockModerationServices, - }); - }); - - it("should return 401 if no user is authenticated", async () => { - req = createMockRequest({ - user: undefined, - query: { uri: "feed:1" }, - }); - - await getModerationServices(req, res); - - expect(res.status).toHaveBeenCalledWith(401); - expect(res.json).toHaveBeenCalledWith({ - error: "Unauthorized: No valid session", - }); - }); - - it("should return 400 if no feed URI is provided", async () => { - req = createMockRequest({ - user: { did: mockUser.did, handle: mockUser.handle }, - query: {}, - }); - - await getModerationServices(req, res); - - expect(res.status).toHaveBeenCalledWith(400); - expect(res.json).toHaveBeenCalledWith({ error: "Uri is required" }); - }); - }); - - describe("reportModerationEvents", () => { - beforeEach(() => { - // Set up the necessary mocks for the underlying functions - jest.spyOn(permissions, "customServiceGate").mockResolvedValue(true); - - // Mock blacksky response with appropriate structure - const blackskyResponse = new Response(JSON.stringify({ ok: true }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - jest - .spyOn(moderation, "reportToBlacksky") - .mockResolvedValue(blackskyResponse); - - // Mock ozone response - jest.spyOn(moderation, "reportToOzone").mockResolvedValue(undefined); - - jest.spyOn(logs, "createModerationLog").mockResolvedValue(undefined); - }); - - it("should process a single report", async () => { - req = createMockRequest({ - user: { did: mockUser.did, handle: mockUser.handle }, - body: mockReport, - }); - - await reportModerationEvents(req, res); - - // Should wrap single report in array and process it - expect(res.json).toHaveBeenCalled(); - const responseData = (res.json as jest.Mock).mock.calls[0][0]; - expect(responseData).toHaveProperty("summary"); - expect(Array.isArray(responseData.summary)).toBe(true); - expect(responseData.summary.length).toBe(1); - }); - - it("should process multiple reports", async () => { - req = createMockRequest({ - user: { did: mockUser.did, handle: mockUser.handle }, - body: mockReports, - }); - - await reportModerationEvents(req, res); - - // Should process all reports - expect(res.json).toHaveBeenCalled(); - const responseData = (res.json as jest.Mock).mock.calls[0][0]; - expect(responseData).toHaveProperty("summary"); - expect(Array.isArray(responseData.summary)).toBe(true); - expect(responseData.summary.length).toBe(mockReports.length); - }); - - it("should return 401 if no user is authenticated", async () => { - req = createMockRequest({ - user: undefined, - body: mockReport, - }); - - await reportModerationEvents(req, res); - - expect(res.status).toHaveBeenCalledWith(401); - expect(res.json).toHaveBeenCalledWith({ - error: "Unauthorized: No valid session", - }); - }); - - it("should handle errors gracefully", async () => { - req = createMockRequest({ - user: { did: mockUser.did, handle: mockUser.handle }, - body: mockReport, - }); - - // Force a global error in the process - jest - .spyOn(moderation, "reportToBlacksky") - .mockRejectedValue(new Error("Unexpected error")); - jest.spyOn(console, "error").mockImplementation(() => {}); - - await reportModerationEvents(req, res); - - // It should still return a response, not crash - expect(res.json).toHaveBeenCalled(); - expect(console.error).toHaveBeenCalled(); - }); - }); + let req: Request; + let res: Response; + + beforeEach(() => { + jest.clearAllMocks(); + res = createMockResponse(); + }); + + describe("getReportOptions", () => { + it("should return available report options", async () => { + // Mock the repository function + jest + .spyOn(moderation, "fetchReportOptions") + .mockResolvedValue(mockReportOptions); + + await getReportOptions(req, res); + + expect(moderation.fetchReportOptions).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ options: mockReportOptions }); + }); + + it("should handle errors gracefully", async () => { + // Mock an error response + jest + .spyOn(moderation, "fetchReportOptions") + .mockRejectedValue(new Error("Database error")); + jest.spyOn(console, "error") + + await getReportOptions(req, res); + + expect(console.error).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ error: "Internal server error" }); + }); + }); + + describe("getModerationServices", () => { + it("should return available moderation services for a feed", async () => { + req = createMockRequest({ + user: { did: mockUser.did, handle: mockUser.handle }, + query: { uri: "feed:1" }, + }); + + jest + .spyOn(moderation, "fetchModerationServices") + .mockResolvedValue(mockModerationServices); + + await getModerationServices(req, res); + + expect(moderation.fetchModerationServices).toHaveBeenCalledWith("feed:1"); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + services: mockModerationServices, + }); + }); + + it("should return 401 if no user is authenticated", async () => { + req = createMockRequest({ + user: undefined, + query: { uri: "feed:1" }, + }); + + await getModerationServices(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ + error: "Unauthorized: No valid session", + }); + }); + + it("should return 400 if no feed URI is provided", async () => { + req = createMockRequest({ + user: { did: mockUser.did, handle: mockUser.handle }, + query: {}, + }); + + await getModerationServices(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: "Uri is required" }); + }); + }); + + describe("reportModerationEvents", () => { + beforeEach(() => { + // Set up the necessary mocks for the underlying functions + jest.spyOn(permissions, "customServiceGate").mockResolvedValue(true); + + // Mock blacksky response with appropriate structure + const blackskyResponse = new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + jest + .spyOn(moderation, "reportToBlacksky") + .mockResolvedValue(blackskyResponse); + + // Mock ozone response + jest.spyOn(moderation, "reportToOzone").mockResolvedValue(undefined); + + jest.spyOn(logs, "createModerationLog").mockResolvedValue(undefined); + }); + + it("should process a single report", async () => { + req = createMockRequest({ + user: { did: mockUser.did, handle: mockUser.handle }, + body: mockReport, + }); + + await reportModerationEvents(req, res); + + // Should wrap single report in array and process it + expect(res.json).toHaveBeenCalled(); + const responseData = (res.json as jest.Mock).mock.calls[0][0]; + expect(responseData).toHaveProperty("summary"); + expect(Array.isArray(responseData.summary)).toBe(true); + expect(responseData.summary.length).toBe(1); + }); + + it("should process multiple reports", async () => { + req = createMockRequest({ + user: { did: mockUser.did, handle: mockUser.handle }, + body: mockReports, + }); + + await reportModerationEvents(req, res); + + // Should process all reports + expect(res.json).toHaveBeenCalled(); + const responseData = (res.json as jest.Mock).mock.calls[0][0]; + expect(responseData).toHaveProperty("summary"); + expect(Array.isArray(responseData.summary)).toBe(true); + expect(responseData.summary.length).toBe(mockReports.length); + }); + + it("should return 401 if no user is authenticated", async () => { + req = createMockRequest({ + user: undefined, + body: mockReport, + }); + + await reportModerationEvents(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ + error: "Unauthorized: No valid session", + }); + }); + + it("should handle errors gracefully", async () => { + req = createMockRequest({ + user: { did: mockUser.did, handle: mockUser.handle }, + body: mockReport, + }); + + // Force a global error in the process + jest + .spyOn(moderation, "reportToBlacksky") + .mockRejectedValue(new Error("Unexpected error")); + jest.spyOn(console, "error") + + await reportModerationEvents(req, res); + + // It should still return a response, not crash + expect(res.json).toHaveBeenCalled(); + expect(console.error).toHaveBeenCalled(); + }); + }); }); diff --git a/test/unit/moderation/processReport.test.ts b/test/unit/moderation/processReport.test.ts index b952c3a..2eed73e 100644 --- a/test/unit/moderation/processReport.test.ts +++ b/test/unit/moderation/processReport.test.ts @@ -1,13 +1,13 @@ import { Request, Response } from "express"; import { - createMockRequest, - createMockResponse, + createMockRequest, + createMockResponse, } from "../../mocks/express.mock"; import { - getReportOptions, - getModerationServices, - reportModerationEvents, + getReportOptions, + getModerationServices, + reportModerationEvents, } from "../../../src/controllers/moderation.controller"; // Import repository functions @@ -17,180 +17,180 @@ import * as logs from "../../../src/repos/logs"; // Import fixtures import { - mockReportOptions, - mockModerationServices, - mockReport, - mockReports, + mockReportOptions, + mockModerationServices, + mockReport, + mockReports, } from "../../fixtures/moderation.fixtures"; import { mockUser } from "../../fixtures/user.fixtures"; describe("Moderation Controller", () => { - let req: Request; - let res: Response; - - beforeEach(() => { - jest.clearAllMocks(); - res = createMockResponse(); - }); - - describe("getReportOptions", () => { - it("should return available report options", async () => { - // Mock the repository function - jest - .spyOn(moderation, "fetchReportOptions") - .mockResolvedValue(mockReportOptions); - - await getReportOptions(req, res); - - expect(moderation.fetchReportOptions).toHaveBeenCalled(); - expect(res.status).toHaveBeenCalledWith(200); - expect(res.json).toHaveBeenCalledWith({ options: mockReportOptions }); - }); - - it("should handle errors gracefully", async () => { - // Mock an error response - jest - .spyOn(moderation, "fetchReportOptions") - .mockRejectedValue(new Error("Database error")); - jest.spyOn(console, "error").mockImplementation(() => {}); - - await getReportOptions(req, res); - - expect(console.error).toHaveBeenCalled(); - expect(res.status).toHaveBeenCalledWith(500); - expect(res.json).toHaveBeenCalledWith({ error: "Internal server error" }); - }); - }); - - describe("getModerationServices", () => { - it("should return available moderation services for a feed", async () => { - req = createMockRequest({ - user: { did: mockUser.did, handle: mockUser.handle }, - query: { uri: "feed:1" }, - }); - - jest - .spyOn(moderation, "fetchModerationServices") - .mockResolvedValue(mockModerationServices); - - await getModerationServices(req, res); - - expect(moderation.fetchModerationServices).toHaveBeenCalledWith("feed:1"); - expect(res.status).toHaveBeenCalledWith(200); - expect(res.json).toHaveBeenCalledWith({ - services: mockModerationServices, - }); - }); - - it("should return 401 if no user is authenticated", async () => { - req = createMockRequest({ - user: undefined, - query: { uri: "feed:1" }, - }); - - await getModerationServices(req, res); - - expect(res.status).toHaveBeenCalledWith(401); - expect(res.json).toHaveBeenCalledWith({ - error: "Unauthorized: No valid session", - }); - }); - - it("should return 400 if no feed URI is provided", async () => { - req = createMockRequest({ - user: { did: mockUser.did, handle: mockUser.handle }, - query: {}, - }); - - await getModerationServices(req, res); - - expect(res.status).toHaveBeenCalledWith(400); - expect(res.json).toHaveBeenCalledWith({ error: "Uri is required" }); - }); - }); - - describe("reportModerationEvents", () => { - beforeEach(() => { - // Set up common mocks needed for all tests - jest.spyOn(permissions, "customServiceGate").mockResolvedValue(true); - - // Mock blacksky response with proper Response type - const blackskyResponse = new Response(JSON.stringify({ ok: true }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - jest - .spyOn(moderation, "reportToBlacksky") - .mockResolvedValue(blackskyResponse); - - // Mock ozone response with proper return type - jest.spyOn(moderation, "reportToOzone").mockResolvedValue(undefined); - - jest.spyOn(logs, "createModerationLog").mockResolvedValue(undefined); - }); - - it("should process a single report", async () => { - req = createMockRequest({ - user: { did: mockUser.did, handle: mockUser.handle }, - body: mockReport, - }); - - await reportModerationEvents(req, res); - - // Should process the report - expect(res.json).toHaveBeenCalled(); - const responseData = (res.json as jest.Mock).mock.calls[0][0]; - expect(responseData).toHaveProperty("summary"); - expect(Array.isArray(responseData.summary)).toBe(true); - expect(responseData.summary.length).toBe(1); - }); - - it("should process multiple reports", async () => { - req = createMockRequest({ - user: { did: mockUser.did, handle: mockUser.handle }, - body: mockReports, - }); - - await reportModerationEvents(req, res); - - // Should process all reports - expect(res.json).toHaveBeenCalled(); - const responseData = (res.json as jest.Mock).mock.calls[0][0]; - expect(responseData).toHaveProperty("summary"); - expect(Array.isArray(responseData.summary)).toBe(true); - expect(responseData.summary.length).toBe(mockReports.length); - }); - - it("should return 401 if no user is authenticated", async () => { - req = createMockRequest({ - user: undefined, - body: {}, - }); - - await reportModerationEvents(req, res); - - expect(res.status).toHaveBeenCalledWith(401); - expect(res.json).toHaveBeenCalledWith({ - error: "Unauthorized: No valid session", - }); - }); - - it("should handle errors gracefully", async () => { - req = createMockRequest({ - user: { did: mockUser.did, handle: mockUser.handle }, - body: {}, - }); - - // Force an error in the process - jest - .spyOn(moderation, "reportToBlacksky") - .mockRejectedValue(new Error("Unexpected error")); - jest.spyOn(console, "error").mockImplementation(() => {}); - - await reportModerationEvents(req, res); - - expect(res.status).toHaveBeenCalledWith(500); - expect(res.json).toHaveBeenCalledWith({ error: "Internal server error" }); - }); - }); + let req: Request; + let res: Response; + + beforeEach(() => { + jest.clearAllMocks(); + res = createMockResponse(); + }); + + describe("getReportOptions", () => { + it("should return available report options", async () => { + // Mock the repository function + jest + .spyOn(moderation, "fetchReportOptions") + .mockResolvedValue(mockReportOptions); + + await getReportOptions(req, res); + + expect(moderation.fetchReportOptions).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ options: mockReportOptions }); + }); + + it("should handle errors gracefully", async () => { + // Mock an error response + jest + .spyOn(moderation, "fetchReportOptions") + .mockRejectedValue(new Error("Database error")); + jest.spyOn(console, "error") + + await getReportOptions(req, res); + + expect(console.error).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ error: "Internal server error" }); + }); + }); + + describe("getModerationServices", () => { + it("should return available moderation services for a feed", async () => { + req = createMockRequest({ + user: { did: mockUser.did, handle: mockUser.handle }, + query: { uri: "feed:1" }, + }); + + jest + .spyOn(moderation, "fetchModerationServices") + .mockResolvedValue(mockModerationServices); + + await getModerationServices(req, res); + + expect(moderation.fetchModerationServices).toHaveBeenCalledWith("feed:1"); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + services: mockModerationServices, + }); + }); + + it("should return 401 if no user is authenticated", async () => { + req = createMockRequest({ + user: undefined, + query: { uri: "feed:1" }, + }); + + await getModerationServices(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ + error: "Unauthorized: No valid session", + }); + }); + + it("should return 400 if no feed URI is provided", async () => { + req = createMockRequest({ + user: { did: mockUser.did, handle: mockUser.handle }, + query: {}, + }); + + await getModerationServices(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: "Uri is required" }); + }); + }); + + describe("reportModerationEvents", () => { + beforeEach(() => { + // Set up common mocks needed for all tests + jest.spyOn(permissions, "customServiceGate").mockResolvedValue(true); + + // Mock blacksky response with proper Response type + const blackskyResponse = new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + jest + .spyOn(moderation, "reportToBlacksky") + .mockResolvedValue(blackskyResponse); + + // Mock ozone response with proper return type + jest.spyOn(moderation, "reportToOzone").mockResolvedValue(undefined); + + jest.spyOn(logs, "createModerationLog").mockResolvedValue(undefined); + }); + + it("should process a single report", async () => { + req = createMockRequest({ + user: { did: mockUser.did, handle: mockUser.handle }, + body: mockReport, + }); + + await reportModerationEvents(req, res); + + // Should process the report + expect(res.json).toHaveBeenCalled(); + const responseData = (res.json as jest.Mock).mock.calls[0][0]; + expect(responseData).toHaveProperty("summary"); + expect(Array.isArray(responseData.summary)).toBe(true); + expect(responseData.summary.length).toBe(1); + }); + + it("should process multiple reports", async () => { + req = createMockRequest({ + user: { did: mockUser.did, handle: mockUser.handle }, + body: mockReports, + }); + + await reportModerationEvents(req, res); + + // Should process all reports + expect(res.json).toHaveBeenCalled(); + const responseData = (res.json as jest.Mock).mock.calls[0][0]; + expect(responseData).toHaveProperty("summary"); + expect(Array.isArray(responseData.summary)).toBe(true); + expect(responseData.summary.length).toBe(mockReports.length); + }); + + it("should return 401 if no user is authenticated", async () => { + req = createMockRequest({ + user: undefined, + body: {}, + }); + + await reportModerationEvents(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ + error: "Unauthorized: No valid session", + }); + }); + + it("should handle errors gracefully", async () => { + req = createMockRequest({ + user: { did: mockUser.did, handle: mockUser.handle }, + body: {}, + }); + + // Force an error in the process + jest + .spyOn(moderation, "reportToBlacksky") + .mockRejectedValue(new Error("Unexpected error")); + jest.spyOn(console, "error") + + await reportModerationEvents(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ error: "Internal server error" }); + }); + }); }); diff --git a/test/unit/permissions/getModerationServicesConfig.test.ts b/test/unit/permissions/getModerationServicesConfig.test.ts index 2a233cf..2a86cd4 100644 --- a/test/unit/permissions/getModerationServicesConfig.test.ts +++ b/test/unit/permissions/getModerationServicesConfig.test.ts @@ -1,8 +1,8 @@ import { tracker, setupDbMocks, cleanupDbMocks } from "../../mocks/db.mocks"; import { - mockCacheGet, - mockCacheSet, - setupNodeCacheMocks, + mockCacheGet, + mockCacheSet, + setupNodeCacheMocks, } from "../../mocks/cache.mocks"; import { mockModerationServices } from "../../fixtures/moderation.fixtures"; @@ -14,75 +14,74 @@ setupDbMocks(); import { getModerationServicesConfig } from "../../../src/repos/moderation"; describe("getModerationServicesConfig", () => { - // Keep track of query count manually - let queryCount = 0; - - beforeEach(() => { - jest.clearAllMocks(); - queryCount = 0; - tracker.install(); - }); - - afterEach(() => { - // Reset after each test - tracker.uninstall(); - }); - - afterAll(() => { - cleanupDbMocks(); // Clean up mock-knex - }); - - it("should return cached value if available", async () => { - // Setup cache to return a value - mockCacheGet.mockReturnValue(mockModerationServices); - - const result = await getModerationServicesConfig(); - - expect(result).toEqual(mockModerationServices); - // No queries should have been tracked when cache hit - expect(queryCount).toBe(0); - }); - - it("should query the database and cache the result if not in cache", async () => { - // Setup cache to not return a value - mockCacheGet.mockReturnValue(null); - - // Setup database to return sample data - tracker.on("query", function (query) { - queryCount++; - expect(query.method).toBe("select"); - query.response(mockModerationServices); - }); - - const result = await getModerationServicesConfig(); - - expect(result).toEqual(mockModerationServices); - expect(queryCount).toBe(1); - expect(mockCacheSet).toHaveBeenCalledWith( - "moderationServices", - mockModerationServices, - ); - }); - - it("should throw an error if the database query fails", async () => { - // Setup cache to not return a value - mockCacheGet.mockReturnValue(null); - - // Setup database to throw an error - const testError = new Error("DB Error"); - tracker.on("query", function (query) { - queryCount++; - query.reject(testError); - }); - - const consoleSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); - - await expect(getModerationServicesConfig()).rejects.toThrow("DB Error"); - expect(consoleSpy).toHaveBeenCalled(); - expect(queryCount).toBe(1); - - consoleSpy.mockRestore(); - }); + // Keep track of query count manually + let queryCount = 0; + + beforeEach(() => { + jest.clearAllMocks(); + queryCount = 0; + tracker.install(); + }); + + afterEach(() => { + // Reset after each test + tracker.uninstall(); + }); + + afterAll(() => { + cleanupDbMocks(); // Clean up mock-knex + }); + + it("should return cached value if available", async () => { + // Setup cache to return a value + mockCacheGet.mockReturnValue(mockModerationServices); + + const result = await getModerationServicesConfig(); + + expect(result).toEqual(mockModerationServices); + // No queries should have been tracked when cache hit + expect(queryCount).toBe(0); + }); + + it("should query the database and cache the result if not in cache", async () => { + // Setup cache to not return a value + mockCacheGet.mockReturnValue(null); + + // Setup database to return sample data + tracker.on("query", function (query) { + queryCount++; + expect(query.method).toBe("select"); + query.response(mockModerationServices); + }); + + const result = await getModerationServicesConfig(); + + expect(result).toEqual(mockModerationServices); + expect(queryCount).toBe(1); + expect(mockCacheSet).toHaveBeenCalledWith( + "moderationServices", + mockModerationServices, + ); + }); + + it("should throw an error if the database query fails", async () => { + // Setup cache to not return a value + mockCacheGet.mockReturnValue(null); + + // Setup database to throw an error + const testError = new Error("DB Error"); + tracker.on("query", function (query) { + queryCount++; + query.reject(testError); + }); + + const consoleSpy = jest + .spyOn(console, "error") + + await expect(getModerationServicesConfig()).rejects.toThrow("DB Error"); + expect(consoleSpy).toHaveBeenCalled(); + expect(queryCount).toBe(1); + + consoleSpy.mockRestore(); + }); }); From 27657882d2b67d9f5260935d3c82466596678b32 Mon Sep 17 00:00:00 2001 From: nulfrost Date: Sat, 19 Jul 2025 21:06:36 -0400 Subject: [PATCH 2/2] fix: add --silent option for jest and update pre-commit hook to look for biome --- .husky/pre-commit | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 10421d4..68c3a48 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,12 +1,12 @@ echo "🔍 Running linting..." -if [ -f .eslintrc.js ] || [ -f .eslintrc.json ] || [ -f .eslintrc.yml ] || [ -f .eslintrc.yaml ] || grep -q 'eslintConfig' package.json 2>/dev/null; then +if [ -f biome.json ] || [ -f biome.jsonc ]; then npm run lint if [ $? -ne 0 ]; then echo "❌ Linting failed. Commit aborted." exit 1 fi else - echo "⚠️ No ESLint config found, skipping lint." + echo "⚠️ No Biome config found, skipping lint." fi echo "🔎 Running TypeScript type checks..." diff --git a/package.json b/package.json index c11fbcc..e19a389 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "ts-node-dev --respawn --transpile-only src/server.ts", "build": "tsc", "start": "node dist/src/server.js", - "test": "NODE_ENV=test jest --detectOpenHandles --forceExit", + "test": "NODE_ENV=test jest --detectOpenHandles --forceExit --silent", "test:watch": "NODE_ENV=test npm test -- --watch", "test:coverage": "NODE_ENV=test jest --coverage --forceExit", "test:e2e": "newman run postman/safe-skies-api.postman_collection.json",