diff --git a/packages/core/src/libraries/shared.ts b/packages/core/src/libraries/shared.ts new file mode 100644 index 000000000..c37f8ec7f --- /dev/null +++ b/packages/core/src/libraries/shared.ts @@ -0,0 +1,5 @@ +import envSet from '#src/env-set/index.js'; +import Queries from '#src/tenants/Queries.js'; + +/** @deprecated Don't use. This is for transition only and will be removed soon. */ +export const defaultQueries = new Queries(envSet.pool); diff --git a/packages/core/src/libraries/sign-in-experience/index.test.ts b/packages/core/src/libraries/sign-in-experience/index.test.ts index ffd08ab18..83853d758 100644 --- a/packages/core/src/libraries/sign-in-experience/index.test.ts +++ b/packages/core/src/libraries/sign-in-experience/index.test.ts @@ -18,34 +18,42 @@ const { mockEsm } = createMockUtils(jest); const allCustomLanguageTags: LanguageTag[] = []; -const { findAllCustomLanguageTags } = mockEsm('#src/queries/custom-phrase.js', () => ({ +const customPhrases = { findAllCustomLanguageTags: jest.fn(async () => allCustomLanguageTags), -})); +}; +const { findAllCustomLanguageTags } = customPhrases; + const { getLogtoConnectors } = mockEsm('#src/connectors.js', () => ({ getLogtoConnectors: jest.fn(), })); -const { findDefaultSignInExperience, updateDefaultSignInExperience } = mockEsm( - '#src/queries/sign-in-experience.js', - () => ({ - findDefaultSignInExperience: jest.fn(), - updateDefaultSignInExperience: jest.fn( - async (data: Partial): Promise => ({ - ...mockSignInExperience, - ...data, - }) - ), - }) -); -const { validateBranding, validateLanguageInfo, removeUnavailableSocialConnectorTargets } = - await import('./index.js'); +const signInExperiences = { + findDefaultSignInExperience: jest.fn(), + updateDefaultSignInExperience: jest.fn( + async (data: Partial): Promise => ({ + ...mockSignInExperience, + ...data, + }) + ), +}; +const { findDefaultSignInExperience, updateDefaultSignInExperience } = signInExperiences; + +const { MockQueries } = await import('#src/test-utils/tenant.js'); +const queries = new MockQueries({ + customPhrases, + signInExperiences, +}); + +const { validateBranding, createSignInExperienceLibrary } = await import('./index.js'); +const { validateLanguageInfo, removeUnavailableSocialConnectorTargets } = + createSignInExperienceLibrary(queries); beforeEach(() => { jest.clearAllMocks(); }); describe('validate branding', () => { - test('should throw when the UI style contains the slogan and slogan is empty', () => { + it('should throw when the UI style contains the slogan and slogan is empty', () => { expect(() => { validateBranding({ ...mockBranding, @@ -55,7 +63,7 @@ describe('validate branding', () => { }).toMatchError(new RequestError('sign_in_experiences.empty_slogan')); }); - test('should throw when the logo is empty', () => { + it('should throw when the logo is empty', () => { expect(() => { validateBranding({ ...mockBranding, @@ -66,7 +74,7 @@ describe('validate branding', () => { }).toMatchError(new RequestError('sign_in_experiences.empty_logo')); }); - test('should throw when the UI style contains the slogan and slogan is blank', () => { + it('should throw when the UI style contains the slogan and slogan is blank', () => { expect(() => { validateBranding({ ...mockBranding, @@ -76,7 +84,7 @@ describe('validate branding', () => { }).toMatchError(new RequestError('sign_in_experiences.empty_slogan')); }); - test('should not throw when the UI style does not contain the slogan and slogan is empty', () => { + it('should not throw when the UI style does not contain the slogan and slogan is empty', () => { expect(() => { validateBranding({ ...mockBranding, @@ -138,7 +146,7 @@ describe('validate language info', () => { }); describe('remove unavailable social connector targets', () => { - test('should remove unavailable social connector targets in sign-in experience', async () => { + it('should remove unavailable social connector targets in sign-in experience', async () => { const mockSocialConnectorTargets = mockSocialConnectors.map( ({ metadata: { target } }) => target ); diff --git a/packages/core/src/libraries/sign-in-experience/index.ts b/packages/core/src/libraries/sign-in-experience/index.ts index 24c6f87d5..81c4e1dbe 100644 --- a/packages/core/src/libraries/sign-in-experience/index.ts +++ b/packages/core/src/libraries/sign-in-experience/index.ts @@ -13,14 +13,11 @@ import i18next from 'i18next'; import { getLogtoConnectors } from '#src/connectors/index.js'; import RequestError from '#src/errors/RequestError/index.js'; -import { findAllCustomLanguageTags } from '#src/queries/custom-phrase.js'; -import { - findDefaultSignInExperience, - updateDefaultSignInExperience, -} from '#src/queries/sign-in-experience.js'; -import { hasActiveUsers } from '#src/queries/user.js'; +import type Queries from '#src/tenants/Queries.js'; import assertThat from '#src/utils/assert-that.js'; +import { defaultQueries } from '../shared.js'; + export * from './sign-up.js'; export * from './sign-in.js'; @@ -32,66 +29,88 @@ export const validateBranding = (branding: Branding) => { assertThat(branding.logoUrl.trim(), 'sign_in_experiences.empty_logo'); }; -export const validateLanguageInfo = async (languageInfo: LanguageInfo) => { - const supportedLanguages = [...builtInLanguages, ...(await findAllCustomLanguageTags())]; +export const createSignInExperienceLibrary = (queries: Queries) => { + const { + customPhrases: { findAllCustomLanguageTags }, + signInExperiences: { findDefaultSignInExperience, updateDefaultSignInExperience }, + users: { hasActiveUsers }, + } = queries; - assertThat( - supportedLanguages.includes(languageInfo.fallbackLanguage), - new RequestError({ - code: 'sign_in_experiences.unsupported_default_language', - language: languageInfo.fallbackLanguage, - }) - ); + const validateLanguageInfo = async (languageInfo: LanguageInfo) => { + const supportedLanguages = [...builtInLanguages, ...(await findAllCustomLanguageTags())]; + + assertThat( + supportedLanguages.includes(languageInfo.fallbackLanguage), + new RequestError({ + code: 'sign_in_experiences.unsupported_default_language', + language: languageInfo.fallbackLanguage, + }) + ); + }; + + const removeUnavailableSocialConnectorTargets = async () => { + const connectors = await getLogtoConnectors(); + const availableSocialConnectorTargets = deduplicate( + connectors + .filter(({ type }) => type === ConnectorType.Social) + .map(({ metadata: { target } }) => target) + ); + + const { socialSignInConnectorTargets } = await findDefaultSignInExperience(); + + await updateDefaultSignInExperience({ + socialSignInConnectorTargets: socialSignInConnectorTargets.filter((target) => + availableSocialConnectorTargets.includes(target) + ), + }); + }; + + const getSignInExperienceForApplication = async ( + applicationId?: string + ): Promise => { + const signInExperience = await findDefaultSignInExperience(); + + // Hard code AdminConsole sign-in methods settings. + if (applicationId === adminConsoleApplicationId) { + return { + ...adminConsoleSignInExperience, + branding: { + ...adminConsoleSignInExperience.branding, + slogan: i18next.t('admin_console.welcome.title'), + }, + termsOfUseUrl: signInExperience.termsOfUseUrl, + languageInfo: signInExperience.languageInfo, + signInMode: (await hasActiveUsers()) ? SignInMode.SignIn : SignInMode.Register, + socialSignInConnectorTargets: [], + }; + } + + // Insert Demo App Notification + if (applicationId === demoAppApplicationId) { + const { socialSignInConnectorTargets } = signInExperience; + + const notification = i18next.t('demo_app.notification'); + + return { + ...signInExperience, + socialSignInConnectorTargets, + notification, + }; + } + + return signInExperience; + }; + + return { + validateLanguageInfo, + removeUnavailableSocialConnectorTargets, + getSignInExperienceForApplication, + }; }; -export const removeUnavailableSocialConnectorTargets = async () => { - const connectors = await getLogtoConnectors(); - const availableSocialConnectorTargets = deduplicate( - connectors - .filter(({ type }) => type === ConnectorType.Social) - .map(({ metadata: { target } }) => target) - ); - - const { socialSignInConnectorTargets } = await findDefaultSignInExperience(); - await updateDefaultSignInExperience({ - socialSignInConnectorTargets: socialSignInConnectorTargets.filter((target) => - availableSocialConnectorTargets.includes(target) - ), - }); -}; - -export const getSignInExperienceForApplication = async ( - applicationId?: string -): Promise => { - const signInExperience = await findDefaultSignInExperience(); - - // Hard code AdminConsole sign-in methods settings. - if (applicationId === adminConsoleApplicationId) { - return { - ...adminConsoleSignInExperience, - branding: { - ...adminConsoleSignInExperience.branding, - slogan: i18next.t('admin_console.welcome.title'), - }, - termsOfUseUrl: signInExperience.termsOfUseUrl, - languageInfo: signInExperience.languageInfo, - signInMode: (await hasActiveUsers()) ? SignInMode.SignIn : SignInMode.Register, - socialSignInConnectorTargets: [], - }; - } - - // Insert Demo App Notification - if (applicationId === demoAppApplicationId) { - const { socialSignInConnectorTargets } = signInExperience; - - const notification = i18next.t('demo_app.notification'); - - return { - ...signInExperience, - socialSignInConnectorTargets, - notification, - }; - } - - return signInExperience; -}; +/** @deprecated Don't use. This is for transition only and will be removed soon. */ +export const { + validateLanguageInfo, + removeUnavailableSocialConnectorTargets, + getSignInExperienceForApplication, +} = createSignInExperienceLibrary(defaultQueries); diff --git a/packages/core/src/libraries/user.ts b/packages/core/src/libraries/user.ts index c2e1403b0..7c7624b75 100644 --- a/packages/core/src/libraries/user.ts +++ b/packages/core/src/libraries/user.ts @@ -10,16 +10,15 @@ import pRetry from 'p-retry'; import { buildInsertIntoWithPool } from '#src/database/insert-into.js'; import envSet from '#src/env-set/index.js'; import RequestError from '#src/errors/RequestError/index.js'; -import Queries from '#src/tenants/Queries.js'; +import type Queries from '#src/tenants/Queries.js'; import assertThat from '#src/utils/assert-that.js'; import { encryptPassword } from '#src/utils/password.js'; +import { defaultQueries } from './shared.js'; + const userId = buildIdGenerator(12); const roleId = buildIdGenerator(21); -/** @deprecated Don't use. This is for transition only and will be removed soon. */ -export const defaultQueries = new Queries(envSet.pool); - export const encryptUserPassword = async ( password: string ): Promise<{ diff --git a/packages/core/src/routes/application.test.ts b/packages/core/src/routes/application.test.ts index 282ef467f..b2f95bd25 100644 --- a/packages/core/src/routes/application.test.ts +++ b/packages/core/src/routes/application.test.ts @@ -30,7 +30,7 @@ const { findApplicationById } = await mockEsmWithActual('#src/queries/applicatio ), })); -mockEsm('@logto/core-kit', () => ({ +await mockEsmWithActual('@logto/core-kit', () => ({ // eslint-disable-next-line unicorn/consistent-function-scoping buildIdGenerator: () => () => 'randomId', generateStandardId: () => 'randomId', diff --git a/packages/core/src/routes/init.ts b/packages/core/src/routes/init.ts index 1faa7b771..ef85809bd 100644 --- a/packages/core/src/routes/init.ts +++ b/packages/core/src/routes/init.ts @@ -23,7 +23,7 @@ import resourceRoutes from './resource.js'; import roleRoutes from './role.js'; import sessionRoutes from './session/index.js'; import settingRoutes from './setting.js'; -import signInExperiencesRoutes from './sign-in-experience.js'; +import signInExperiencesRoutes from './sign-in-experience/index.js'; import statusRoutes from './status.js'; import swaggerRoutes from './swagger.js'; import type { AnonymousRouter, AnonymousRouterLegacy, AuthedRouter } from './types.js'; diff --git a/packages/core/src/routes/profile.ts b/packages/core/src/routes/profile.ts index a1ade1252..c9a409d95 100644 --- a/packages/core/src/routes/profile.ts +++ b/packages/core/src/routes/profile.ts @@ -73,14 +73,11 @@ export default function profileRoutes( body: object({ username: string().regex(usernameRegEx) }), }), async (ctx, next) => { - console.log('?0'); const userId = await checkSessionHealth(ctx, tenant, verificationTimeout); assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 })); const { username } = ctx.guard.body; - console.log('?1'); await checkIdentifierCollision({ username }, userId); - console.log('?2'); await updateUserById(userId, { username }, 'replace'); ctx.status = 204; diff --git a/packages/core/src/routes/sign-in-experience.branding.guard.test.ts b/packages/core/src/routes/sign-in-experience/guard.branding.test.ts similarity index 82% rename from packages/core/src/routes/sign-in-experience.branding.guard.test.ts rename to packages/core/src/routes/sign-in-experience/guard.branding.test.ts index fbfd9a39f..07580b2dd 100644 --- a/packages/core/src/routes/sign-in-experience.branding.guard.test.ts +++ b/packages/core/src/routes/sign-in-experience/guard.branding.test.ts @@ -3,25 +3,30 @@ import { BrandingStyle } from '@logto/schemas'; import { pickDefault, createMockUtils } from '@logto/shared/esm'; import { mockBranding, mockSignInExperience } from '#src/__mocks__/index.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; -const { mockEsm, mockEsmWithActual } = createMockUtils(import.meta.jest); - -await mockEsmWithActual('#src/queries/sign-in-experience.js', () => ({ - updateDefaultSignInExperience: async ( - data: Partial - ): Promise => ({ - ...mockSignInExperience, - ...data, - }), -})); +const { mockEsm } = createMockUtils(import.meta.jest); mockEsm('#src/connectors.js', () => ({ getLogtoConnectors: async () => [], })); -const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js')); -const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes }); +const tenantContext = new MockTenant(undefined, { + signInExperiences: { + updateDefaultSignInExperience: async ( + data: Partial + ): Promise => ({ + ...mockSignInExperience, + ...data, + }), + }, +}); +const signInExperiencesRoutes = await pickDefault(import('./index.js')); +const signInExperienceRequester = createRequester({ + authedRoutes: signInExperiencesRoutes, + tenantContext, +}); const expectPatchResponseStatus = async ( signInExperience: Record, diff --git a/packages/core/src/routes/sign-in-experience.color.guard.test.ts b/packages/core/src/routes/sign-in-experience/guard.color.test.ts similarity index 70% rename from packages/core/src/routes/sign-in-experience.color.guard.test.ts rename to packages/core/src/routes/sign-in-experience/guard.color.test.ts index ca9bb20b7..d8aebc447 100644 --- a/packages/core/src/routes/sign-in-experience.color.guard.test.ts +++ b/packages/core/src/routes/sign-in-experience/guard.color.test.ts @@ -2,25 +2,29 @@ import type { CreateSignInExperience, SignInExperience } from '@logto/schemas'; import { pickDefault, createMockUtils } from '@logto/shared/esm'; import { mockColor, mockSignInExperience } from '#src/__mocks__/index.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; -const { mockEsm, mockEsmWithActual } = createMockUtils(import.meta.jest); - -await mockEsmWithActual('#src/queries/sign-in-experience.js', () => ({ - updateDefaultSignInExperience: async ( - data: Partial - ): Promise => ({ - ...mockSignInExperience, - ...data, - }), -})); +const { mockEsm } = createMockUtils(import.meta.jest); mockEsm('#src/connectors.js', () => ({ getLogtoConnectors: async () => [], })); -const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js')); -const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes }); +const signInExperiencesRoutes = await pickDefault(import('./index.js')); +const signInExperienceRequester = createRequester({ + authedRoutes: signInExperiencesRoutes, + tenantContext: new MockTenant(undefined, { + signInExperiences: { + updateDefaultSignInExperience: async ( + data: Partial + ): Promise => ({ + ...mockSignInExperience, + ...data, + }), + }, + }), +}); const expectPatchResponseStatus = async ( signInExperience: Record, diff --git a/packages/core/src/routes/sign-in-experience.guard.test.ts b/packages/core/src/routes/sign-in-experience/guard.test.ts similarity index 83% rename from packages/core/src/routes/sign-in-experience.guard.test.ts rename to packages/core/src/routes/sign-in-experience/guard.test.ts index 69281113e..9b1aeb5e8 100644 --- a/packages/core/src/routes/sign-in-experience.guard.test.ts +++ b/packages/core/src/routes/sign-in-experience/guard.test.ts @@ -10,9 +10,10 @@ import { mockLanguageInfo, mockSignInExperience, } from '#src/__mocks__/index.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; const { jest } = import.meta; -const { mockEsm, mockEsmWithActual } = createMockUtils(jest); +const { mockEsm } = createMockUtils(jest); mockEsm('#src/connectors.js', () => ({ getLogtoConnectors: jest.fn(async () => [ @@ -24,25 +25,33 @@ mockEsm('#src/connectors.js', () => ({ ]), })); -const { validateLanguageInfo } = await mockEsmWithActual( - '#src/libraries/sign-in-experience.js', - () => ({ - validateLanguageInfo: jest.fn(), - }) +const validateLanguageInfo = jest.fn(); + +const tenantContext = new MockTenant( + undefined, + { + signInExperiences: { + updateDefaultSignInExperience: async ( + data: Partial + ): Promise => ({ + ...mockSignInExperience, + ...data, + }), + }, + }, + { + signInExperiences: { + validateLanguageInfo, + }, + } ); -await mockEsmWithActual('#src/queries/sign-in-experience.js', () => ({ - updateDefaultSignInExperience: async ( - data: Partial - ): Promise => ({ - ...mockSignInExperience, - ...data, - }), -})); - -const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js')); +const signInExperiencesRoutes = await pickDefault(import('./index.js')); const { createRequester } = await import('#src/utils/test-utils.js'); -const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes }); +const signInExperienceRequester = createRequester({ + authedRoutes: signInExperiencesRoutes, + tenantContext, +}); const expectPatchResponseStatus = async ( signInExperience: Record, diff --git a/packages/core/src/routes/sign-in-experience.test.ts b/packages/core/src/routes/sign-in-experience/index.test.ts similarity index 82% rename from packages/core/src/routes/sign-in-experience.test.ts rename to packages/core/src/routes/sign-in-experience/index.test.ts index 4eac6682c..4d7b0b0e1 100644 --- a/packages/core/src/routes/sign-in-experience.test.ts +++ b/packages/core/src/routes/sign-in-experience/index.test.ts @@ -15,18 +15,11 @@ import { mockAliyunSmsConnector, mockTermsOfUseUrl, } from '#src/__mocks__/index.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; const { jest } = import.meta; -const { mockEsm, mockEsmWithActual } = createMockUtils(jest); - -const { validateBranding, validateLanguageInfo, validateSignIn, validateSignUp } = - await mockEsmWithActual('#src/libraries/sign-in-experience/index.js', () => ({ - validateBranding: jest.fn(), - validateLanguageInfo: jest.fn(), - validateSignIn: jest.fn(), - validateSignUp: jest.fn(), - })); +const { mockEsmWithActual } = createMockUtils(jest); const logtoConnectors = [ mockFacebookConnector, @@ -40,7 +33,16 @@ await mockEsmWithActual('#src/connectors.js', () => ({ getLogtoConnectors: async () => logtoConnectors, })); -const { findDefaultSignInExperience } = mockEsm('#src/queries/sign-in-experience.js', () => ({ +const { validateBranding, validateSignIn, validateSignUp } = await mockEsmWithActual( + '#src/libraries/sign-in-experience/index.js', + () => ({ + validateBranding: jest.fn(), + validateSignIn: jest.fn(), + validateSignUp: jest.fn(), + }) +); + +const signInExperiences = { findDefaultSignInExperience: jest.fn(async () => mockSignInExperience), updateDefaultSignInExperience: async ( data: Partial @@ -48,14 +50,22 @@ const { findDefaultSignInExperience } = mockEsm('#src/queries/sign-in-experience ...mockSignInExperience, ...data, }), -})); +}; +const { findDefaultSignInExperience } = signInExperiences; -mockEsm('#src/queries/custom-phrase.js', () => ({ - findAllCustomLanguageTags: async () => [], -})); +const validateLanguageInfo = jest.fn(); -const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js')); -const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes }); +const tenantContext = new MockTenant( + undefined, + { signInExperiences, customPhrases: { findAllCustomLanguageTags: async () => [] } }, + { signInExperiences: { validateLanguageInfo } } +); + +const signInExperiencesRoutes = await pickDefault(import('./index.js')); +const signInExperienceRequester = createRequester({ + authedRoutes: signInExperiencesRoutes, + tenantContext, +}); describe('GET /sign-in-exp', () => { afterAll(() => { diff --git a/packages/core/src/routes/sign-in-experience.ts b/packages/core/src/routes/sign-in-experience/index.ts similarity index 88% rename from packages/core/src/routes/sign-in-experience.ts rename to packages/core/src/routes/sign-in-experience/index.ts index 607178c5d..e493aa00c 100644 --- a/packages/core/src/routes/sign-in-experience.ts +++ b/packages/core/src/routes/sign-in-experience/index.ts @@ -4,21 +4,19 @@ import { literal, object, string } from 'zod'; import { getLogtoConnectors } from '#src/connectors/index.js'; import { validateBranding, - validateLanguageInfo, validateSignUp, validateSignIn, } from '#src/libraries/sign-in-experience/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; -import { - findDefaultSignInExperience, - updateDefaultSignInExperience, -} from '#src/queries/sign-in-experience.js'; -import type { AuthedRouter, RouterInitArgs } from './types.js'; +import type { AuthedRouter, RouterInitArgs } from '../types.js'; export default function signInExperiencesRoutes( - ...[router]: RouterInitArgs + ...[router, { queries, libraries }]: RouterInitArgs ) { + const { findDefaultSignInExperience, updateDefaultSignInExperience } = queries.signInExperiences; + const { validateLanguageInfo } = libraries.signInExperiences; + /** * As we only support single signInExperience settings for V1 * always return the default settings in DB for the /sign-in-exp get method diff --git a/packages/core/src/routes/well-known.test.ts b/packages/core/src/routes/well-known.test.ts index 0c2c488e6..854bb9808 100644 --- a/packages/core/src/routes/well-known.test.ts +++ b/packages/core/src/routes/well-known.test.ts @@ -15,9 +15,6 @@ import { mockWechatConnector, mockWechatNativeConnector, } from '#src/__mocks__/index.js'; -import { createMockProvider } from '#src/test-utils/oidc-provider.js'; -import { MockTenant } from '#src/test-utils/tenant.js'; -import { createRequester } from '#src/utils/test-utils.js'; const { jest } = import.meta; const { mockEsm, mockEsmWithActual } = createMockUtils(jest); @@ -28,14 +25,11 @@ await mockEsmWithActual('i18next', () => ({ }, })); -const { findDefaultSignInExperience } = mockEsm('#src/queries/sign-in-experience.js', () => ({ +const sieQueries = { updateDefaultSignInExperience: jest.fn(), findDefaultSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience), -})); - -await mockEsmWithActual('#src/queries/user.js', () => ({ - hasActiveUsers: jest.fn().mockResolvedValue(true), -})); +}; +const { findDefaultSignInExperience } = sieQueries; mockEsm('#src/connectors.js', () => ({ getLogtoConnectors: jest.fn(async () => [ @@ -50,6 +44,9 @@ mockEsm('#src/connectors.js', () => ({ })); const wellKnownRoutes = await pickDefault(import('#src/routes/well-known.js')); +const { createMockProvider } = await import('#src/test-utils/oidc-provider.js'); +const { MockTenant } = await import('#src/test-utils/tenant.js'); +const { createRequester } = await import('#src/utils/test-utils.js'); describe('GET /.well-known/sign-in-exp', () => { afterEach(() => { @@ -59,7 +56,10 @@ describe('GET /.well-known/sign-in-exp', () => { const provider = createMockProvider(); const sessionRequest = createRequester({ anonymousRoutes: wellKnownRoutes, - tenantContext: new MockTenant(provider), + tenantContext: new MockTenant(provider, { + signInExperiences: sieQueries, + users: { hasActiveUsers: jest.fn().mockResolvedValue(true) }, + }), middlewares: [ async (ctx, next) => { ctx.addLogContext = jest.fn(); diff --git a/packages/core/src/routes/well-known.ts b/packages/core/src/routes/well-known.ts index 2c4c75308..178e91d70 100644 --- a/packages/core/src/routes/well-known.ts +++ b/packages/core/src/routes/well-known.ts @@ -5,13 +5,14 @@ import etag from 'etag'; import { getLogtoConnectors } from '#src/connectors/index.js'; import { getApplicationIdFromInteraction } from '#src/libraries/session.js'; -import { getSignInExperienceForApplication } from '#src/libraries/sign-in-experience/index.js'; import type { AnonymousRouter, RouterInitArgs } from './types.js'; export default function wellKnownRoutes( - ...[router, { provider }]: RouterInitArgs + ...[router, { provider, libraries }]: RouterInitArgs ) { + const { getSignInExperienceForApplication } = libraries.signInExperiences; + router.get( '/.well-known/sign-in-exp', async (ctx, next) => { diff --git a/packages/core/src/tenants/Libraries.ts b/packages/core/src/tenants/Libraries.ts index a68ef784f..d6d056b1c 100644 --- a/packages/core/src/tenants/Libraries.ts +++ b/packages/core/src/tenants/Libraries.ts @@ -1,9 +1,11 @@ +import { createSignInExperienceLibrary } from '#src/libraries/sign-in-experience/index.js'; import { createUserLibrary } from '#src/libraries/user.js'; import type Queries from './Queries.js'; export default class Libraries { users = createUserLibrary(this.queries); + signInExperiences = createSignInExperienceLibrary(this.queries); constructor(public readonly queries: Queries) {} } diff --git a/packages/core/src/test-utils/tenant.ts b/packages/core/src/test-utils/tenant.ts index 8d2a6abd5..a895edf7e 100644 --- a/packages/core/src/test-utils/tenant.ts +++ b/packages/core/src/test-utils/tenant.ts @@ -6,13 +6,39 @@ import type TenantContext from '#src/tenants/TenantContext.js'; import { createMockProvider } from './oidc-provider.js'; -const { jest } = import.meta; +export const createQueriesWithMockPool = () => + new Queries( + createMockPool({ + query: async (sql, values) => { + return createMockQueryResult([]); + }, + }) + ); -const pool = createMockPool({ - query: async (sql, values) => { - return createMockQueryResult([]); - }, -}); +export class MockQueries extends Queries { + constructor(queriesOverride?: Partial2) { + super( + createMockPool({ + query: async (sql, values) => { + return createMockQueryResult([]); + }, + }) + ); + + if (!queriesOverride) { + return; + } + + const overrideKey = (key: Key) => { + this[key] = { ...this[key], ...queriesOverride[key] }; + }; + + // eslint-disable-next-line no-restricted-syntax + for (const key of Object.keys(queriesOverride) as Array) { + overrideKey(key); + } + } +} // eslint-disable-next-line @typescript-eslint/ban-types export type DeepPartial = T extends object @@ -32,8 +58,7 @@ export class MockTenant implements TenantContext { queriesOverride?: Partial2, librariesOverride?: Partial2 ) { - this.queries = new Queries(pool); - this.setPartial('queries', queriesOverride); + this.queries = new MockQueries(queriesOverride); this.libraries = new Libraries(this.queries); this.setPartial('libraries', librariesOverride); }