From e0db94ecb965ce7223820c392f163c94601bfcbc Mon Sep 17 00:00:00 2001 From: "gitauto-ai[bot]" <161652217+gitauto-ai[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:50:31 +0000 Subject: [PATCH 1/5] Initial empty commit to create PR [skip ci] From 7591d93c7d7514f6641d8b08f15a9700338ceb25 Mon Sep 17 00:00:00 2001 From: "gitauto-ai[bot]" <161652217+gitauto-ai[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:54:31 +0000 Subject: [PATCH 2/5] Create app/actions/supabase/owners/check-all-auto-reloads.test.ts [skip ci] Co-Authored-By: hiroshinishio --- .../owners/check-all-auto-reloads.test.ts | 433 ++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 app/actions/supabase/owners/check-all-auto-reloads.test.ts diff --git a/app/actions/supabase/owners/check-all-auto-reloads.test.ts b/app/actions/supabase/owners/check-all-auto-reloads.test.ts new file mode 100644 index 00000000..d99c7849 --- /dev/null +++ b/app/actions/supabase/owners/check-all-auto-reloads.test.ts @@ -0,0 +1,433 @@ +import { checkAllAutoReloads } from "./check-all-auto-reloads"; +import { supabaseAdmin } from "@/lib/supabase/server"; +import { chargeSavedPaymentMethod } from "@/app/actions/stripe/charge-saved-payment-method"; +import { slackUs } from "@/app/actions/slack/slack-us"; +import { validateAutoReloadSpendingLimit } from "./validate-spending-limit"; + +jest.mock("@/lib/supabase/server", () => ({ + supabaseAdmin: { + from: jest.fn().mockReturnThis(), + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockReturnThis(), + neq: jest.fn().mockReturnThis(), + update: jest.fn().mockReturnThis(), + rpc: jest.fn(), + }, +})); + +jest.mock("@/app/actions/stripe/charge-saved-payment-method"); +jest.mock("@/app/actions/slack/slack-us"); +jest.mock("./validate-spending-limit"); + +describe("checkAllAutoReloads solitary", () => { + beforeEach(() => { + jest.clearAllMocks(); + // Mock console.log/error to keep test output clean + jest.spyOn(console, "log").mockImplementation(() => {}); + jest.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + // ===== solitary ===== + + it("should throw error if fetching owners fails", async () => { + // Verify that if Supabase returns an error during initial fetch, it's thrown + (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + data: null, + error: { message: "Database connection error" }, + }); + + await expect(checkAllAutoReloads()).rejects.toThrow("Failed to fetch owners for auto-reload: Database connection error"); + }); + + it("should return success with 0 processed if no owners are eligible", async () => { + // Verify behavior when no owners meet the criteria (enabled, has stripe id, balance <= threshold) + (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + data: [ + { + owner_id: 1, + credit_balance_usd: 100, + auto_reload_threshold_usd: 50, + auto_reload_target_usd: 200, + stripe_customer_id: "cus_123", + }, + ], + error: null, + }); + + const result = await checkAllAutoReloads(); + + expect(result).toEqual({ + success: true, + processed: 0, + results: [], + }); + }); + + it("should skip owner if lock cannot be acquired", async () => { + // Verify that if acquire_auto_reload_lock returns 0, the owner is skipped + (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + data: [ + { + owner_id: 1, + credit_balance_usd: 10, + auto_reload_threshold_usd: 50, + auto_reload_target_usd: 100, + stripe_customer_id: "cus_123", + }, + ], + error: null, + }); + + (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 0 }); + + const result = await checkAllAutoReloads(); + + expect(result.results[0]).toEqual({ + ownerId: 1, + success: false, + reason: "Auto-reload already in progress", + }); + // Ensure lock release is still called in finally + expect(supabaseAdmin.update).toHaveBeenCalledWith({ auto_reload_in_progress: null }); + }); + + it("should skip owner if target amount is less than or equal to current balance", async () => { + // Verify that if target <= balance, it's skipped to avoid zero/negative charges + (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + data: [ + { + owner_id: 1, + credit_balance_usd: 100, + auto_reload_threshold_usd: 100, // Eligible because balance <= threshold + auto_reload_target_usd: 100, // But target is not > balance + stripe_customer_id: "cus_123", + }, + ], + error: null, + }); + + (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); + + const result = await checkAllAutoReloads(); + + expect(result.results[0]).toEqual({ + ownerId: 1, + success: false, + reason: "Target amount would be negative or zero", + }); + }); + + it("should skip owner if spending limit is not allowed", async () => { + // Verify that if validateAutoReloadSpendingLimit returns allowed: false, it's skipped + (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + data: [ + { + owner_id: 1, + credit_balance_usd: 10, + auto_reload_threshold_usd: 50, + auto_reload_target_usd: 100, + stripe_customer_id: "cus_123", + }, + ], + error: null, + }); + + (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); + (validateAutoReloadSpendingLimit as jest.Mock).mockResolvedValueOnce({ + allowed: false, + reason: "Monthly spending limit reached", + }); + + const result = await checkAllAutoReloads(); + + expect(result.results[0]).toEqual({ + ownerId: 1, + success: false, + reason: "Monthly spending limit reached", + }); + }); + + it("should skip owner if adjusted amount is zero or negative", async () => { + // Verify that if spending limit adjusts the amount to <= 0, it's skipped + (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + data: [ + { + owner_id: 1, + credit_balance_usd: 10, + auto_reload_threshold_usd: 50, + auto_reload_target_usd: 100, + stripe_customer_id: "cus_123", + }, + ], + error: null, + }); + + (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); + (validateAutoReloadSpendingLimit as jest.Mock).mockResolvedValueOnce({ + allowed: true, + adjustedAmountUsd: 0, + isAdjusted: true, + }); + + const result = await checkAllAutoReloads(); + + expect(result.results[0]).toEqual({ + ownerId: 1, + success: false, + reason: "Adjusted amount is zero due to spending limit", + }); + }); + + it("should successfully charge owner and log adjusted amount if applicable", async () => { + // Verify happy path where charge is successful and amount was adjusted + (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + data: [ + { + owner_id: 1, + credit_balance_usd: 10, + auto_reload_threshold_usd: 50, + auto_reload_target_usd: 100, + stripe_customer_id: "cus_123", + }, + ], + error: null, + }); + + (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); + (validateAutoReloadSpendingLimit as jest.Mock).mockResolvedValueOnce({ + allowed: true, + adjustedAmountUsd: 40, // Adjusted from 90 + isAdjusted: true, + }); + (chargeSavedPaymentMethod as jest.Mock).mockResolvedValueOnce({ + success: true, + paymentIntentId: "pi_test_123", + }); + + const result = await checkAllAutoReloads(); + + expect(result.results[0]).toEqual({ + ownerId: 1, + success: true, + amountCharged: 40, + paymentIntentId: "pi_test_123", + }); + expect(console.log).toHaveBeenCalledWith(expect.stringContaining("adjusted from $90")); + }); + + it("should successfully charge owner without adjustment", async () => { + // Verify happy path where charge is successful and no adjustment was needed + (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + data: [ + { + owner_id: 1, + credit_balance_usd: 10, + auto_reload_threshold_usd: 50, + auto_reload_target_usd: 100, + stripe_customer_id: "cus_123", + }, + ], + error: null, + }); + + (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); + (validateAutoReloadSpendingLimit as jest.Mock).mockResolvedValueOnce({ + allowed: true, + adjustedAmountUsd: 90, + isAdjusted: false, + }); + (chargeSavedPaymentMethod as jest.Mock).mockResolvedValueOnce({ + success: true, + paymentIntentId: "pi_test_123", + }); + + const result = await checkAllAutoReloads(); + + expect(result.results[0]).toEqual({ + ownerId: 1, + success: true, + amountCharged: 90, + paymentIntentId: "pi_test_123", + }); + expect(console.log).not.toHaveBeenCalledWith(expect.stringContaining("adjusted from")); + }); + + it("should handle charge failure with error message and notify Slack", async () => { + // Verify that if charge fails, it's recorded, Slack is notified, and console.error is called for non-test customers + (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + data: [ + { + owner_id: 1, + credit_balance_usd: 10, + auto_reload_threshold_usd: 50, + auto_reload_target_usd: 100, + stripe_customer_id: "cus_real_123", + }, + ], + error: null, + }); + + (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); + (validateAutoReloadSpendingLimit as jest.Mock).mockResolvedValueOnce({ + allowed: true, + adjustedAmountUsd: 90, + isAdjusted: false, + }); + (chargeSavedPaymentMethod as jest.Mock).mockResolvedValueOnce({ + success: false, + error: "Card declined", + }); + + const result = await checkAllAutoReloads(); + + expect(result.results[0]).toEqual({ + ownerId: 1, + success: false, + error: "Card declined", + }); + expect(slackUs).toHaveBeenCalledWith(expect.stringContaining("❌ Auto-reload failed!")); + expect(slackUs).toHaveBeenCalledWith(expect.stringContaining("Error: Card declined")); + expect(console.error).toHaveBeenCalledWith(expect.stringContaining("❌ Auto-reload failed for owner 1:"), "Card declined"); + }); + + it("should handle charge failure without error message and not log for test customers", async () => { + // Verify that if charge fails without error message, a default is used, and no console.error for test customers + (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + data: [ + { + owner_id: 1, + credit_balance_usd: 10, + auto_reload_threshold_usd: 50, + auto_reload_target_usd: 100, + stripe_customer_id: "cus_test_123", + }, + ], + error: null, + }); + + (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); + (validateAutoReloadSpendingLimit as jest.Mock).mockResolvedValueOnce({ + allowed: true, + adjustedAmountUsd: 90, + isAdjusted: false, + }); + (chargeSavedPaymentMethod as jest.Mock).mockResolvedValueOnce({ + success: false, + error: null, + }); + + const result = await checkAllAutoReloads(); + + expect(result.results[0]).toEqual({ + ownerId: 1, + success: false, + error: "Unknown payment error", + }); + expect(slackUs).toHaveBeenCalledWith(expect.stringContaining("Error: Unknown error")); + expect(console.error).not.toHaveBeenCalled(); + }); + + it("should handle unexpected errors during processing and notify Slack", async () => { + // Verify that if an unexpected error occurs in the loop, it's caught, recorded, and Slack is notified + (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + data: [ + { + owner_id: 1, + credit_balance_usd: 10, + auto_reload_threshold_usd: 50, + auto_reload_target_usd: 100, + stripe_customer_id: "cus_123", + }, + ], + error: null, + }); + + (supabaseAdmin.rpc as jest.Mock).mockRejectedValueOnce(new Error("Unexpected RPC failure")); + + const result = await checkAllAutoReloads(); + + expect(result.results[0]).toEqual({ + ownerId: 1, + success: false, + error: "Unexpected RPC failure", + }); + expect(slackUs).toHaveBeenCalledWith(expect.stringContaining("❌ Auto-reload error!")); + expect(slackUs).toHaveBeenCalledWith(expect.stringContaining("Error: Unexpected RPC failure")); + expect(supabaseAdmin.update).toHaveBeenCalledWith({ auto_reload_in_progress: null }); + }); + + it("should handle non-Error objects thrown during processing", async () => { + // Verify that if a non-Error object is thrown, it's handled gracefully + (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + data: [ + { + owner_id: 1, + credit_balance_usd: 10, + auto_reload_threshold_usd: 50, + auto_reload_target_usd: 100, + stripe_customer_id: "cus_123", + }, + ], + error: null, + }); + + (supabaseAdmin.rpc as jest.Mock).mockRejectedValueOnce("String error"); + + const result = await checkAllAutoReloads(); + + expect(result.results[0]).toEqual({ + ownerId: 1, + success: false, + error: "Unknown error", + }); + }); + + it("should process multiple owners and return correct summary", async () => { + // Verify that multiple owners are processed and the success count is correct + (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + data: [ + { + owner_id: 1, + credit_balance_usd: 10, + auto_reload_threshold_usd: 50, + auto_reload_target_usd: 100, + stripe_customer_id: "cus_1", + }, + { + owner_id: 2, + credit_balance_usd: 10, + auto_reload_threshold_usd: 50, + auto_reload_target_usd: 100, + stripe_customer_id: "cus_2", + }, + ], + error: null, + }); + + (supabaseAdmin.rpc as jest.Mock) + .mockResolvedValueOnce({ data: 1 }) // Owner 1 lock + .mockResolvedValueOnce({ data: 1 }); // Owner 2 lock + + (validateAutoReloadSpendingLimit as jest.Mock).mockResolvedValue({ + allowed: true, + adjustedAmountUsd: 90, + isAdjusted: false, + }); + + (chargeSavedPaymentMethod as jest.Mock) + .mockResolvedValueOnce({ success: true, paymentIntentId: "pi_1" }) + .mockResolvedValueOnce({ success: false, error: "Failed" }); + + const result = await checkAllAutoReloads(); + + expect(result.processed).toBe(2); + expect(result.results).toHaveLength(2); + expect(result.results[0].success).toBe(true); + expect(result.results[1].success).toBe(false); + expect(console.log).toHaveBeenCalledWith(expect.stringContaining("Auto-reload completed: 1/2 successful")); + }); +}); From 56828018dd6e0be2e3fedc5fe992ae8702dee743 Mon Sep 17 00:00:00 2001 From: "gitauto-ai[bot]" <161652217+gitauto-ai[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:56:49 +0000 Subject: [PATCH 3/5] Update app/actions/supabase/owners/check-all-auto-reloads.test.ts [skip ci] Co-Authored-By: hiroshinishio --- .../owners/check-all-auto-reloads.test.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/app/actions/supabase/owners/check-all-auto-reloads.test.ts b/app/actions/supabase/owners/check-all-auto-reloads.test.ts index d99c7849..33240a94 100644 --- a/app/actions/supabase/owners/check-all-auto-reloads.test.ts +++ b/app/actions/supabase/owners/check-all-auto-reloads.test.ts @@ -1,3 +1,5 @@ +process.env.STRIPE_SECRET_KEY = "sk_test_dummy"; + import { checkAllAutoReloads } from "./check-all-auto-reloads"; import { supabaseAdmin } from "@/lib/supabase/server"; import { chargeSavedPaymentMethod } from "@/app/actions/stripe/charge-saved-payment-method"; @@ -35,7 +37,7 @@ describe("checkAllAutoReloads solitary", () => { it("should throw error if fetching owners fails", async () => { // Verify that if Supabase returns an error during initial fetch, it's thrown - (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ data: null, error: { message: "Database connection error" }, }); @@ -45,7 +47,7 @@ describe("checkAllAutoReloads solitary", () => { it("should return success with 0 processed if no owners are eligible", async () => { // Verify behavior when no owners meet the criteria (enabled, has stripe id, balance <= threshold) - (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ data: [ { owner_id: 1, @@ -69,7 +71,7 @@ describe("checkAllAutoReloads solitary", () => { it("should skip owner if lock cannot be acquired", async () => { // Verify that if acquire_auto_reload_lock returns 0, the owner is skipped - (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ data: [ { owner_id: 1, @@ -97,7 +99,7 @@ describe("checkAllAutoReloads solitary", () => { it("should skip owner if target amount is less than or equal to current balance", async () => { // Verify that if target <= balance, it's skipped to avoid zero/negative charges - (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ data: [ { owner_id: 1, @@ -123,7 +125,7 @@ describe("checkAllAutoReloads solitary", () => { it("should skip owner if spending limit is not allowed", async () => { // Verify that if validateAutoReloadSpendingLimit returns allowed: false, it's skipped - (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ data: [ { owner_id: 1, @@ -153,7 +155,7 @@ describe("checkAllAutoReloads solitary", () => { it("should skip owner if adjusted amount is zero or negative", async () => { // Verify that if spending limit adjusts the amount to <= 0, it's skipped - (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ data: [ { owner_id: 1, @@ -184,7 +186,7 @@ describe("checkAllAutoReloads solitary", () => { it("should successfully charge owner and log adjusted amount if applicable", async () => { // Verify happy path where charge is successful and amount was adjusted - (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ data: [ { owner_id: 1, @@ -221,7 +223,7 @@ describe("checkAllAutoReloads solitary", () => { it("should successfully charge owner without adjustment", async () => { // Verify happy path where charge is successful and no adjustment was needed - (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ data: [ { owner_id: 1, @@ -258,7 +260,7 @@ describe("checkAllAutoReloads solitary", () => { it("should handle charge failure with error message and notify Slack", async () => { // Verify that if charge fails, it's recorded, Slack is notified, and console.error is called for non-test customers - (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ data: [ { owner_id: 1, @@ -296,7 +298,7 @@ describe("checkAllAutoReloads solitary", () => { it("should handle charge failure without error message and not log for test customers", async () => { // Verify that if charge fails without error message, a default is used, and no console.error for test customers - (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ data: [ { owner_id: 1, @@ -333,7 +335,7 @@ describe("checkAllAutoReloads solitary", () => { it("should handle unexpected errors during processing and notify Slack", async () => { // Verify that if an unexpected error occurs in the loop, it's caught, recorded, and Slack is notified - (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ data: [ { owner_id: 1, @@ -362,7 +364,7 @@ describe("checkAllAutoReloads solitary", () => { it("should handle non-Error objects thrown during processing", async () => { // Verify that if a non-Error object is thrown, it's handled gracefully - (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ data: [ { owner_id: 1, @@ -388,7 +390,7 @@ describe("checkAllAutoReloads solitary", () => { it("should process multiple owners and return correct summary", async () => { // Verify that multiple owners are processed and the success count is correct - (supabaseAdmin.select as jest.Mock).mockResolvedValueOnce({ + ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ data: [ { owner_id: 1, From 26ffe298925c54288135f7f1be4bc317a9b6291d Mon Sep 17 00:00:00 2001 From: "gitauto-ai[bot]" <161652217+gitauto-ai[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:57:56 +0000 Subject: [PATCH 4/5] Update app/actions/supabase/owners/check-all-auto-reloads.test.ts [skip ci] Co-Authored-By: hiroshinishio --- app/actions/supabase/owners/check-all-auto-reloads.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/actions/supabase/owners/check-all-auto-reloads.test.ts b/app/actions/supabase/owners/check-all-auto-reloads.test.ts index 33240a94..bb4e6982 100644 --- a/app/actions/supabase/owners/check-all-auto-reloads.test.ts +++ b/app/actions/supabase/owners/check-all-auto-reloads.test.ts @@ -1,5 +1,7 @@ process.env.STRIPE_SECRET_KEY = "sk_test_dummy"; +process.env.STRIPE_SECRET_KEY = "sk_test_dummy"; + import { checkAllAutoReloads } from "./check-all-auto-reloads"; import { supabaseAdmin } from "@/lib/supabase/server"; import { chargeSavedPaymentMethod } from "@/app/actions/stripe/charge-saved-payment-method"; @@ -17,6 +19,7 @@ jest.mock("@/lib/supabase/server", () => ({ }, })); +jest.mock("@/lib/stripe", () => ({ stripe: { customers: { retrieve: jest.fn() }, paymentIntents: { create: jest.fn() } } })); jest.mock("@/app/actions/stripe/charge-saved-payment-method"); jest.mock("@/app/actions/slack/slack-us"); jest.mock("./validate-spending-limit"); @@ -94,7 +97,7 @@ describe("checkAllAutoReloads solitary", () => { reason: "Auto-reload already in progress", }); // Ensure lock release is still called in finally - expect(supabaseAdmin.update).toHaveBeenCalledWith({ auto_reload_in_progress: null }); + expect((supabaseAdmin as any).update).toHaveBeenCalledWith({ auto_reload_in_progress: null }); }); it("should skip owner if target amount is less than or equal to current balance", async () => { @@ -359,7 +362,7 @@ describe("checkAllAutoReloads solitary", () => { }); expect(slackUs).toHaveBeenCalledWith(expect.stringContaining("❌ Auto-reload error!")); expect(slackUs).toHaveBeenCalledWith(expect.stringContaining("Error: Unexpected RPC failure")); - expect(supabaseAdmin.update).toHaveBeenCalledWith({ auto_reload_in_progress: null }); + expect((supabaseAdmin as any).update).toHaveBeenCalledWith({ auto_reload_in_progress: null }); }); it("should handle non-Error objects thrown during processing", async () => { From ab502166910d20c1302d68c2764078637ed48246 Mon Sep 17 00:00:00 2001 From: "gitauto-ai[bot]" <161652217+gitauto-ai[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:01:54 +0000 Subject: [PATCH 5/5] Update app/actions/supabase/owners/check-all-auto-reloads.test.ts [skip ci] Co-Authored-By: hiroshinishio --- .../owners/check-all-auto-reloads.test.ts | 122 ++++++++++-------- 1 file changed, 69 insertions(+), 53 deletions(-) diff --git a/app/actions/supabase/owners/check-all-auto-reloads.test.ts b/app/actions/supabase/owners/check-all-auto-reloads.test.ts index bb4e6982..df4c28f8 100644 --- a/app/actions/supabase/owners/check-all-auto-reloads.test.ts +++ b/app/actions/supabase/owners/check-all-auto-reloads.test.ts @@ -1,25 +1,33 @@ process.env.STRIPE_SECRET_KEY = "sk_test_dummy"; -process.env.STRIPE_SECRET_KEY = "sk_test_dummy"; - import { checkAllAutoReloads } from "./check-all-auto-reloads"; import { supabaseAdmin } from "@/lib/supabase/server"; import { chargeSavedPaymentMethod } from "@/app/actions/stripe/charge-saved-payment-method"; import { slackUs } from "@/app/actions/slack/slack-us"; import { validateAutoReloadSpendingLimit } from "./validate-spending-limit"; +const mockSupabase = { + from: jest.fn().mockReturnThis(), + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockReturnThis(), + neq: jest.fn().mockReturnThis(), + update: jest.fn().mockReturnThis(), + rpc: jest.fn(), + then: jest.fn().mockImplementation(function (resolve) { + resolve((mockSupabase as any)._resolveValue || { data: null, error: null }); + }), +}; + jest.mock("@/lib/supabase/server", () => ({ - supabaseAdmin: { - from: jest.fn().mockReturnThis(), - select: jest.fn().mockReturnThis(), - eq: jest.fn().mockReturnThis(), - neq: jest.fn().mockReturnThis(), - update: jest.fn().mockReturnThis(), - rpc: jest.fn(), - }, + supabaseAdmin: mockSupabase, })); -jest.mock("@/lib/stripe", () => ({ stripe: { customers: { retrieve: jest.fn() }, paymentIntents: { create: jest.fn() } } })); +jest.mock("@/lib/stripe", () => ({ + stripe: { + customers: { retrieve: jest.fn() }, + paymentIntents: { create: jest.fn() }, + }, +})); jest.mock("@/app/actions/stripe/charge-saved-payment-method"); jest.mock("@/app/actions/slack/slack-us"); jest.mock("./validate-spending-limit"); @@ -27,6 +35,7 @@ jest.mock("./validate-spending-limit"); describe("checkAllAutoReloads solitary", () => { beforeEach(() => { jest.clearAllMocks(); + (mockSupabase as any)._resolveValue = undefined; // Mock console.log/error to keep test output clean jest.spyOn(console, "log").mockImplementation(() => {}); jest.spyOn(console, "error").mockImplementation(() => {}); @@ -40,17 +49,19 @@ describe("checkAllAutoReloads solitary", () => { it("should throw error if fetching owners fails", async () => { // Verify that if Supabase returns an error during initial fetch, it's thrown - ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ + (mockSupabase as any)._resolveValue = { data: null, error: { message: "Database connection error" }, - }); + }; - await expect(checkAllAutoReloads()).rejects.toThrow("Failed to fetch owners for auto-reload: Database connection error"); + await expect(checkAllAutoReloads()).rejects.toThrow( + "Failed to fetch owners for auto-reload: Database connection error" + ); }); it("should return success with 0 processed if no owners are eligible", async () => { // Verify behavior when no owners meet the criteria (enabled, has stripe id, balance <= threshold) - ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ + (mockSupabase as any)._resolveValue = { data: [ { owner_id: 1, @@ -61,7 +72,7 @@ describe("checkAllAutoReloads solitary", () => { }, ], error: null, - }); + }; const result = await checkAllAutoReloads(); @@ -74,7 +85,7 @@ describe("checkAllAutoReloads solitary", () => { it("should skip owner if lock cannot be acquired", async () => { // Verify that if acquire_auto_reload_lock returns 0, the owner is skipped - ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ + (mockSupabase as any)._resolveValue = { data: [ { owner_id: 1, @@ -85,9 +96,9 @@ describe("checkAllAutoReloads solitary", () => { }, ], error: null, - }); + }; - (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 0 }); + (mockSupabase.rpc as jest.Mock).mockResolvedValueOnce({ data: 0 }); const result = await checkAllAutoReloads(); @@ -97,12 +108,12 @@ describe("checkAllAutoReloads solitary", () => { reason: "Auto-reload already in progress", }); // Ensure lock release is still called in finally - expect((supabaseAdmin as any).update).toHaveBeenCalledWith({ auto_reload_in_progress: null }); + expect(mockSupabase.update).toHaveBeenCalledWith({ auto_reload_in_progress: null }); }); it("should skip owner if target amount is less than or equal to current balance", async () => { // Verify that if target <= balance, it's skipped to avoid zero/negative charges - ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ + (mockSupabase as any)._resolveValue = { data: [ { owner_id: 1, @@ -113,9 +124,9 @@ describe("checkAllAutoReloads solitary", () => { }, ], error: null, - }); + }; - (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); + (mockSupabase.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); const result = await checkAllAutoReloads(); @@ -128,7 +139,7 @@ describe("checkAllAutoReloads solitary", () => { it("should skip owner if spending limit is not allowed", async () => { // Verify that if validateAutoReloadSpendingLimit returns allowed: false, it's skipped - ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ + (mockSupabase as any)._resolveValue = { data: [ { owner_id: 1, @@ -139,9 +150,9 @@ describe("checkAllAutoReloads solitary", () => { }, ], error: null, - }); + }; - (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); + (mockSupabase.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); (validateAutoReloadSpendingLimit as jest.Mock).mockResolvedValueOnce({ allowed: false, reason: "Monthly spending limit reached", @@ -158,7 +169,7 @@ describe("checkAllAutoReloads solitary", () => { it("should skip owner if adjusted amount is zero or negative", async () => { // Verify that if spending limit adjusts the amount to <= 0, it's skipped - ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ + (mockSupabase as any)._resolveValue = { data: [ { owner_id: 1, @@ -169,9 +180,9 @@ describe("checkAllAutoReloads solitary", () => { }, ], error: null, - }); + }; - (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); + (mockSupabase.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); (validateAutoReloadSpendingLimit as jest.Mock).mockResolvedValueOnce({ allowed: true, adjustedAmountUsd: 0, @@ -189,7 +200,7 @@ describe("checkAllAutoReloads solitary", () => { it("should successfully charge owner and log adjusted amount if applicable", async () => { // Verify happy path where charge is successful and amount was adjusted - ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ + (mockSupabase as any)._resolveValue = { data: [ { owner_id: 1, @@ -200,9 +211,9 @@ describe("checkAllAutoReloads solitary", () => { }, ], error: null, - }); + }; - (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); + (mockSupabase.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); (validateAutoReloadSpendingLimit as jest.Mock).mockResolvedValueOnce({ allowed: true, adjustedAmountUsd: 40, // Adjusted from 90 @@ -226,7 +237,7 @@ describe("checkAllAutoReloads solitary", () => { it("should successfully charge owner without adjustment", async () => { // Verify happy path where charge is successful and no adjustment was needed - ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ + (mockSupabase as any)._resolveValue = { data: [ { owner_id: 1, @@ -237,9 +248,9 @@ describe("checkAllAutoReloads solitary", () => { }, ], error: null, - }); + }; - (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); + (mockSupabase.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); (validateAutoReloadSpendingLimit as jest.Mock).mockResolvedValueOnce({ allowed: true, adjustedAmountUsd: 90, @@ -263,7 +274,7 @@ describe("checkAllAutoReloads solitary", () => { it("should handle charge failure with error message and notify Slack", async () => { // Verify that if charge fails, it's recorded, Slack is notified, and console.error is called for non-test customers - ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ + (mockSupabase as any)._resolveValue = { data: [ { owner_id: 1, @@ -274,9 +285,9 @@ describe("checkAllAutoReloads solitary", () => { }, ], error: null, - }); + }; - (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); + (mockSupabase.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); (validateAutoReloadSpendingLimit as jest.Mock).mockResolvedValueOnce({ allowed: true, adjustedAmountUsd: 90, @@ -296,12 +307,15 @@ describe("checkAllAutoReloads solitary", () => { }); expect(slackUs).toHaveBeenCalledWith(expect.stringContaining("❌ Auto-reload failed!")); expect(slackUs).toHaveBeenCalledWith(expect.stringContaining("Error: Card declined")); - expect(console.error).toHaveBeenCalledWith(expect.stringContaining("❌ Auto-reload failed for owner 1:"), "Card declined"); + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining("❌ Auto-reload failed for owner 1:"), + "Card declined" + ); }); it("should handle charge failure without error message and not log for test customers", async () => { // Verify that if charge fails without error message, a default is used, and no console.error for test customers - ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ + (mockSupabase as any)._resolveValue = { data: [ { owner_id: 1, @@ -312,9 +326,9 @@ describe("checkAllAutoReloads solitary", () => { }, ], error: null, - }); + }; - (supabaseAdmin.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); + (mockSupabase.rpc as jest.Mock).mockResolvedValueOnce({ data: 1 }); (validateAutoReloadSpendingLimit as jest.Mock).mockResolvedValueOnce({ allowed: true, adjustedAmountUsd: 90, @@ -338,7 +352,7 @@ describe("checkAllAutoReloads solitary", () => { it("should handle unexpected errors during processing and notify Slack", async () => { // Verify that if an unexpected error occurs in the loop, it's caught, recorded, and Slack is notified - ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ + (mockSupabase as any)._resolveValue = { data: [ { owner_id: 1, @@ -349,9 +363,9 @@ describe("checkAllAutoReloads solitary", () => { }, ], error: null, - }); + }; - (supabaseAdmin.rpc as jest.Mock).mockRejectedValueOnce(new Error("Unexpected RPC failure")); + (mockSupabase.rpc as jest.Mock).mockRejectedValueOnce(new Error("Unexpected RPC failure")); const result = await checkAllAutoReloads(); @@ -362,12 +376,12 @@ describe("checkAllAutoReloads solitary", () => { }); expect(slackUs).toHaveBeenCalledWith(expect.stringContaining("❌ Auto-reload error!")); expect(slackUs).toHaveBeenCalledWith(expect.stringContaining("Error: Unexpected RPC failure")); - expect((supabaseAdmin as any).update).toHaveBeenCalledWith({ auto_reload_in_progress: null }); + expect(mockSupabase.update).toHaveBeenCalledWith({ auto_reload_in_progress: null }); }); it("should handle non-Error objects thrown during processing", async () => { // Verify that if a non-Error object is thrown, it's handled gracefully - ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ + (mockSupabase as any)._resolveValue = { data: [ { owner_id: 1, @@ -378,9 +392,9 @@ describe("checkAllAutoReloads solitary", () => { }, ], error: null, - }); + }; - (supabaseAdmin.rpc as jest.Mock).mockRejectedValueOnce("String error"); + (mockSupabase.rpc as jest.Mock).mockRejectedValueOnce("String error"); const result = await checkAllAutoReloads(); @@ -393,7 +407,7 @@ describe("checkAllAutoReloads solitary", () => { it("should process multiple owners and return correct summary", async () => { // Verify that multiple owners are processed and the success count is correct - ((supabaseAdmin as any).select as jest.Mock).mockResolvedValueOnce({ + (mockSupabase as any)._resolveValue = { data: [ { owner_id: 1, @@ -411,9 +425,9 @@ describe("checkAllAutoReloads solitary", () => { }, ], error: null, - }); + }; - (supabaseAdmin.rpc as jest.Mock) + (mockSupabase.rpc as jest.Mock) .mockResolvedValueOnce({ data: 1 }) // Owner 1 lock .mockResolvedValueOnce({ data: 1 }); // Owner 2 lock @@ -433,6 +447,8 @@ describe("checkAllAutoReloads solitary", () => { expect(result.results).toHaveLength(2); expect(result.results[0].success).toBe(true); expect(result.results[1].success).toBe(false); - expect(console.log).toHaveBeenCalledWith(expect.stringContaining("Auto-reload completed: 1/2 successful")); + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining("Auto-reload completed: 1/2 successful") + ); }); });