0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00
logto/packages/core/src/caches/well-known.test.ts
Charles Zhao 35f57639e5
fix(core): dispose tenant cache on demand in order to hot reload oidc provider (#4641)
* fix(core): dispose tenant cache on demand

* chore: add comments for the tenant cache invalidation mechanism

* refactor(core): refactor dispose tenant cache implementation per review comments

* refactor(core): change `invalidateCache` to a class member method

* test(core): add test cases for Tenant class
2023-10-14 12:34:03 +00:00

168 lines
6 KiB
TypeScript

import { TtlCache } from '@logto/shared';
import { pick } from '@silverhand/essentials';
import { mockConnector0 } from '#src/__mocks__/connector-base-data.js';
import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js';
import { WellKnownCache } from './well-known.js';
const { jest } = import.meta;
const tenantId = 'mock_id';
const cacheStore = new TtlCache<string, string>(60_000);
afterEach(() => {
jest.clearAllMocks();
cacheStore.clear();
});
describe('Well-known cache basics', () => {
it('should be able to get, set, and delete values', async () => {
const cache = new WellKnownCache(tenantId, cacheStore);
await cache.set('sie', WellKnownCache.defaultKey, mockSignInExperience);
expect(await cache.get('sie', WellKnownCache.defaultKey)).toStrictEqual(mockSignInExperience);
await cache.set('connectors-well-known', '123', [mockConnector0]);
expect(await cache.get('connectors-well-known', '123')).toStrictEqual([
pick(mockConnector0, 'connectorId', 'id', 'metadata'),
]);
await cache.set('tenant-cache-expires-at', WellKnownCache.defaultKey, 123);
expect(await cache.get('tenant-cache-expires-at', WellKnownCache.defaultKey)).toBe(123);
await cache.delete('sie', WellKnownCache.defaultKey);
expect(await cache.get('sie', WellKnownCache.defaultKey)).toBe(undefined);
});
it('should NOT be able to set the value with wrong structure', async () => {
const cache = new WellKnownCache(tenantId, cacheStore);
// @ts-expect-error
await cache.set('custom-phrases-tags', WellKnownCache.defaultKey, 1);
expect(await cache.get('custom-phrases-tags', WellKnownCache.defaultKey)).toBe(undefined);
});
it('should NOT be able to set and get when cache type is wrong', async () => {
const cache = new WellKnownCache(tenantId, cacheStore);
// @ts-expect-error
await cache.set('custom-phrases-tags-', WellKnownCache.defaultKey, []);
expect(
// @ts-expect-error
await cache.get('custom-phrases-tags-', WellKnownCache.defaultKey)
).toBe(undefined);
});
});
describe('Well-known cache function wrappers', () => {
it('can memoize function and cache the promise', async () => {
jest.useFakeTimers();
const runResult = Object.freeze({ foo: 'bar' });
const run = jest.fn(
async () =>
new Promise<Record<string, unknown>>((resolve) => {
setTimeout(() => {
resolve(runResult);
}, 0);
jest.runOnlyPendingTimers(); // Ensure this runs in fake timers
})
);
const cache = new WellKnownCache(tenantId, cacheStore);
const memoized = cache.memoize(run, ['custom-phrases']);
const [result1, result2] = await Promise.all([memoized(), memoized()]);
expect(result1).toStrictEqual(runResult);
expect(result2).toStrictEqual(runResult);
expect(await cache.get('custom-phrases', WellKnownCache.defaultKey)).toStrictEqual(runResult);
expect(run).toBeCalledTimes(1);
// Second call
expect(await memoized()).toStrictEqual(runResult);
expect(run).toBeCalledTimes(1);
// Cache expired
jest.setSystemTime(Date.now() + 100_000); // Ensure cache is expired
expect(await memoized()).toStrictEqual(runResult);
expect(run).toBeCalledTimes(2);
expect(await cache.get('custom-phrases', WellKnownCache.defaultKey)).toStrictEqual(runResult);
jest.useRealTimers();
});
it('can memoize function with customized cache key builder', async () => {
const run = jest.fn(
async (foo: string, bar: number) =>
new Promise<Record<string, unknown>>((resolve) => {
setTimeout(() => {
resolve({ foo, bar });
}, 0);
})
);
const cache = new WellKnownCache(tenantId, cacheStore);
const memoized = cache.memoize(run, ['custom-phrases', (foo, bar) => `${foo}+${bar}`]);
const [result1, result2] = await Promise.all([memoized('1', 1), memoized('2', 2)]);
expect(result1).toStrictEqual({ foo: '1', bar: 1 });
expect(result2).toStrictEqual({ foo: '2', bar: 2 });
expect(
await Promise.all([cache.get('custom-phrases', '1+1'), cache.get('custom-phrases', '2+2')])
).toStrictEqual([
{ foo: '1', bar: 1 },
{ foo: '2', bar: 2 },
]);
});
it('can create mutate function wrapper with default cache key builder', async () => {
const run = jest.fn(
async () =>
new Promise<Record<string, unknown>>((resolve) => {
setTimeout(() => {
resolve({});
}, 0);
})
);
const update = jest.fn(async () => true);
const cache = new WellKnownCache(tenantId, cacheStore);
const memoized = cache.memoize(run, ['custom-phrases']);
const mutate = cache.mutate(update, ['custom-phrases']);
await memoized();
await mutate();
expect(await cache.get('custom-phrases', WellKnownCache.defaultKey)).toBeUndefined();
await memoized();
expect(run).toBeCalledTimes(2);
});
it('can create mutate function wrapper with customized cache key builder', async () => {
const run = jest.fn(
async (foo: string, bar: number) =>
new Promise<Record<string, unknown>>((resolve) => {
setTimeout(() => {
resolve({ foo, bar });
}, 0);
})
);
const update = jest.fn(async (value: number) => value);
const cache = new WellKnownCache(tenantId, cacheStore);
const memoized = cache.memoize(run, ['custom-phrases', (foo, bar) => `${foo}+${bar}`]);
const mutate = cache.mutate(update, ['custom-phrases', (value) => `1+${value}`]);
await Promise.all([memoized('1', 1), memoized('2', 2)]);
await Promise.all([mutate(1), mutate(2)]);
expect(
await Promise.all([cache.get('custom-phrases', '1+1'), cache.get('custom-phrases', '2+2')])
).toStrictEqual([undefined, { foo: '2', bar: 2 }]);
expect(run).toBeCalledTimes(2);
const mutate2 = cache.mutate(update, ['custom-phrases', () => `2+2`]);
await mutate2(1);
expect(await cache.get('custom-phrases', '2+2')).toBeUndefined();
expect(run).toBeCalledTimes(2);
});
});