|
|
|
|
@@ -98,6 +98,7 @@ describe("License Core Logic", () => {
|
|
|
|
|
mockCache.get.mockReset();
|
|
|
|
|
mockCache.set.mockReset();
|
|
|
|
|
mockCache.del.mockReset();
|
|
|
|
|
mockCache.exists.mockReset();
|
|
|
|
|
mockCache.withCache.mockReset();
|
|
|
|
|
mockLogger.error.mockReset();
|
|
|
|
|
mockLogger.warn.mockReset();
|
|
|
|
|
@@ -105,9 +106,10 @@ describe("License Core Logic", () => {
|
|
|
|
|
mockLogger.debug.mockReset();
|
|
|
|
|
|
|
|
|
|
// Set up default mock implementations for Result types
|
|
|
|
|
// fetchLicense uses get + exists; getPreviousResult uses get with :previous_result key
|
|
|
|
|
mockCache.get.mockResolvedValue({ ok: true, data: null });
|
|
|
|
|
mockCache.exists.mockResolvedValue({ ok: true, data: false }); // default: cache miss
|
|
|
|
|
mockCache.set.mockResolvedValue({ ok: true });
|
|
|
|
|
mockCache.withCache.mockImplementation(async (fn) => await fn());
|
|
|
|
|
|
|
|
|
|
vi.mocked(prisma.response.count).mockResolvedValue(100);
|
|
|
|
|
vi.mocked(prisma.organization.findFirst).mockResolvedValue({
|
|
|
|
|
@@ -164,16 +166,22 @@ describe("License Core Logic", () => {
|
|
|
|
|
const { getEnterpriseLicense } = await import("./license");
|
|
|
|
|
const fetch = (await import("node-fetch")).default as Mock;
|
|
|
|
|
|
|
|
|
|
// Mock cache.withCache to return cached license details (simulating cache hit)
|
|
|
|
|
mockCache.withCache.mockResolvedValue(mockFetchedLicenseDetails);
|
|
|
|
|
// Mock cache hit: get returns license for status key, exists returns true
|
|
|
|
|
mockCache.get.mockImplementation(async (key: string) => {
|
|
|
|
|
if (key.includes(":previous_result")) {
|
|
|
|
|
return { ok: true, data: null };
|
|
|
|
|
}
|
|
|
|
|
if (key.includes(":status")) {
|
|
|
|
|
return { ok: true, data: mockFetchedLicenseDetails };
|
|
|
|
|
}
|
|
|
|
|
return { ok: true, data: null };
|
|
|
|
|
});
|
|
|
|
|
mockCache.exists.mockResolvedValue({ ok: true, data: true });
|
|
|
|
|
|
|
|
|
|
const license = await getEnterpriseLicense();
|
|
|
|
|
expect(license).toEqual(expectedActiveLicenseState);
|
|
|
|
|
expect(mockCache.withCache).toHaveBeenCalledWith(
|
|
|
|
|
expect.any(Function),
|
|
|
|
|
expect.stringContaining("fb:license:"),
|
|
|
|
|
expect.any(Number)
|
|
|
|
|
);
|
|
|
|
|
expect(mockCache.get).toHaveBeenCalledWith(expect.stringContaining("fb:license:"));
|
|
|
|
|
expect(mockCache.exists).toHaveBeenCalledWith(expect.stringContaining("fb:license:"));
|
|
|
|
|
expect(fetch).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -181,9 +189,7 @@ describe("License Core Logic", () => {
|
|
|
|
|
const { getEnterpriseLicense } = await import("./license");
|
|
|
|
|
const fetch = (await import("node-fetch")).default as Mock;
|
|
|
|
|
|
|
|
|
|
// Mock cache.withCache to execute the function (simulating cache miss)
|
|
|
|
|
mockCache.withCache.mockImplementation(async (fn) => await fn());
|
|
|
|
|
|
|
|
|
|
// Default mocks give cache miss (get null, exists false)
|
|
|
|
|
fetch.mockResolvedValueOnce({
|
|
|
|
|
ok: true,
|
|
|
|
|
json: async () => ({ data: mockFetchedLicenseDetails }),
|
|
|
|
|
@@ -192,11 +198,8 @@ describe("License Core Logic", () => {
|
|
|
|
|
const license = await getEnterpriseLicense();
|
|
|
|
|
|
|
|
|
|
expect(fetch).toHaveBeenCalledTimes(1);
|
|
|
|
|
expect(mockCache.withCache).toHaveBeenCalledWith(
|
|
|
|
|
expect.any(Function),
|
|
|
|
|
expect.stringContaining("fb:license:"),
|
|
|
|
|
expect.any(Number)
|
|
|
|
|
);
|
|
|
|
|
expect(mockCache.get).toHaveBeenCalledWith(expect.stringContaining("fb:license:"));
|
|
|
|
|
expect(mockCache.exists).toHaveBeenCalledWith(expect.stringContaining("fb:license:"));
|
|
|
|
|
expect(license).toEqual(expectedActiveLicenseState);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -212,11 +215,9 @@ describe("License Core Logic", () => {
|
|
|
|
|
version: 1,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Mock cache.withCache to return null (simulating fetch failure)
|
|
|
|
|
mockCache.withCache.mockResolvedValue(null);
|
|
|
|
|
|
|
|
|
|
// Mock cache.get to return previous result when requested
|
|
|
|
|
mockCache.get.mockImplementation(async (key) => {
|
|
|
|
|
// Cache miss for fetch (get null, exists false) -> fetch fails -> null
|
|
|
|
|
// getPreviousResult returns previous result for :previous_result key
|
|
|
|
|
mockCache.get.mockImplementation(async (key: string) => {
|
|
|
|
|
if (key.includes(":previous_result")) {
|
|
|
|
|
return { ok: true, data: mockPreviousResult };
|
|
|
|
|
}
|
|
|
|
|
@@ -227,7 +228,7 @@ describe("License Core Logic", () => {
|
|
|
|
|
|
|
|
|
|
const license = await getEnterpriseLicense();
|
|
|
|
|
|
|
|
|
|
expect(mockCache.withCache).toHaveBeenCalled();
|
|
|
|
|
expect(mockCache.get).toHaveBeenCalled();
|
|
|
|
|
expect(license).toEqual({
|
|
|
|
|
active: true,
|
|
|
|
|
features: mockPreviousResult.features,
|
|
|
|
|
@@ -250,11 +251,8 @@ describe("License Core Logic", () => {
|
|
|
|
|
version: 1,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Mock cache.withCache to return null (simulating fetch failure)
|
|
|
|
|
mockCache.withCache.mockResolvedValue(null);
|
|
|
|
|
|
|
|
|
|
// Mock cache.get to return previous result when requested
|
|
|
|
|
mockCache.get.mockImplementation(async (key) => {
|
|
|
|
|
// Cache miss -> fetch fails -> null; getPreviousResult returns old previous result
|
|
|
|
|
mockCache.get.mockImplementation(async (key: string) => {
|
|
|
|
|
if (key.includes(":previous_result")) {
|
|
|
|
|
return { ok: true, data: mockPreviousResult };
|
|
|
|
|
}
|
|
|
|
|
@@ -265,7 +263,7 @@ describe("License Core Logic", () => {
|
|
|
|
|
|
|
|
|
|
const license = await getEnterpriseLicense();
|
|
|
|
|
|
|
|
|
|
expect(mockCache.withCache).toHaveBeenCalled();
|
|
|
|
|
expect(mockCache.get).toHaveBeenCalled();
|
|
|
|
|
expect(mockCache.set).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining("fb:license:"),
|
|
|
|
|
{
|
|
|
|
|
@@ -319,12 +317,7 @@ describe("License Core Logic", () => {
|
|
|
|
|
const { getEnterpriseLicense } = await import("./license");
|
|
|
|
|
const fetch = (await import("node-fetch")).default as Mock;
|
|
|
|
|
|
|
|
|
|
// Mock cache.withCache to return null (simulating fetch failure)
|
|
|
|
|
mockCache.withCache.mockResolvedValue(null);
|
|
|
|
|
|
|
|
|
|
// Mock cache.get to return no previous result
|
|
|
|
|
mockCache.get.mockResolvedValue({ ok: true, data: null });
|
|
|
|
|
|
|
|
|
|
// Cache miss -> fetch fails; no previous result (default get returns null)
|
|
|
|
|
fetch.mockRejectedValueOnce(new Error("Network error"));
|
|
|
|
|
|
|
|
|
|
const license = await getEnterpriseLicense();
|
|
|
|
|
@@ -397,49 +390,52 @@ describe("License Core Logic", () => {
|
|
|
|
|
});
|
|
|
|
|
expect(mockCache.get).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockCache.set).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockCache.withCache).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockCache.exists).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("should handle fetch throwing an error and use grace period or return inactive", async () => {
|
|
|
|
|
const { getEnterpriseLicense } = await import("./license");
|
|
|
|
|
const fetch = (await import("node-fetch")).default as Mock;
|
|
|
|
|
|
|
|
|
|
// Mock cache.withCache to return null (simulating fetch failure)
|
|
|
|
|
mockCache.withCache.mockResolvedValue(null);
|
|
|
|
|
|
|
|
|
|
// Mock cache.get to return no previous result
|
|
|
|
|
mockCache.get.mockResolvedValue({ ok: true, data: null });
|
|
|
|
|
|
|
|
|
|
fetch.mockRejectedValueOnce(new Error("Network error"));
|
|
|
|
|
|
|
|
|
|
const license = await getEnterpriseLicense();
|
|
|
|
|
expect(license).toEqual({
|
|
|
|
|
active: false,
|
|
|
|
|
features: null,
|
|
|
|
|
lastChecked: expect.any(Date),
|
|
|
|
|
isPendingDowngrade: false,
|
|
|
|
|
fallbackLevel: "default" as const,
|
|
|
|
|
status: "no-license" as const,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("getLicenseFeatures", () => {
|
|
|
|
|
test("should return features if license is active", async () => {
|
|
|
|
|
// Set up environment before import
|
|
|
|
|
vi.stubGlobal("window", undefined);
|
|
|
|
|
// Runs after "no-license" test which uses vi.doMock; env may have empty key
|
|
|
|
|
vi.resetModules();
|
|
|
|
|
vi.doMock("@/lib/env", () => ({
|
|
|
|
|
env: {
|
|
|
|
|
ENTERPRISE_LICENSE_KEY: "test-license-key",
|
|
|
|
|
ENVIRONMENT: "production",
|
|
|
|
|
VERCEL_URL: "some.vercel.url",
|
|
|
|
|
FORMBRICKS_COM_URL: "https://app.formbricks.com",
|
|
|
|
|
HTTPS_PROXY: undefined,
|
|
|
|
|
HTTP_PROXY: undefined,
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
// Mock cache.withCache to return license details
|
|
|
|
|
mockCache.withCache.mockResolvedValue({
|
|
|
|
|
status: "active",
|
|
|
|
|
|
|
|
|
|
const { getEnterpriseLicense } = await import("./license");
|
|
|
|
|
const fetch = (await import("node-fetch")).default as Mock;
|
|
|
|
|
|
|
|
|
|
// Cache miss -> fetch throws -> no previous result -> handleInitialFailure
|
|
|
|
|
fetch.mockRejectedValueOnce(new Error("Network error"));
|
|
|
|
|
|
|
|
|
|
const license = await getEnterpriseLicense();
|
|
|
|
|
expect(license).toEqual({
|
|
|
|
|
active: false,
|
|
|
|
|
features: expect.objectContaining({
|
|
|
|
|
isMultiOrgEnabled: false,
|
|
|
|
|
projects: 3,
|
|
|
|
|
removeBranding: false,
|
|
|
|
|
}),
|
|
|
|
|
lastChecked: expect.any(Date),
|
|
|
|
|
isPendingDowngrade: false,
|
|
|
|
|
fallbackLevel: "default" as const,
|
|
|
|
|
status: "unreachable" as const,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("getLicenseFeatures", () => {
|
|
|
|
|
test("should return features if license is active", async () => {
|
|
|
|
|
vi.resetModules();
|
|
|
|
|
vi.stubGlobal("window", undefined);
|
|
|
|
|
// Mock cache hit for fetchLicense (get returns license, exists true)
|
|
|
|
|
const activeLicenseDetails = {
|
|
|
|
|
status: "active" as const,
|
|
|
|
|
features: {
|
|
|
|
|
isMultiOrgEnabled: true,
|
|
|
|
|
contacts: true,
|
|
|
|
|
@@ -452,9 +448,22 @@ describe("License Core Logic", () => {
|
|
|
|
|
spamProtection: true,
|
|
|
|
|
ai: true,
|
|
|
|
|
auditLogs: true,
|
|
|
|
|
multiLanguageSurveys: true,
|
|
|
|
|
accessControl: true,
|
|
|
|
|
quotas: true,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
mockCache.get.mockImplementation(async (key: string) => {
|
|
|
|
|
if (key.includes(":previous_result")) {
|
|
|
|
|
return { ok: true, data: null };
|
|
|
|
|
}
|
|
|
|
|
if (key.includes(":status")) {
|
|
|
|
|
return { ok: true, data: activeLicenseDetails };
|
|
|
|
|
}
|
|
|
|
|
return { ok: true, data: null };
|
|
|
|
|
});
|
|
|
|
|
// Import after env and mocks are set
|
|
|
|
|
mockCache.exists.mockResolvedValue({ ok: true, data: true });
|
|
|
|
|
|
|
|
|
|
const { getLicenseFeatures } = await import("./license");
|
|
|
|
|
const features = await getLicenseFeatures();
|
|
|
|
|
expect(features).toEqual({
|
|
|
|
|
@@ -469,14 +478,47 @@ describe("License Core Logic", () => {
|
|
|
|
|
spamProtection: true,
|
|
|
|
|
ai: true,
|
|
|
|
|
auditLogs: true,
|
|
|
|
|
multiLanguageSurveys: true,
|
|
|
|
|
accessControl: true,
|
|
|
|
|
quotas: true,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("should return null if license is inactive", async () => {
|
|
|
|
|
const { getLicenseFeatures } = await import("./license");
|
|
|
|
|
|
|
|
|
|
// Mock cache.withCache to return expired license
|
|
|
|
|
mockCache.withCache.mockResolvedValue({ status: "expired", features: null });
|
|
|
|
|
// Mock cache hit with expired license (features can be default for schema)
|
|
|
|
|
mockCache.get.mockImplementation(async (key: string) => {
|
|
|
|
|
if (key.includes(":previous_result")) {
|
|
|
|
|
return { ok: true, data: null };
|
|
|
|
|
}
|
|
|
|
|
if (key.includes(":status")) {
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
data: {
|
|
|
|
|
status: "expired",
|
|
|
|
|
features: {
|
|
|
|
|
isMultiOrgEnabled: false,
|
|
|
|
|
projects: 3,
|
|
|
|
|
twoFactorAuth: false,
|
|
|
|
|
sso: false,
|
|
|
|
|
whitelabel: false,
|
|
|
|
|
removeBranding: false,
|
|
|
|
|
contacts: false,
|
|
|
|
|
ai: false,
|
|
|
|
|
saml: false,
|
|
|
|
|
spamProtection: false,
|
|
|
|
|
auditLogs: false,
|
|
|
|
|
multiLanguageSurveys: false,
|
|
|
|
|
accessControl: false,
|
|
|
|
|
quotas: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return { ok: true, data: null };
|
|
|
|
|
});
|
|
|
|
|
mockCache.exists.mockResolvedValue({ ok: true, data: true });
|
|
|
|
|
|
|
|
|
|
const features = await getLicenseFeatures();
|
|
|
|
|
expect(features).toBeNull();
|
|
|
|
|
@@ -485,8 +527,8 @@ describe("License Core Logic", () => {
|
|
|
|
|
test("should return null if getEnterpriseLicense throws", async () => {
|
|
|
|
|
const { getLicenseFeatures } = await import("./license");
|
|
|
|
|
|
|
|
|
|
// Mock cache.withCache to throw an error
|
|
|
|
|
mockCache.withCache.mockRejectedValue(new Error("Cache error"));
|
|
|
|
|
// Mock cache.get to throw so getEnterpriseLicense fails
|
|
|
|
|
mockCache.get.mockRejectedValue(new Error("Cache error"));
|
|
|
|
|
|
|
|
|
|
const features = await getLicenseFeatures();
|
|
|
|
|
expect(features).toBeNull();
|
|
|
|
|
@@ -499,23 +541,59 @@ describe("License Core Logic", () => {
|
|
|
|
|
mockCache.get.mockReset();
|
|
|
|
|
mockCache.set.mockReset();
|
|
|
|
|
mockCache.del.mockReset();
|
|
|
|
|
mockCache.withCache.mockReset();
|
|
|
|
|
mockCache.exists.mockReset();
|
|
|
|
|
vi.resetModules();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("should use 'browser' as cache key in browser environment", async () => {
|
|
|
|
|
vi.stubGlobal("window", {});
|
|
|
|
|
|
|
|
|
|
// Set up default mock for cache.withCache
|
|
|
|
|
mockCache.withCache.mockImplementation(async (fn) => await fn());
|
|
|
|
|
// Ensure env has license key (previous "no-license" test may have poisoned env)
|
|
|
|
|
vi.doMock("@/lib/env", () => ({
|
|
|
|
|
env: {
|
|
|
|
|
ENTERPRISE_LICENSE_KEY: "test-license-key",
|
|
|
|
|
ENVIRONMENT: "production",
|
|
|
|
|
VERCEL_URL: "some.vercel.url",
|
|
|
|
|
FORMBRICKS_COM_URL: "https://app.formbricks.com",
|
|
|
|
|
HTTPS_PROXY: undefined,
|
|
|
|
|
HTTP_PROXY: undefined,
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Cache miss so fetch runs; mock get/exists for cache checks
|
|
|
|
|
mockCache.get.mockResolvedValue({ ok: true, data: null });
|
|
|
|
|
mockCache.exists.mockResolvedValue({ ok: true, data: false });
|
|
|
|
|
|
|
|
|
|
const fetch = (await import("node-fetch")).default as Mock;
|
|
|
|
|
fetch.mockResolvedValueOnce({
|
|
|
|
|
ok: true,
|
|
|
|
|
json: async () => ({
|
|
|
|
|
data: {
|
|
|
|
|
status: "active",
|
|
|
|
|
features: {
|
|
|
|
|
isMultiOrgEnabled: true,
|
|
|
|
|
projects: 5,
|
|
|
|
|
twoFactorAuth: true,
|
|
|
|
|
sso: true,
|
|
|
|
|
whitelabel: true,
|
|
|
|
|
removeBranding: true,
|
|
|
|
|
contacts: true,
|
|
|
|
|
ai: true,
|
|
|
|
|
saml: true,
|
|
|
|
|
spamProtection: true,
|
|
|
|
|
auditLogs: true,
|
|
|
|
|
multiLanguageSurveys: true,
|
|
|
|
|
accessControl: true,
|
|
|
|
|
quotas: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
const { getEnterpriseLicense } = await import("./license");
|
|
|
|
|
await getEnterpriseLicense();
|
|
|
|
|
expect(mockCache.withCache).toHaveBeenCalledWith(
|
|
|
|
|
expect.any(Function),
|
|
|
|
|
expect.stringContaining("fb:license:browser:status"),
|
|
|
|
|
expect.any(Number)
|
|
|
|
|
);
|
|
|
|
|
expect(mockCache.get).toHaveBeenCalledWith(expect.stringContaining("fb:license:browser:status"));
|
|
|
|
|
expect(mockCache.exists).toHaveBeenCalledWith(expect.stringContaining("fb:license:browser:status"));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("should use 'no-license' as cache key when ENTERPRISE_LICENSE_KEY is not set", async () => {
|
|
|
|
|
@@ -534,16 +612,19 @@ describe("License Core Logic", () => {
|
|
|
|
|
await getEnterpriseLicense();
|
|
|
|
|
// The cache should NOT be accessed if there is no license key
|
|
|
|
|
expect(mockCache.get).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockCache.withCache).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockCache.exists).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("should use hashed license key as cache key when ENTERPRISE_LICENSE_KEY is set", async () => {
|
|
|
|
|
vi.resetModules();
|
|
|
|
|
const testLicenseKey = "test-license-key";
|
|
|
|
|
vi.stubGlobal("window", undefined);
|
|
|
|
|
|
|
|
|
|
// Ensure env has license key (restore after "no-license" test)
|
|
|
|
|
vi.doMock("@/lib/env", () => ({
|
|
|
|
|
env: {
|
|
|
|
|
ENTERPRISE_LICENSE_KEY: testLicenseKey,
|
|
|
|
|
ENVIRONMENT: "production",
|
|
|
|
|
VERCEL_URL: "some.vercel.url",
|
|
|
|
|
FORMBRICKS_COM_URL: "https://app.formbricks.com",
|
|
|
|
|
HTTPS_PROXY: undefined,
|
|
|
|
|
@@ -551,22 +632,49 @@ describe("License Core Logic", () => {
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Set up default mock for cache.withCache
|
|
|
|
|
mockCache.withCache.mockImplementation(async (fn) => await fn());
|
|
|
|
|
mockCache.get.mockResolvedValue({ ok: true, data: null });
|
|
|
|
|
mockCache.exists.mockResolvedValue({ ok: true, data: false });
|
|
|
|
|
|
|
|
|
|
const fetch = (await import("node-fetch")).default as Mock;
|
|
|
|
|
fetch.mockResolvedValueOnce({
|
|
|
|
|
ok: true,
|
|
|
|
|
json: async () => ({
|
|
|
|
|
data: {
|
|
|
|
|
status: "active",
|
|
|
|
|
features: {
|
|
|
|
|
isMultiOrgEnabled: true,
|
|
|
|
|
projects: 5,
|
|
|
|
|
twoFactorAuth: true,
|
|
|
|
|
sso: true,
|
|
|
|
|
whitelabel: true,
|
|
|
|
|
removeBranding: true,
|
|
|
|
|
contacts: true,
|
|
|
|
|
ai: true,
|
|
|
|
|
saml: true,
|
|
|
|
|
spamProtection: true,
|
|
|
|
|
auditLogs: true,
|
|
|
|
|
multiLanguageSurveys: true,
|
|
|
|
|
accessControl: true,
|
|
|
|
|
quotas: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
const { hashString } = await import("@/lib/hash-string");
|
|
|
|
|
const expectedHash = hashString(testLicenseKey);
|
|
|
|
|
const { getEnterpriseLicense } = await import("./license");
|
|
|
|
|
await getEnterpriseLicense();
|
|
|
|
|
expect(mockCache.withCache).toHaveBeenCalledWith(
|
|
|
|
|
expect.any(Function),
|
|
|
|
|
expect.stringContaining(`fb:license:${expectedHash}:status`),
|
|
|
|
|
expect.any(Number)
|
|
|
|
|
);
|
|
|
|
|
expect(mockCache.get).toHaveBeenCalledWith(expect.stringContaining(`fb:license:${expectedHash}:status`));
|
|
|
|
|
expect(mockCache.exists).toHaveBeenCalledWith(expect.stringContaining(`fb:license:${expectedHash}:status`));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("Error and Warning Logging", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.resetModules();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("should log warning when setPreviousResult cache.set fails (line 176-178)", async () => {
|
|
|
|
|
const { getEnterpriseLicense } = await import("./license");
|
|
|
|
|
const fetch = (await import("node-fetch")).default as Mock;
|
|
|
|
|
@@ -591,10 +699,19 @@ describe("License Core Logic", () => {
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Mock successful fetch from API
|
|
|
|
|
mockCache.withCache.mockResolvedValue(mockFetchedLicenseDetails);
|
|
|
|
|
// Cache hit - fetchLicense returns cached license
|
|
|
|
|
mockCache.get.mockImplementation(async (key: string) => {
|
|
|
|
|
if (key.includes(":previous_result")) {
|
|
|
|
|
return { ok: true, data: null };
|
|
|
|
|
}
|
|
|
|
|
if (key.includes(":status")) {
|
|
|
|
|
return { ok: true, data: mockFetchedLicenseDetails };
|
|
|
|
|
}
|
|
|
|
|
return { ok: true, data: null };
|
|
|
|
|
});
|
|
|
|
|
mockCache.exists.mockResolvedValue({ ok: true, data: true });
|
|
|
|
|
|
|
|
|
|
// Mock cache.set to fail when saving previous result
|
|
|
|
|
// cache.set fails when setPreviousResult tries to save (called for previous_result key)
|
|
|
|
|
mockCache.set.mockResolvedValue({
|
|
|
|
|
ok: false,
|
|
|
|
|
error: new Error("Redis connection failed"),
|
|
|
|
|
@@ -602,7 +719,6 @@ describe("License Core Logic", () => {
|
|
|
|
|
|
|
|
|
|
await getEnterpriseLicense();
|
|
|
|
|
|
|
|
|
|
// Verify that the warning was logged
|
|
|
|
|
expect(mockLogger.warn).toHaveBeenCalledWith(
|
|
|
|
|
{ error: new Error("Redis connection failed") },
|
|
|
|
|
"Failed to cache previous result"
|
|
|
|
|
@@ -613,10 +729,7 @@ describe("License Core Logic", () => {
|
|
|
|
|
const { getEnterpriseLicense } = await import("./license");
|
|
|
|
|
const fetch = (await import("node-fetch")).default as Mock;
|
|
|
|
|
|
|
|
|
|
// Mock cache.withCache to execute the function (simulating cache miss)
|
|
|
|
|
mockCache.withCache.mockImplementation(async (fn) => await fn());
|
|
|
|
|
|
|
|
|
|
// Mock API response with 500 status
|
|
|
|
|
// Cache miss -> fetch returns 500
|
|
|
|
|
const mockStatus = 500;
|
|
|
|
|
fetch.mockResolvedValueOnce({
|
|
|
|
|
ok: false,
|
|
|
|
|
@@ -626,7 +739,6 @@ describe("License Core Logic", () => {
|
|
|
|
|
|
|
|
|
|
await getEnterpriseLicense();
|
|
|
|
|
|
|
|
|
|
// Verify that the API error was logged with correct structure
|
|
|
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
status: mockStatus,
|
|
|
|
|
@@ -641,8 +753,7 @@ describe("License Core Logic", () => {
|
|
|
|
|
const { getEnterpriseLicense } = await import("./license");
|
|
|
|
|
const fetch = (await import("node-fetch")).default as Mock;
|
|
|
|
|
|
|
|
|
|
// Test with 403 Forbidden
|
|
|
|
|
mockCache.withCache.mockImplementation(async (fn) => await fn());
|
|
|
|
|
// Cache miss -> fetch returns 403
|
|
|
|
|
const mockStatus = 403;
|
|
|
|
|
fetch.mockResolvedValueOnce({
|
|
|
|
|
ok: false,
|
|
|
|
|
@@ -652,7 +763,6 @@ describe("License Core Logic", () => {
|
|
|
|
|
|
|
|
|
|
await getEnterpriseLicense();
|
|
|
|
|
|
|
|
|
|
// Verify that the API error was logged with correct structure
|
|
|
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
status: mockStatus,
|
|
|
|
|
@@ -675,8 +785,7 @@ describe("License Core Logic", () => {
|
|
|
|
|
version: 1,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockCache.withCache.mockResolvedValue(null);
|
|
|
|
|
mockCache.get.mockImplementation(async (key) => {
|
|
|
|
|
mockCache.get.mockImplementation(async (key: string) => {
|
|
|
|
|
if (key.includes(":previous_result")) {
|
|
|
|
|
return { ok: true, data: mockPreviousResult };
|
|
|
|
|
}
|
|
|
|
|
@@ -687,7 +796,6 @@ describe("License Core Logic", () => {
|
|
|
|
|
|
|
|
|
|
await getEnterpriseLicense();
|
|
|
|
|
|
|
|
|
|
// Verify that the fallback info was logged
|
|
|
|
|
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
fallbackLevel: "grace",
|
|
|
|
|
@@ -712,8 +820,9 @@ describe("License Core Logic", () => {
|
|
|
|
|
|
|
|
|
|
const fetch = (await import("node-fetch")).default as Mock;
|
|
|
|
|
|
|
|
|
|
// Mock cache.withCache to execute the function (simulating cache miss)
|
|
|
|
|
mockCache.withCache.mockImplementation(async (fn) => await fn());
|
|
|
|
|
// Cache miss so fetchLicense fetches from server
|
|
|
|
|
mockCache.get.mockResolvedValue({ ok: true, data: null });
|
|
|
|
|
mockCache.exists.mockResolvedValue({ ok: true, data: false });
|
|
|
|
|
|
|
|
|
|
fetch.mockResolvedValueOnce({
|
|
|
|
|
ok: true,
|
|
|
|
|
|