From d1b78d3dda28583b5987bfd4205c433ccc57d880 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Sat, 18 Apr 2026 07:28:54 +0200 Subject: [PATCH 1/5] refactor(tests): extract parseSessionCookie helper to eliminate duplication The getAuthHeaders function contained two identical 8-line blocks for parsing session cookies from Set-Cookie headers. Extract this logic into a standalone helper function. Fallow report (before): test/helpers/test-instance.js:134-141 test/helpers/test-instance.js:159-166 Identical code blocks detected Fallow report (after): No duplication in test-instance.js This is the first step in reducing the complexity of getTestInstance. --- test/helpers/test-instance.js | 43 ++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/test/helpers/test-instance.js b/test/helpers/test-instance.js index b2312a6..21f9eef 100644 --- a/test/helpers/test-instance.js +++ b/test/helpers/test-instance.js @@ -130,15 +130,7 @@ export async function getTestInstance() { } if (setCookie) { - // Parse cookie to extract session token - const cookies = setCookie.split(",").map((c) => c.trim()); - const sessionCookie = cookies.find((c) => - c.startsWith("better-auth.session_token"), - ); - - if (sessionCookie) { - return { cookie: sessionCookie.split(";")[0] }; - } + return parseSessionCookie(setCookie); } } throw new Error(`Magic link verification failed: ${error.message}`, { @@ -155,18 +147,7 @@ export async function getTestInstance() { throw new Error("No session cookie in magic link verification response"); } - // Parse cookie to extract session token - const cookies = setCookie.split(",").map((c) => c.trim()); - const sessionCookie = cookies.find((c) => - c.startsWith("better-auth.session_token"), - ); - - if (!sessionCookie) { - throw new Error("No session token cookie found"); - } - - // Return just the cookie header value - return { cookie: sessionCookie.split(";")[0] }; + return parseSessionCookie(setCookie); }; return { @@ -177,3 +158,23 @@ export async function getTestInstance() { getMagicLinks: () => auth._testMagicLinks || [], }; } + +/** + * Parse a Set-Cookie header to extract the better-auth session token + * @param {string} setCookie - The Set-Cookie header value + * @returns {{cookie: string}} Object containing the cookie header value + * @throws {Error} If no session token cookie is found + */ +function parseSessionCookie(setCookie) { + const cookies = setCookie.split(",").map((c) => c.trim()); + const sessionCookie = cookies.find((c) => + c.startsWith("better-auth.session_token"), + ); + + if (!sessionCookie) { + throw new Error("No session token cookie found"); + } + + // Return just the cookie header value (before the semicolon) + return { cookie: sessionCookie.split(";")[0] }; +} From 8024be37ec0936038e619789fa9a5fdf67f2cbc9 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Sat, 18 Apr 2026 07:30:06 +0200 Subject: [PATCH 2/5] refactor(tests): extract extractCookieFromError to reduce complexity The getAuthHeaders function had high cyclomatic (13) and cognitive (20) complexity due to nested conditionals for handling 302 redirect errors. Extract the cookie extraction logic from error responses into a dedicated helper function. Fallow health report (before): test/helpers/test-instance.js:86 getAuthHeaders 13 cyclomatic 20 cognitive 66 lines Functions exceeding cyclomatic or cognitive complexity thresholds Fallow health report (after): No high complexity functions This refactoring separates the error handling concern and reduces the cognitive load of the main getAuthHeaders function. --- test/helpers/test-instance.js | 47 ++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/test/helpers/test-instance.js b/test/helpers/test-instance.js index 21f9eef..df4ec11 100644 --- a/test/helpers/test-instance.js +++ b/test/helpers/test-instance.js @@ -115,23 +115,9 @@ export async function getTestInstance() { }); } catch (error) { // magicLinkVerify may throw an APIError with the redirect response - // Extract the headers from the error if it's a 302 redirect - if (error.statusCode === 302) { - // error.headers is a Map-like object from Headers - let setCookie; - if (error.headers && typeof error.headers.get === "function") { - setCookie = error.headers.get("set-cookie"); - } else if (error.headers && Array.isArray(error.headers)) { - // If it's an array of tuples - const setCookieHeader = error.headers.find( - (h) => h[0].toLowerCase() === "set-cookie", - ); - setCookie = setCookieHeader ? setCookieHeader[1] : null; - } - - if (setCookie) { - return parseSessionCookie(setCookie); - } + const cookieFromError = extractCookieFromError(error); + if (cookieFromError) { + return cookieFromError; } throw new Error(`Magic link verification failed: ${error.message}`, { cause: error, @@ -159,6 +145,33 @@ export async function getTestInstance() { }; } +/** + * Extract session cookie from a 302 redirect error response + * @param {Error} error - The error thrown by magicLinkVerify + * @returns {{cookie: string}|null} Session cookie object or null if not found + */ +function extractCookieFromError(error) { + if (error.statusCode !== 302) { + return null; + } + + let setCookie; + if (error.headers && typeof error.headers.get === "function") { + setCookie = error.headers.get("set-cookie"); + } else if (error.headers && Array.isArray(error.headers)) { + const setCookieHeader = error.headers.find( + (h) => h[0].toLowerCase() === "set-cookie", + ); + setCookie = setCookieHeader ? setCookieHeader[1] : null; + } + + if (setCookie) { + return parseSessionCookie(setCookie); + } + + return null; +} + /** * Parse a Set-Cookie header to extract the better-auth session token * @param {string} setCookie - The Set-Cookie header value From 6b28a90c83b870dcb8ee22724c439fdabd614e34 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Sat, 18 Apr 2026 07:52:05 +0200 Subject: [PATCH 3/5] refactor(tests): extract createTestAuth factory function Extract the Better Auth configuration logic from getTestInstance into a dedicated createTestAuth factory function. This separates the auth setup concern from the orchestration logic. Key changes: - Created createTestAuth(db, magicLinksStore) function - Fixed closure issue by using external magicLinksStore array instead of auth._testMagicLinks property - getTestInstance now orchestrates rather than configures Fallow health improvement: - getTestInstance reduced from 120 lines to ~100 lines - Auth configuration now isolated and reusable --- test/helpers/test-instance.js | 81 +++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/test/helpers/test-instance.js b/test/helpers/test-instance.js index df4ec11..8dfef5a 100644 --- a/test/helpers/test-instance.js +++ b/test/helpers/test-instance.js @@ -27,42 +27,10 @@ import Database from "better-sqlite3"; export async function getTestInstance() { // Create in-memory database const db = new Database(":memory:"); + const magicLinksStore = []; // Configure Better Auth for testing - const auth = betterAuth({ - database: db, - baseURL: "http://localhost:3000", - logger: { - disabled: true, // Suppress logs during tests - }, - socialProviders: { - github: { - clientId: "test-client-id", - clientSecret: "test-client-secret", - }, - }, - telemetry: { - enabled: false, - }, - session: { - expiresIn: 60 * 60 * 24 * 7, // 7 days - updateAge: 60 * 60 * 24, // 1 day - }, - plugins: [ - magicLink({ - sendMagicLink: async ({ email, token, url }) => { - // Store magic link for testing - // Tests can access this via the returned magicLinks array - if (!auth._testMagicLinks) { - auth._testMagicLinks = []; - } - auth._testMagicLinks.push({ email, token, url }); - return Promise.resolve(); - }, - }), - admin(), - ], - }); + const auth = createTestAuth(db, magicLinksStore); // Run migrations to create database tables const migrations = await getMigrations(auth.options); @@ -94,8 +62,7 @@ export async function getTestInstance() { }); // Get the magic link from the test array - const magicLinks = auth._testMagicLinks || []; - const magicLink = magicLinks.find((link) => link.email === email); + const magicLink = magicLinksStore.find((link) => link.email === email); if (!magicLink) { throw new Error(`No magic link found for ${email}`); @@ -141,10 +108,50 @@ export async function getTestInstance() { client, db, getAuthHeaders, - getMagicLinks: () => auth._testMagicLinks || [], + getMagicLinks: () => magicLinksStore, }; } +/** + * Creates a Better Auth instance configured for testing + * @param {Database} db - better-sqlite3 database instance + * @param {Array} magicLinksStore - External array to store captured magic links + * @returns {Object} Configured Better Auth instance + */ +function createTestAuth(db, magicLinksStore) { + return betterAuth({ + database: db, + baseURL: "http://localhost:3000", + logger: { + disabled: true, // Suppress logs during tests + }, + socialProviders: { + github: { + clientId: "test-client-id", + clientSecret: "test-client-secret", + }, + }, + telemetry: { + enabled: false, + }, + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days + updateAge: 60 * 60 * 24, // 1 day + }, + plugins: [ + magicLink({ + sendMagicLink: async ({ email, token, url }) => { + // Store magic link for testing + // Tests can access this via the returned magicLinks array + magicLinksStore.push({ email, token, url }); + return Promise.resolve(); + }, + }), + admin(), + ], + }); +} + /** * Extract session cookie from a 302 redirect error response * @param {Error} error - The error thrown by magicLinkVerify From 035ba48eb2133ed875b7831239965a285263da21 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Sat, 18 Apr 2026 07:52:53 +0200 Subject: [PATCH 4/5] refactor(tests): extract createTestClient factory function Extract the client helper creation logic from getTestInstance into a dedicated createTestClient factory function. This isolates the admin operations configuration. Key changes: - Created createTestClient(auth) function - Client helper logic now separated from main orchestration - Improves testability of admin operations Fallow health improvement: - getTestInstance further reduced in size - Client configuration now isolated and reusable --- test/helpers/test-instance.js | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/test/helpers/test-instance.js b/test/helpers/test-instance.js index 8dfef5a..9a10ce9 100644 --- a/test/helpers/test-instance.js +++ b/test/helpers/test-instance.js @@ -37,18 +37,7 @@ export async function getTestInstance() { await migrations.runMigrations(); // Create client helper for common operations - const client = { - // Admin methods (based on spike findings) - admin: { - setRole: async ({ userId, role }) => { - // Based on spike findings: direct database UPDATE is required - // The admin.setRole endpoint requires authentication, so direct DB access is simpler for testing - const db = auth.options?.database; - if (!db) throw new Error("Cannot access database for role update"); - db.prepare("UPDATE user SET role = ? WHERE id = ?").run(role, userId); - }, - }, - }; + const client = createTestClient(auth); // Helper to get session headers for authenticated requests via magic link const getAuthHeaders = async (email) => { @@ -112,6 +101,26 @@ export async function getTestInstance() { }; } +/** + * Creates a test client helper with admin operations + * @param {Object} auth - Better Auth instance + * @returns {Object} Client helper object with admin methods + */ +function createTestClient(auth) { + return { + // Admin methods (based on spike findings) + admin: { + setRole: async ({ userId, role }) => { + // Based on spike findings: direct database UPDATE is required + // The admin.setRole endpoint requires authentication, so direct DB access is simpler for testing + const db = auth.options?.database; + if (!db) throw new Error("Cannot access database for role update"); + db.prepare("UPDATE user SET role = ? WHERE id = ?").run(role, userId); + }, + }, + }; +} + /** * Creates a Better Auth instance configured for testing * @param {Database} db - better-sqlite3 database instance From 1c4bda6b5150dcf2eaa3cb15d37df4c3ef110732 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Sat, 18 Apr 2026 07:54:13 +0200 Subject: [PATCH 5/5] refactor(tests): extract createGetAuthHeaders factory function Extract the getAuthHeaders helper creation from getTestInstance into a dedicated createGetAuthHeaders factory function. This completes the decomposition of the large getTestInstance function. Key changes: - Created createGetAuthHeaders(auth, magicLinksStore) function - Returns the async getAuthHeaders function - Uses external magicLinksStore instead of auth._testMagicLinks Fallow health improvement: - getTestInstance reduced to ~20 lines (pure orchestration) - Large function warning completely resolved - All factory functions are independently testable --- test/helpers/test-instance.js | 68 ++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/test/helpers/test-instance.js b/test/helpers/test-instance.js index 9a10ce9..ca26933 100644 --- a/test/helpers/test-instance.js +++ b/test/helpers/test-instance.js @@ -40,7 +40,45 @@ export async function getTestInstance() { const client = createTestClient(auth); // Helper to get session headers for authenticated requests via magic link - const getAuthHeaders = async (email) => { + const getAuthHeaders = createGetAuthHeaders(auth, magicLinksStore); + + return { + auth, + client, + db, + getAuthHeaders, + getMagicLinks: () => magicLinksStore, + }; +} + +/** + * Creates a test client helper with admin operations + * @param {Object} auth - Better Auth instance + * @returns {Object} Client helper object with admin methods + */ +function createTestClient(auth) { + return { + // Admin methods (based on spike findings) + admin: { + setRole: async ({ userId, role }) => { + // Based on spike findings: direct database UPDATE is required + // The admin.setRole endpoint requires authentication, so direct DB access is simpler for testing + const db = auth.options?.database; + if (!db) throw new Error("Cannot access database for role update"); + db.prepare("UPDATE user SET role = ? WHERE id = ?").run(role, userId); + }, + }, + }; +} + +/** + * Creates a function to get authentication headers for test requests + * @param {Object} auth - Better Auth instance + * @param {Array} magicLinksStore - Array storing captured magic links + * @returns {Function} Async function that takes an email and returns auth headers + */ +function createGetAuthHeaders(auth, magicLinksStore) { + return async function getAuthHeaders(email) { // Send magic link await auth.api.signInMagicLink({ body: { @@ -91,34 +129,6 @@ export async function getTestInstance() { return parseSessionCookie(setCookie); }; - - return { - auth, - client, - db, - getAuthHeaders, - getMagicLinks: () => magicLinksStore, - }; -} - -/** - * Creates a test client helper with admin operations - * @param {Object} auth - Better Auth instance - * @returns {Object} Client helper object with admin methods - */ -function createTestClient(auth) { - return { - // Admin methods (based on spike findings) - admin: { - setRole: async ({ userId, role }) => { - // Based on spike findings: direct database UPDATE is required - // The admin.setRole endpoint requires authentication, so direct DB access is simpler for testing - const db = auth.options?.database; - if (!db) throw new Error("Cannot access database for role update"); - db.prepare("UPDATE user SET role = ? WHERE id = ?").run(role, userId); - }, - }, - }; } /**