|
|
@@ -0,0 +1,231 @@
|
|
|
+import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
+import { locales } from "@/i18n/config";
|
|
|
+
|
|
|
+// Mock dependencies
|
|
|
+const getSessionMock = vi.fn();
|
|
|
+const revalidatePathMock = vi.fn();
|
|
|
+const invalidateSystemSettingsCacheMock = vi.fn();
|
|
|
+const updateSystemSettingsMock = vi.fn();
|
|
|
+const getSystemSettingsMock = vi.fn();
|
|
|
+
|
|
|
+vi.mock("@/lib/auth", () => ({
|
|
|
+ getSession: () => getSessionMock(),
|
|
|
+}));
|
|
|
+
|
|
|
+vi.mock("next/cache", () => ({
|
|
|
+ revalidatePath: (...args: unknown[]) => revalidatePathMock(...args),
|
|
|
+}));
|
|
|
+
|
|
|
+vi.mock("@/lib/config", () => ({
|
|
|
+ invalidateSystemSettingsCache: () => invalidateSystemSettingsCacheMock(),
|
|
|
+}));
|
|
|
+
|
|
|
+vi.mock("@/lib/logger", () => ({
|
|
|
+ logger: {
|
|
|
+ trace: vi.fn(),
|
|
|
+ debug: vi.fn(),
|
|
|
+ info: vi.fn(),
|
|
|
+ warn: vi.fn(),
|
|
|
+ error: vi.fn(),
|
|
|
+ },
|
|
|
+}));
|
|
|
+
|
|
|
+vi.mock("@/lib/utils/timezone", () => ({
|
|
|
+ resolveSystemTimezone: vi.fn(async () => "UTC"),
|
|
|
+ isValidIANATimezone: vi.fn(() => true),
|
|
|
+}));
|
|
|
+
|
|
|
+vi.mock("@/repository/system-config", () => ({
|
|
|
+ getSystemSettings: () => getSystemSettingsMock(),
|
|
|
+ updateSystemSettings: (...args: unknown[]) => updateSystemSettingsMock(...args),
|
|
|
+}));
|
|
|
+
|
|
|
+// Import the action after mocks are set up
|
|
|
+import { saveSystemSettings } from "@/actions/system-config";
|
|
|
+
|
|
|
+describe("saveSystemSettings", () => {
|
|
|
+ beforeEach(() => {
|
|
|
+ vi.clearAllMocks();
|
|
|
+ // Default: admin session
|
|
|
+ getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
|
|
|
+ // Default: successful update
|
|
|
+ updateSystemSettingsMock.mockResolvedValue({
|
|
|
+ id: 1,
|
|
|
+ siteTitle: "Test Site",
|
|
|
+ allowGlobalUsageView: false,
|
|
|
+ currencyDisplay: "CNY",
|
|
|
+ billingModelSource: "original",
|
|
|
+ timezone: null,
|
|
|
+ enableAutoCleanup: false,
|
|
|
+ cleanupRetentionDays: 30,
|
|
|
+ cleanupSchedule: "0 3 * * *",
|
|
|
+ cleanupBatchSize: 1000,
|
|
|
+ enableClientVersionCheck: false,
|
|
|
+ verboseProviderError: false,
|
|
|
+ enableHttp2: false,
|
|
|
+ interceptAnthropicWarmupRequests: false,
|
|
|
+ enableThinkingSignatureRectifier: false,
|
|
|
+ enableCodexSessionIdCompletion: false,
|
|
|
+ enableResponseFixer: false,
|
|
|
+ responseFixerConfig: {
|
|
|
+ fixEncoding: false,
|
|
|
+ fixStreamingJson: false,
|
|
|
+ fixEmptyResponse: false,
|
|
|
+ fixContentBlockDelta: false,
|
|
|
+ maxRetries: 3,
|
|
|
+ timeout: 5000,
|
|
|
+ },
|
|
|
+ quotaDbRefreshIntervalSeconds: 60,
|
|
|
+ quotaLeasePercent5h: 0.05,
|
|
|
+ quotaLeasePercentDaily: 0.05,
|
|
|
+ quotaLeasePercentWeekly: 0.05,
|
|
|
+ quotaLeasePercentMonthly: 0.05,
|
|
|
+ quotaLeaseCapUsd: null,
|
|
|
+ createdAt: new Date(),
|
|
|
+ updatedAt: new Date(),
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ it("should return error when user is not admin", async () => {
|
|
|
+ getSessionMock.mockResolvedValue({ user: { id: 1, role: "user" } });
|
|
|
+
|
|
|
+ const result = await saveSystemSettings({ siteTitle: "New Title" });
|
|
|
+
|
|
|
+ expect(result.ok).toBe(false);
|
|
|
+ expect(result.error).toContain("无权限");
|
|
|
+ expect(updateSystemSettingsMock).not.toHaveBeenCalled();
|
|
|
+ });
|
|
|
+
|
|
|
+ it("should return error when user is not logged in", async () => {
|
|
|
+ getSessionMock.mockResolvedValue(null);
|
|
|
+
|
|
|
+ const result = await saveSystemSettings({ siteTitle: "New Title" });
|
|
|
+
|
|
|
+ expect(result.ok).toBe(false);
|
|
|
+ expect(result.error).toContain("无权限");
|
|
|
+ expect(updateSystemSettingsMock).not.toHaveBeenCalled();
|
|
|
+ });
|
|
|
+
|
|
|
+ it("should call updateSystemSettings with validated data", async () => {
|
|
|
+ const result = await saveSystemSettings({
|
|
|
+ siteTitle: "New Site Title",
|
|
|
+ verboseProviderError: true,
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(result.ok).toBe(true);
|
|
|
+ expect(updateSystemSettingsMock).toHaveBeenCalledWith(
|
|
|
+ expect.objectContaining({
|
|
|
+ siteTitle: "New Site Title",
|
|
|
+ verboseProviderError: true,
|
|
|
+ })
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ it("should invalidate system settings cache after successful save", async () => {
|
|
|
+ await saveSystemSettings({ siteTitle: "New Title" });
|
|
|
+
|
|
|
+ expect(invalidateSystemSettingsCacheMock).toHaveBeenCalled();
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("revalidatePath locale coverage", () => {
|
|
|
+ it("should revalidate paths for ALL supported locales", async () => {
|
|
|
+ await saveSystemSettings({ siteTitle: "New Title" });
|
|
|
+
|
|
|
+ // Collect all revalidatePath calls
|
|
|
+ const calls = revalidatePathMock.mock.calls.map((call) => call[0]);
|
|
|
+
|
|
|
+ // Check that each locale's settings/config path is revalidated
|
|
|
+ for (const locale of locales) {
|
|
|
+ const expectedSettingsPath = `/${locale}/settings/config`;
|
|
|
+ expect(calls).toContain(expectedSettingsPath);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it("should revalidate dashboard paths for ALL supported locales", async () => {
|
|
|
+ await saveSystemSettings({ siteTitle: "New Title" });
|
|
|
+
|
|
|
+ const calls = revalidatePathMock.mock.calls.map((call) => call[0]);
|
|
|
+
|
|
|
+ // Check that each locale's dashboard path is revalidated
|
|
|
+ for (const locale of locales) {
|
|
|
+ const expectedDashboardPath = `/${locale}/dashboard`;
|
|
|
+ expect(calls).toContain(expectedDashboardPath);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it("should revalidate root layout", async () => {
|
|
|
+ await saveSystemSettings({ siteTitle: "New Title" });
|
|
|
+
|
|
|
+ // Check that root layout is revalidated
|
|
|
+ expect(revalidatePathMock).toHaveBeenCalledWith("/", "layout");
|
|
|
+ });
|
|
|
+
|
|
|
+ it("should call revalidatePath at least 2 * locales.length + 1 times", async () => {
|
|
|
+ await saveSystemSettings({ siteTitle: "New Title" });
|
|
|
+
|
|
|
+ // 2 paths per locale (settings/config + dashboard) + 1 for root layout
|
|
|
+ const expectedMinCalls = locales.length * 2 + 1;
|
|
|
+ expect(revalidatePathMock).toHaveBeenCalledTimes(expectedMinCalls);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ it("should return updated settings on success", async () => {
|
|
|
+ const mockUpdated = {
|
|
|
+ id: 1,
|
|
|
+ siteTitle: "Updated Title",
|
|
|
+ allowGlobalUsageView: true,
|
|
|
+ currencyDisplay: "USD",
|
|
|
+ billingModelSource: "original",
|
|
|
+ timezone: "America/New_York",
|
|
|
+ enableAutoCleanup: false,
|
|
|
+ cleanupRetentionDays: 30,
|
|
|
+ cleanupSchedule: "0 3 * * *",
|
|
|
+ cleanupBatchSize: 1000,
|
|
|
+ enableClientVersionCheck: false,
|
|
|
+ verboseProviderError: true,
|
|
|
+ enableHttp2: true,
|
|
|
+ interceptAnthropicWarmupRequests: false,
|
|
|
+ enableThinkingSignatureRectifier: false,
|
|
|
+ enableCodexSessionIdCompletion: false,
|
|
|
+ enableResponseFixer: false,
|
|
|
+ responseFixerConfig: {
|
|
|
+ fixEncoding: false,
|
|
|
+ fixStreamingJson: false,
|
|
|
+ fixEmptyResponse: false,
|
|
|
+ fixContentBlockDelta: false,
|
|
|
+ maxRetries: 3,
|
|
|
+ timeout: 5000,
|
|
|
+ },
|
|
|
+ quotaDbRefreshIntervalSeconds: 60,
|
|
|
+ quotaLeasePercent5h: 0.05,
|
|
|
+ quotaLeasePercentDaily: 0.05,
|
|
|
+ quotaLeasePercentWeekly: 0.05,
|
|
|
+ quotaLeasePercentMonthly: 0.05,
|
|
|
+ quotaLeaseCapUsd: null,
|
|
|
+ createdAt: new Date(),
|
|
|
+ updatedAt: new Date(),
|
|
|
+ };
|
|
|
+ updateSystemSettingsMock.mockResolvedValue(mockUpdated);
|
|
|
+
|
|
|
+ const result = await saveSystemSettings({
|
|
|
+ siteTitle: "Updated Title",
|
|
|
+ allowGlobalUsageView: true,
|
|
|
+ currencyDisplay: "USD",
|
|
|
+ timezone: "America/New_York",
|
|
|
+ verboseProviderError: true,
|
|
|
+ enableHttp2: true,
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(result.ok).toBe(true);
|
|
|
+ expect(result.data).toEqual(mockUpdated);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("should handle repository errors gracefully", async () => {
|
|
|
+ updateSystemSettingsMock.mockRejectedValue(new Error("Database error"));
|
|
|
+
|
|
|
+ const result = await saveSystemSettings({ siteTitle: "New Title" });
|
|
|
+
|
|
|
+ expect(result.ok).toBe(false);
|
|
|
+ expect(result.error).toContain("Database error");
|
|
|
+ });
|
|
|
+});
|