mirror of
https://github.com/logto-io/logto.git
synced 2025-03-17 22:31:28 -05:00
test: use native ESM (#2621)
This commit is contained in:
parent
d65e25a10e
commit
25f0a2e158
105 changed files with 1691 additions and 1520 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -1,2 +1,3 @@
|
|||
/packages/schemas/tables @simeng-li @wangsijie
|
||||
/packages/core/src/routes/session @simeng-li @wangsijie
|
||||
/.changeset @gao-sun
|
||||
|
|
8
.github/workflows/main.yml
vendored
8
.github/workflows/main.yml
vendored
|
@ -20,20 +20,22 @@ jobs:
|
|||
|
||||
- name: Setup Node and pnpm
|
||||
uses: silverhand-io/actions-node-pnpm-run-steps@v2
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Build
|
||||
run: pnpm ci:build
|
||||
|
||||
main-lint:
|
||||
# avoid out of memory issue since macOS has bigger memory
|
||||
# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node and pnpm
|
||||
uses: silverhand-io/actions-node-pnpm-run-steps@v2
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Prepack
|
||||
run: pnpm prepack
|
||||
|
|
10
packages/cli/jest.config.js
Normal file
10
packages/cli/jest.config.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
const config = {
|
||||
coveragePathIgnorePatterns: ['/node_modules/', '/src/__mocks__/'],
|
||||
coverageReporters: ['text-summary', 'lcov'],
|
||||
roots: ['./lib'],
|
||||
moduleNameMapper: {
|
||||
'^(chalk|inquirer)$': '<rootDir>/../shared/lib/esm/module-proxy.js',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1,17 +0,0 @@
|
|||
import type { Config } from '@silverhand/jest-config';
|
||||
import { merge } from '@silverhand/jest-config';
|
||||
|
||||
const config: Config.InitialOptions = {
|
||||
...merge({
|
||||
setupFilesAfterEnv: ['./jest.setup.ts'],
|
||||
roots: ['./src'],
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||
'^(chalk|inquirer|ora)$': '<rootDir>/../shared/src/utils/module-proxy.ts',
|
||||
},
|
||||
}),
|
||||
// Will update common config soon
|
||||
transformIgnorePatterns: ['node_modules/(?!(.*(nanoid|jose|ky|@logto))/)'],
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1,19 +0,0 @@
|
|||
/**
|
||||
* Mocking `import.meta.url` and `got` here since they inevitably needs native ESM, but jest is sticking with CJS.
|
||||
* Will figure out a way to run tests in native ESM mode.
|
||||
*/
|
||||
|
||||
jest.mock('./src/commands/database/alteration/meta-url.js', () => ({
|
||||
metaUrl: 'file:///',
|
||||
}));
|
||||
|
||||
jest.mock('./src/meta-url.js', () => ({
|
||||
metaUrl: 'file:///',
|
||||
}));
|
||||
|
||||
jest.mock('got', () => ({
|
||||
got: {},
|
||||
}));
|
||||
|
||||
// Make lint-staged happy
|
||||
export {};
|
|
@ -25,13 +25,14 @@
|
|||
"precommit": "lint-staged",
|
||||
"prepare:package-json": "node -p \"'export const packageJson = ' + JSON.stringify(require('./package.json'), undefined, 2) + ';'\" > src/package-json.ts",
|
||||
"build": "rimraf lib && pnpm prepare:package-json && tsc -p tsconfig.build.json",
|
||||
"build:test": "rm -rf build/ && tsc -p tsconfig.test.json --sourcemap",
|
||||
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental",
|
||||
"start": "node .",
|
||||
"start:dev": "ts-node --files src/index.ts",
|
||||
"lint": "eslint --ext .ts src",
|
||||
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||
"test": "jest",
|
||||
"test:ci": "jest",
|
||||
"test": "pnpm build:test && NODE_OPTIONS=--experimental-vm-modules jest",
|
||||
"test:ci": "pnpm run test",
|
||||
"prepack": "pnpm build"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -52,7 +53,7 @@
|
|||
"hpagent": "^1.2.0",
|
||||
"inquirer": "^8.2.2",
|
||||
"nanoid": "^3.3.4",
|
||||
"ora": "^5.0.0",
|
||||
"ora": "^6.1.2",
|
||||
"p-retry": "^4.6.1",
|
||||
"roarr": "^7.11.0",
|
||||
"semver": "^7.3.8",
|
||||
|
@ -65,13 +66,13 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@silverhand/eslint-config": "1.3.0",
|
||||
"@silverhand/jest-config": "1.2.2",
|
||||
"@silverhand/ts-config": "1.2.1",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/inquirer": "^8.2.1",
|
||||
"@types/jest": "^29.1.2",
|
||||
"@types/node": "^16.0.0",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@types/sinon": "^10.0.13",
|
||||
"@types/tar": "^6.1.2",
|
||||
"@types/yargs": "^17.0.13",
|
||||
"eslint": "^8.21.0",
|
||||
|
@ -79,7 +80,7 @@
|
|||
"lint-staged": "^13.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"sinon": "^15.0.0",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
|
|
@ -1,42 +1,45 @@
|
|||
import { mockEsmWithActual } from '@logto/shared/esm';
|
||||
import Sinon from 'sinon';
|
||||
import { createMockPool } from 'slonik';
|
||||
|
||||
import * as queries from '../../../queries/logto-config.js';
|
||||
import type { QueryType } from '../../../test-utilities.js';
|
||||
import * as functions from './index.js';
|
||||
import { chooseAlterationsByVersion } from './version.js';
|
||||
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
const { jest } = import.meta;
|
||||
|
||||
const pool = createMockPool({
|
||||
query: async (sql, values) => {
|
||||
return mockQuery(sql, values);
|
||||
},
|
||||
query: jest.fn(),
|
||||
});
|
||||
|
||||
const files = Object.freeze([
|
||||
{ filename: '1.0.0-1663923770-a.js', path: '/alterations-js/1.0.0-1663923770-a.js' },
|
||||
{ filename: '1.0.0-1663923771-b.js', path: '/alterations-js/1.0.0-1663923771-b.js' },
|
||||
{ filename: '1.0.0-1663923772-c.js', path: '/alterations-js/1.0.0-1663923772-c.js' },
|
||||
]);
|
||||
|
||||
await mockEsmWithActual('./utils.js', () => ({
|
||||
getAlterationFiles: async () => files,
|
||||
}));
|
||||
|
||||
const { getCurrentDatabaseAlterationTimestamp } = await mockEsmWithActual(
|
||||
'../../../queries/logto-config.js',
|
||||
() => ({
|
||||
getCurrentDatabaseAlterationTimestamp: jest.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
const { getUndeployedAlterations } = await import('./index.js');
|
||||
|
||||
describe('getUndeployedAlterations()', () => {
|
||||
const files = Object.freeze([
|
||||
{ filename: '1.0.0-1663923770-a.js', path: '/alterations-js/1.0.0-1663923770-a.js' },
|
||||
{ filename: '1.0.0-1663923771-b.js', path: '/alterations-js/1.0.0-1663923771-b.js' },
|
||||
{ filename: '1.0.0-1663923772-c.js', path: '/alterations-js/1.0.0-1663923772-c.js' },
|
||||
]);
|
||||
|
||||
beforeEach(() => {
|
||||
// `getAlterationFiles()` will ensure the order
|
||||
jest.spyOn(functions, 'getAlterationFiles').mockResolvedValueOnce([...files]);
|
||||
});
|
||||
|
||||
it('returns all files if database timestamp is 0', async () => {
|
||||
jest.spyOn(queries, 'getCurrentDatabaseAlterationTimestamp').mockResolvedValueOnce(0);
|
||||
getCurrentDatabaseAlterationTimestamp.mockResolvedValue(0);
|
||||
|
||||
await expect(functions.getUndeployedAlterations(pool)).resolves.toEqual(files);
|
||||
await expect(getUndeployedAlterations(pool)).resolves.toEqual(files);
|
||||
});
|
||||
|
||||
it('returns files whose timestamp is greater then database timestamp', async () => {
|
||||
jest
|
||||
.spyOn(queries, 'getCurrentDatabaseAlterationTimestamp')
|
||||
.mockResolvedValueOnce(1_663_923_770);
|
||||
getCurrentDatabaseAlterationTimestamp.mockResolvedValue(1_663_923_770);
|
||||
|
||||
await expect(functions.getUndeployedAlterations(pool)).resolves.toEqual([files[1], files[2]]);
|
||||
await expect(getUndeployedAlterations(pool)).resolves.toEqual([files[1], files[2]]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -58,12 +61,19 @@ describe('chooseAlterationsByVersion()', () => {
|
|||
'next1-1663923781-c.js',
|
||||
].map((filename) => ({ filename, path: '/alterations/' + filename }))
|
||||
);
|
||||
const stub = Sinon.stub(global, 'process').value({ stdin: { isTTY: false } });
|
||||
|
||||
afterAll(() => {
|
||||
stub.restore();
|
||||
});
|
||||
|
||||
it('chooses nothing when input version is invalid', async () => {
|
||||
await expect(chooseAlterationsByVersion(files, 'next1')).rejects.toThrow(
|
||||
'Invalid Version: next1'
|
||||
new TypeError('Invalid Version: next1')
|
||||
);
|
||||
await expect(chooseAlterationsByVersion([], 'ok')).rejects.toThrow(
|
||||
new TypeError('Invalid Version: ok')
|
||||
);
|
||||
await expect(chooseAlterationsByVersion([], 'ok')).rejects.toThrow('Invalid Version: ok');
|
||||
});
|
||||
|
||||
it('chooses correct alteration files', async () => {
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'path';
|
||||
|
||||
import type { AlterationScript } from '@logto/schemas/lib/types/alteration.js';
|
||||
import { findPackage } from '@logto/shared';
|
||||
import { conditionalString } from '@silverhand/essentials';
|
||||
import chalk from 'chalk';
|
||||
import fsExtra from 'fs-extra';
|
||||
import type { DatabasePool } from 'slonik';
|
||||
import type { CommandModule } from 'yargs';
|
||||
|
||||
|
@ -14,25 +9,11 @@ import {
|
|||
getCurrentDatabaseAlterationTimestamp,
|
||||
updateDatabaseTimestamp,
|
||||
} from '../../../queries/logto-config.js';
|
||||
import { getPathInModule, log } from '../../../utilities.js';
|
||||
import { metaUrl } from './meta-url.js';
|
||||
import { log } from '../../../utilities.js';
|
||||
import type { AlterationFile } from './type.js';
|
||||
import { getAlterationFiles, getTimestampFromFilename } from './utils.js';
|
||||
import { chooseAlterationsByVersion } from './version.js';
|
||||
|
||||
const currentDirname = path.dirname(fileURLToPath(metaUrl));
|
||||
const { copy, existsSync, remove, readdir } = fsExtra;
|
||||
const alterationFilenameRegex = /-(\d+)-?.*\.js$/;
|
||||
|
||||
const getTimestampFromFilename = (filename: string) => {
|
||||
const match = alterationFilenameRegex.exec(filename);
|
||||
|
||||
if (!match?.[1]) {
|
||||
throw new Error(`Can not get timestamp: ${filename}`);
|
||||
}
|
||||
|
||||
return Number(match[1]);
|
||||
};
|
||||
|
||||
const importAlterationScript = async (filePath: string): Promise<AlterationScript> => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const module = await import(filePath);
|
||||
|
@ -41,38 +22,6 @@ const importAlterationScript = async (filePath: string): Promise<AlterationScrip
|
|||
return module.default as AlterationScript;
|
||||
};
|
||||
|
||||
export const getAlterationFiles = async (): Promise<AlterationFile[]> => {
|
||||
const alterationDirectory = getPathInModule('@logto/schemas', 'alterations-js');
|
||||
|
||||
/**
|
||||
* We copy all alteration scripts to the CLI package root directory,
|
||||
* since they need a proper context that includes required dependencies (such as slonik) in `node_modules/`.
|
||||
* While the original `@logto/schemas` may remove them in production.
|
||||
*/
|
||||
const packageDirectory = await findPackage(currentDirname);
|
||||
|
||||
const localAlterationDirectory = path.resolve(
|
||||
packageDirectory ?? currentDirname,
|
||||
'alteration-scripts'
|
||||
);
|
||||
|
||||
if (!existsSync(alterationDirectory)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// We need to copy alteration files to execute in the CLI context to make `slonik` available
|
||||
await remove(localAlterationDirectory);
|
||||
await copy(alterationDirectory, localAlterationDirectory);
|
||||
|
||||
const directory = await readdir(localAlterationDirectory);
|
||||
const files = directory.filter((file) => alterationFilenameRegex.test(file));
|
||||
|
||||
return files
|
||||
.slice()
|
||||
.sort((file1, file2) => getTimestampFromFilename(file1) - getTimestampFromFilename(file2))
|
||||
.map((filename) => ({ path: path.join(localAlterationDirectory, filename), filename }));
|
||||
};
|
||||
|
||||
export const getLatestAlterationTimestamp = async () => {
|
||||
const files = await getAlterationFiles();
|
||||
const lastFile = files[files.length - 1];
|
||||
|
|
55
packages/cli/src/commands/database/alteration/utils.ts
Normal file
55
packages/cli/src/commands/database/alteration/utils.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'path';
|
||||
|
||||
import { findPackage } from '@logto/shared';
|
||||
import fsExtra from 'fs-extra';
|
||||
|
||||
import { getPathInModule } from '../../../utilities.js';
|
||||
import { metaUrl } from './meta-url.js';
|
||||
import type { AlterationFile } from './type.js';
|
||||
|
||||
const currentDirname = path.dirname(fileURLToPath(metaUrl));
|
||||
const { copy, existsSync, remove, readdir } = fsExtra;
|
||||
const alterationFilenameRegex = /-(\d+)-?.*\.js$/;
|
||||
|
||||
export const getTimestampFromFilename = (filename: string) => {
|
||||
const match = alterationFilenameRegex.exec(filename);
|
||||
|
||||
if (!match?.[1]) {
|
||||
throw new Error(`Can not get timestamp: ${filename}`);
|
||||
}
|
||||
|
||||
return Number(match[1]);
|
||||
};
|
||||
|
||||
export const getAlterationFiles = async (): Promise<AlterationFile[]> => {
|
||||
const alterationDirectory = getPathInModule('@logto/schemas', 'alterations-js');
|
||||
|
||||
/**
|
||||
* We copy all alteration scripts to the CLI package root directory,
|
||||
* since they need a proper context that includes required dependencies (such as slonik) in `node_modules/`.
|
||||
* While the original `@logto/schemas` may remove them in production.
|
||||
*/
|
||||
const packageDirectory = await findPackage(currentDirname);
|
||||
|
||||
const localAlterationDirectory = path.resolve(
|
||||
packageDirectory ?? currentDirname,
|
||||
'alteration-scripts'
|
||||
);
|
||||
|
||||
if (!existsSync(alterationDirectory)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// We need to copy alteration files to execute in the CLI context to make `slonik` available
|
||||
await remove(localAlterationDirectory);
|
||||
await copy(alterationDirectory, localAlterationDirectory);
|
||||
|
||||
const directory = await readdir(localAlterationDirectory);
|
||||
const files = directory.filter((file) => alterationFilenameRegex.test(file));
|
||||
|
||||
return files
|
||||
.slice()
|
||||
.sort((file1, file2) => getTimestampFromFilename(file1) - getTimestampFromFilename(file2))
|
||||
.map((filename) => ({ path: path.join(localAlterationDirectory, filename), filename }));
|
||||
};
|
|
@ -42,6 +42,7 @@ export const chooseAlterationsByVersion = async (
|
|||
.filter((version, index, self) => index === self.findIndex((another) => eq(version, another)))
|
||||
.slice()
|
||||
.sort((i, j) => compare(j, i));
|
||||
|
||||
const initialSemVersion = conditional(
|
||||
initialVersion && initialVersion !== latestTag && new SemVer(initialVersion)
|
||||
);
|
||||
|
|
|
@ -6,6 +6,7 @@ import type { QueryType } from '../test-utilities.js';
|
|||
import { expectSqlAssert } from '../test-utilities.js';
|
||||
import { updateDatabaseTimestamp, getCurrentDatabaseAlterationTimestamp } from './logto-config.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
|
||||
const pool = createMockPool({
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { Progress } from 'got';
|
|||
import { got } from 'got';
|
||||
import { HttpsProxyAgent } from 'hpagent';
|
||||
import inquirer from 'inquirer';
|
||||
import type { Options } from 'ora';
|
||||
import ora from 'ora';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -92,7 +93,7 @@ export const getPathInModule = (moduleName: string, relativePath = '/') =>
|
|||
|
||||
export const oraPromise = async <T>(
|
||||
promise: PromiseLike<T>,
|
||||
options?: ora.Options,
|
||||
options?: Options,
|
||||
exitOnError = false
|
||||
) => {
|
||||
const spinner = ora(options).start();
|
||||
|
|
|
@ -3,5 +3,6 @@
|
|||
"compilerOptions": {
|
||||
"isolatedModules": false,
|
||||
"allowJs": true
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
"lint-staged": "^13.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"nanoid": "^3.1.23",
|
||||
"nanoid": "^3.3.4",
|
||||
"parcel": "2.8.0",
|
||||
"postcss": "^8.4.6",
|
||||
"postcss-modules": "^4.3.0",
|
||||
|
|
14
packages/core/jest.config.js
Normal file
14
packages/core/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/** @type {import('jest').Config} */
|
||||
const config = {
|
||||
coveragePathIgnorePatterns: ['/node_modules/', '/src/__mocks__/'],
|
||||
coverageReporters: ['text-summary', 'lcov'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/build/routes/session/'], // `routes/session` is freezed
|
||||
setupFilesAfterEnv: ['jest-matcher-specific-error', './jest.setup.js'],
|
||||
roots: ['./build'],
|
||||
moduleNameMapper: {
|
||||
'^#src/(.*)\\.js(x)?$': '<rootDir>/build/$1',
|
||||
'^(chalk|inquirer)$': '<rootDir>/../shared/lib/esm/module-proxy.js',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1,18 +0,0 @@
|
|||
import type { Config } from '@silverhand/jest-config';
|
||||
import { merge } from '@silverhand/jest-config';
|
||||
|
||||
const config: Config.InitialOptions = {
|
||||
...merge({
|
||||
testPathIgnorePatterns: ['/core/connectors/'],
|
||||
setupFilesAfterEnv: ['jest-matcher-specific-error', './jest.setup.ts'],
|
||||
moduleNameMapper: {
|
||||
'^#src/(.*)\\.js(x)?$': '<rootDir>/src/$1',
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||
'^(chalk|inquirer|ora)$': '<rootDir>/../shared/src/utils/module-proxy.ts',
|
||||
},
|
||||
}),
|
||||
// Will update common config soon
|
||||
transformIgnorePatterns: ['node_modules/(?!(.*(nanoid|jose|ky|@logto|got))/)'],
|
||||
};
|
||||
|
||||
export default config;
|
39
packages/core/jest.setup.js
Normal file
39
packages/core/jest.setup.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* Setup environment variables for unit test
|
||||
*/
|
||||
|
||||
import { mockEsm } from '@logto/shared/esm';
|
||||
import { createMockQueryResult, createMockPool } from 'slonik';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
mockEsm('#src/env-set/index.js', () => ({
|
||||
MountedApps: {
|
||||
Api: 'api',
|
||||
Oidc: 'oidc',
|
||||
Console: 'console',
|
||||
DemoApp: 'demo-app',
|
||||
Welcome: 'welcome',
|
||||
},
|
||||
default: {
|
||||
get values() {
|
||||
return {
|
||||
endpoint: 'https://logto.test',
|
||||
adminConsoleUrl: 'https://logto.test/console',
|
||||
};
|
||||
},
|
||||
get oidc() {
|
||||
return {
|
||||
issuer: 'https://logto.test/oidc',
|
||||
};
|
||||
},
|
||||
get pool() {
|
||||
return createMockPool({ query: async () => createMockQueryResult([]) });
|
||||
},
|
||||
load: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Logger is not considered in all test cases
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
mockEsm('koa-logger', () => ({ default: () => (_, next) => next() }));
|
|
@ -1,34 +0,0 @@
|
|||
/**
|
||||
* Setup environment variables for unit test
|
||||
*/
|
||||
|
||||
import envSet from '#src/env-set/index.js';
|
||||
|
||||
jest.mock('#src/lib/logto-config.js');
|
||||
jest.mock('#src/env-set/check-alteration-state.js');
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-top-level-await
|
||||
(async () => {
|
||||
await envSet.load();
|
||||
})();
|
||||
|
||||
/**
|
||||
* Mocking `import.meta.url` and `got` here since they inevitably needs native ESM, but jest is sticking with CJS.
|
||||
* Will figure out a way to run tests in native ESM mode.
|
||||
*/
|
||||
|
||||
jest.mock('./src/connectors/meta-url.js', () => ({
|
||||
metaUrl: 'file:///',
|
||||
}));
|
||||
|
||||
jest.mock('../cli/lib/meta-url.js', () => ({
|
||||
metaUrl: 'file:///',
|
||||
}));
|
||||
|
||||
jest.mock('../cli/lib/commands/database/alteration/meta-url.js', () => ({
|
||||
metaUrl: 'file:///',
|
||||
}));
|
||||
|
||||
jest.mock('got', () => ({
|
||||
got: {},
|
||||
}));
|
|
@ -14,12 +14,14 @@
|
|||
"precommit": "lint-staged",
|
||||
"copyfiles": "copyfiles -u 1 src/**/*.md build",
|
||||
"build": "rm -rf build/ && tsc -p tsconfig.build.json && pnpm run copyfiles",
|
||||
"build:test": "rm -rf build/ && tsc -p tsconfig.test.json --sourcemap",
|
||||
"lint": "eslint --ext .ts src",
|
||||
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||
"dev": "rm -rf build/ && pnpm run copyfiles && nodemon",
|
||||
"start": "NODE_ENV=production node build/index.js",
|
||||
"test": "jest",
|
||||
"test:ci": "jest --coverage --silent",
|
||||
"test:only": "NODE_OPTIONS=--experimental-vm-modules jest",
|
||||
"test": "pnpm build:test && pnpm test:only build/",
|
||||
"test:ci": "pnpm run test --coverage --silent",
|
||||
"test:report": "codecov -F core"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -56,7 +58,7 @@
|
|||
"koa-router": "^12.0.0",
|
||||
"koa-send": "^5.0.1",
|
||||
"lodash.pick": "^4.4.0",
|
||||
"nanoid": "^3.1.23",
|
||||
"nanoid": "^3.3.4",
|
||||
"oidc-provider": "^7.13.0",
|
||||
"p-retry": "^4.6.1",
|
||||
"query-string": "^7.0.1",
|
||||
|
@ -69,9 +71,7 @@
|
|||
"zod": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@shopify/jest-koa-mocks": "^5.0.1",
|
||||
"@silverhand/eslint-config": "1.3.0",
|
||||
"@silverhand/jest-config": "1.2.2",
|
||||
"@silverhand/ts-config": "1.2.1",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/etag": "^1.8.1",
|
||||
|
@ -87,6 +87,7 @@
|
|||
"@types/lodash.pick": "^4.4.6",
|
||||
"@types/node": "^16.0.0",
|
||||
"@types/oidc-provider": "^7.12.0",
|
||||
"@types/sinon": "^10.0.13",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"copyfiles": "^2.4.1",
|
||||
"eslint": "^8.21.0",
|
||||
|
@ -94,10 +95,11 @@
|
|||
"jest": "^29.1.2",
|
||||
"jest-matcher-specific-error": "^1.0.0",
|
||||
"lint-staged": "^13.0.0",
|
||||
"nock": "^13.2.2",
|
||||
"node-mocks-http": "^1.12.1",
|
||||
"nodemon": "^2.0.19",
|
||||
"openapi-types": "^12.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"sinon": "^15.0.0",
|
||||
"supertest": "^6.2.2",
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
|
|
|
@ -33,6 +33,8 @@ export {
|
|||
mockMetadata3,
|
||||
} from './connector-base-data.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
export const mockConnector: Connector = {
|
||||
id: 'id',
|
||||
config: {},
|
||||
|
|
|
@ -1,46 +1,39 @@
|
|||
import { mockEsmDefault, pickDefault } from '@logto/shared/esm';
|
||||
import Koa from 'koa';
|
||||
|
||||
import * as koaErrorHandler from '#src/middleware/koa-error-handler.js';
|
||||
import * as koaI18next from '#src/middleware/koa-i18next.js';
|
||||
import * as koaLog from '#src/middleware/koa-log.js';
|
||||
import * as koaOIDCErrorHandler from '#src/middleware/koa-oidc-error-handler.js';
|
||||
import * as koaSlonikErrorHandler from '#src/middleware/koa-slonik-error-handler.js';
|
||||
import * as koaSpaProxy from '#src/middleware/koa-spa-proxy.js';
|
||||
import * as initOidc from '#src/oidc/init.js';
|
||||
import * as initRouter from '#src/routes/init.js';
|
||||
import { emptyMiddleware } from '#src/utils/test-utils.js';
|
||||
|
||||
import initI18n from '../i18n/init.js';
|
||||
import initApp from './init.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
const middlewareList = [
|
||||
'error-handler',
|
||||
'i18next',
|
||||
'log',
|
||||
'oidc-error-handler',
|
||||
'slonik-error-handler',
|
||||
'spa-proxy',
|
||||
].map((name) => {
|
||||
const mock = jest.fn(() => emptyMiddleware);
|
||||
mockEsmDefault(`#src/middleware/koa-${name}.js`, () => mock);
|
||||
|
||||
return mock;
|
||||
});
|
||||
|
||||
const initI18n = await pickDefault(import('../i18n/init.js'));
|
||||
const initApp = await pickDefault(import('./init.js'));
|
||||
|
||||
describe('App Init', () => {
|
||||
const listenMock = jest.spyOn(Koa.prototype, 'listen').mockImplementation(jest.fn());
|
||||
|
||||
const middlewareList = [
|
||||
koaErrorHandler,
|
||||
koaI18next,
|
||||
koaLog,
|
||||
koaOIDCErrorHandler,
|
||||
koaSlonikErrorHandler,
|
||||
koaSpaProxy,
|
||||
];
|
||||
const initMethods = [initRouter, initOidc];
|
||||
|
||||
const middlewareSpys = middlewareList.map((module) => jest.spyOn(module, 'default'));
|
||||
const initMethodSpys = initMethods.map((module) => jest.spyOn(module, 'default'));
|
||||
|
||||
it('app init properly with 404 not found route', async () => {
|
||||
const app = new Koa();
|
||||
await initI18n();
|
||||
await initApp(app);
|
||||
|
||||
for (const middleware of middlewareSpys) {
|
||||
for (const middleware of middlewareList) {
|
||||
expect(middleware).toBeCalled();
|
||||
}
|
||||
|
||||
for (const inits of initMethodSpys) {
|
||||
expect(inits).toBeCalled();
|
||||
}
|
||||
|
||||
expect(listenMock).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import type { Connector } from '@logto/schemas';
|
||||
import { mockEsmWithActual } from '@logto/shared/esm';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
|
||||
import { getConnectorConfig } from './index.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
const connectors: Connector[] = [
|
||||
{
|
||||
|
@ -15,13 +16,12 @@ const connectors: Connector[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const findAllConnectors = jest.fn(async () => connectors);
|
||||
|
||||
jest.mock('#src/queries/connector.js', () => ({
|
||||
...jest.requireActual('#src/queries/connector.js'),
|
||||
findAllConnectors: async () => findAllConnectors(),
|
||||
await mockEsmWithActual('#src/queries/connector.js', () => ({
|
||||
findAllConnectors: jest.fn(async () => connectors),
|
||||
}));
|
||||
|
||||
const { getConnectorConfig } = await import('./index.js');
|
||||
|
||||
it('getConnectorConfig() should return right config', async () => {
|
||||
const config = await getConnectorConfig('id');
|
||||
expect(config).toMatchObject({ foo: 'bar' });
|
||||
|
|
|
@ -9,6 +9,7 @@ import { createTestPool } from '#src/utils/test-utils.js';
|
|||
|
||||
import { buildInsertInto } from './insert-into.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const poolSpy = jest.spyOn(envSet, 'pool', 'get');
|
||||
|
||||
const buildExpectedInsertIntoSql = (keys: string[]) => [
|
||||
|
|
|
@ -8,6 +8,7 @@ import { createTestPool } from '#src/utils/test-utils.js';
|
|||
|
||||
import { buildUpdateWhere } from './update-where.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const poolSpy = jest.spyOn(envSet, 'pool', 'get');
|
||||
|
||||
describe('buildUpdateWhere()', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { SchemaLike, GeneratedSchema } from '@logto/schemas';
|
||||
import type { UpdateWhereData } from '@logto/shared';
|
||||
import { convertToIdentifiers, convertToPrimitiveOrSql, conditionalSql } from '@logto/shared';
|
||||
import type { UpdateWhereData } from '@logto/shared';
|
||||
import type { Truthy } from '@silverhand/essentials';
|
||||
import { notFalsy } from '@silverhand/essentials';
|
||||
import { sql } from 'slonik';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { createMockContext } from '@shopify/jest-koa-mocks';
|
||||
import type { ParameterizedContext } from 'koa';
|
||||
|
||||
import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js';
|
||||
|
||||
import detectLanguage from './detect-language.js';
|
||||
|
||||
describe('detectLanguage', () => {
|
||||
|
|
|
@ -1,56 +1,47 @@
|
|||
import { ConnectorType } from '@logto/connector-kit';
|
||||
import type { Passcode } from '@logto/schemas';
|
||||
import { PasscodeType } from '@logto/schemas';
|
||||
import { mockEsm } from '@logto/shared/esm';
|
||||
import { any } from 'zod';
|
||||
|
||||
import { mockConnector, mockMetadata } from '#src/__mocks__/index.js';
|
||||
import { defaultConnectorMethods } from '#src/connectors/consts.js';
|
||||
import { getLogtoConnectors } from '#src/connectors/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import {
|
||||
consumePasscode,
|
||||
deletePasscodesByIds,
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const {
|
||||
findUnconsumedPasscodeByJtiAndType,
|
||||
findUnconsumedPasscodesByJtiAndType,
|
||||
deletePasscodesByIds,
|
||||
increasePasscodeTryCount,
|
||||
insertPasscode,
|
||||
} from '#src/queries/passcode.js';
|
||||
consumePasscode,
|
||||
} = mockEsm('#src/queries/passcode.js', () => ({
|
||||
findUnconsumedPasscodesByJtiAndType: jest.fn(),
|
||||
findUnconsumedPasscodeByJtiAndType: jest.fn(),
|
||||
deletePasscodesByIds: jest.fn(),
|
||||
insertPasscode: jest.fn(),
|
||||
consumePasscode: jest.fn(),
|
||||
increasePasscodeTryCount: jest.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
const { getLogtoConnectors } = mockEsm('#src/connectors/index.js', () => ({
|
||||
getLogtoConnectors: jest.fn(),
|
||||
}));
|
||||
|
||||
const {
|
||||
createPasscode,
|
||||
passcodeExpiration,
|
||||
passcodeMaxTryCount,
|
||||
passcodeLength,
|
||||
sendPasscode,
|
||||
verifyPasscode,
|
||||
} from './passcode.js';
|
||||
|
||||
jest.mock('#src/queries/passcode.js');
|
||||
jest.mock('#src/connectors.js');
|
||||
|
||||
const mockedFindUnconsumedPasscodesByJtiAndType =
|
||||
findUnconsumedPasscodesByJtiAndType as jest.MockedFunction<
|
||||
typeof findUnconsumedPasscodesByJtiAndType
|
||||
>;
|
||||
const mockedFindUnconsumedPasscodeByJtiAndType =
|
||||
findUnconsumedPasscodeByJtiAndType as jest.MockedFunction<
|
||||
typeof findUnconsumedPasscodeByJtiAndType
|
||||
>;
|
||||
const mockedDeletePasscodesByIds = deletePasscodesByIds as jest.MockedFunction<
|
||||
typeof deletePasscodesByIds
|
||||
>;
|
||||
const mockedInsertPasscode = insertPasscode as jest.MockedFunction<typeof insertPasscode>;
|
||||
const mockedGetLogtoConnectors = getLogtoConnectors as jest.MockedFunction<
|
||||
typeof getLogtoConnectors
|
||||
>;
|
||||
const mockedConsumePasscode = consumePasscode as jest.MockedFunction<typeof consumePasscode>;
|
||||
const mockedIncreasePasscodeTryCount = increasePasscodeTryCount as jest.MockedFunction<
|
||||
typeof increasePasscodeTryCount
|
||||
>;
|
||||
} = await import('./passcode.js');
|
||||
|
||||
beforeAll(() => {
|
||||
mockedFindUnconsumedPasscodesByJtiAndType.mockResolvedValue([]);
|
||||
mockedInsertPasscode.mockImplementation(async (data): Promise<Passcode> => {
|
||||
findUnconsumedPasscodesByJtiAndType.mockResolvedValue([]);
|
||||
insertPasscode.mockImplementation(async (data): Promise<Passcode> => {
|
||||
return {
|
||||
phone: null,
|
||||
email: null,
|
||||
|
@ -88,7 +79,7 @@ describe('createPasscode', () => {
|
|||
it('should disable existing passcode', async () => {
|
||||
const email = 'jony@example.com';
|
||||
const jti = 'jti';
|
||||
mockedFindUnconsumedPasscodesByJtiAndType.mockResolvedValue([
|
||||
findUnconsumedPasscodesByJtiAndType.mockResolvedValue([
|
||||
{
|
||||
id: 'id',
|
||||
interactionJti: jti,
|
||||
|
@ -104,7 +95,7 @@ describe('createPasscode', () => {
|
|||
await createPasscode(jti, PasscodeType.SignIn, {
|
||||
email,
|
||||
});
|
||||
expect(mockedDeletePasscodesByIds).toHaveBeenCalledWith(['id']);
|
||||
expect(deletePasscodesByIds).toHaveBeenCalledWith(['id']);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -127,7 +118,7 @@ describe('sendPasscode', () => {
|
|||
});
|
||||
|
||||
it('should throw error when email or sms connector can not be found', async () => {
|
||||
mockedGetLogtoConnectors.mockResolvedValueOnce([
|
||||
getLogtoConnectors.mockResolvedValueOnce([
|
||||
{
|
||||
...defaultConnectorMethods,
|
||||
dbEntry: {
|
||||
|
@ -164,7 +155,7 @@ describe('sendPasscode', () => {
|
|||
|
||||
it('should call sendPasscode with params matching', async () => {
|
||||
const sendMessage = jest.fn();
|
||||
mockedGetLogtoConnectors.mockResolvedValueOnce([
|
||||
getLogtoConnectors.mockResolvedValueOnce([
|
||||
{
|
||||
...defaultConnectorMethods,
|
||||
configGuard: any(),
|
||||
|
@ -230,20 +221,20 @@ describe('verifyPasscode', () => {
|
|||
};
|
||||
|
||||
it('should mark as consumed on successful verification', async () => {
|
||||
mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue(passcode);
|
||||
findUnconsumedPasscodeByJtiAndType.mockResolvedValue(passcode);
|
||||
await verifyPasscode(passcode.interactionJti, passcode.type, passcode.code, { phone: 'phone' });
|
||||
expect(mockedConsumePasscode).toHaveBeenCalledWith(passcode.id);
|
||||
expect(consumePasscode).toHaveBeenCalledWith(passcode.id);
|
||||
});
|
||||
|
||||
it('should fail when passcode not found', async () => {
|
||||
mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue(null);
|
||||
findUnconsumedPasscodeByJtiAndType.mockResolvedValue(null);
|
||||
await expect(
|
||||
verifyPasscode(passcode.interactionJti, passcode.type, passcode.code, { phone: 'phone' })
|
||||
).rejects.toThrow(new RequestError('passcode.not_found'));
|
||||
});
|
||||
|
||||
it('should fail when phone mismatch', async () => {
|
||||
mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue(passcode);
|
||||
findUnconsumedPasscodeByJtiAndType.mockResolvedValue(passcode);
|
||||
await expect(
|
||||
verifyPasscode(passcode.interactionJti, passcode.type, passcode.code, {
|
||||
phone: 'invalid_phone',
|
||||
|
@ -252,7 +243,7 @@ describe('verifyPasscode', () => {
|
|||
});
|
||||
|
||||
it('should fail when email mismatch', async () => {
|
||||
mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue({
|
||||
findUnconsumedPasscodeByJtiAndType.mockResolvedValue({
|
||||
...passcode,
|
||||
phone: null,
|
||||
email: 'email',
|
||||
|
@ -265,7 +256,7 @@ describe('verifyPasscode', () => {
|
|||
});
|
||||
|
||||
it('should fail when expired', async () => {
|
||||
mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue({
|
||||
findUnconsumedPasscodeByJtiAndType.mockResolvedValue({
|
||||
...passcode,
|
||||
createdAt: Date.now() - passcodeExpiration - 100,
|
||||
});
|
||||
|
@ -275,7 +266,7 @@ describe('verifyPasscode', () => {
|
|||
});
|
||||
|
||||
it('should fail when exceed max count', async () => {
|
||||
mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue({
|
||||
findUnconsumedPasscodeByJtiAndType.mockResolvedValue({
|
||||
...passcode,
|
||||
tryCount: passcodeMaxTryCount,
|
||||
});
|
||||
|
@ -285,10 +276,10 @@ describe('verifyPasscode', () => {
|
|||
});
|
||||
|
||||
it('should fail when invalid code, and should increase try_count', async () => {
|
||||
mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue(passcode);
|
||||
findUnconsumedPasscodeByJtiAndType.mockResolvedValue(passcode);
|
||||
await expect(
|
||||
verifyPasscode(passcode.interactionJti, passcode.type, 'invalid', { phone: 'phone' })
|
||||
).rejects.toThrow(new RequestError('passcode.code_mismatch'));
|
||||
expect(mockedIncreasePasscodeTryCount).toHaveBeenCalledWith(passcode.id);
|
||||
expect(increasePasscodeTryCount).toHaveBeenCalledWith(passcode.id);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import resource from '@logto/phrases-ui';
|
||||
import type { CustomPhrase } from '@logto/schemas';
|
||||
import { mockEsm } from '@logto/shared/esm';
|
||||
import deepmerge from 'deepmerge';
|
||||
|
||||
import {
|
||||
|
@ -12,8 +13,8 @@ import {
|
|||
zhHkTag,
|
||||
} from '#src/__mocks__/custom-phrase.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { getPhrase } from '#src/lib/phrase.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const englishBuiltInPhrase = resource[enTag];
|
||||
|
||||
const customOnlyLanguage = zhHkTag;
|
||||
|
@ -39,10 +40,12 @@ const findCustomPhraseByLanguageTag = jest.fn(async (languageTag: string) => {
|
|||
return mockCustomPhrase;
|
||||
});
|
||||
|
||||
jest.mock('#src/queries/custom-phrase.js', () => ({
|
||||
findCustomPhraseByLanguageTag: async (key: string) => findCustomPhraseByLanguageTag(key),
|
||||
mockEsm('#src/queries/custom-phrase.js', () => ({
|
||||
findCustomPhraseByLanguageTag,
|
||||
}));
|
||||
|
||||
const { getPhrase } = await import('#src/lib/phrase.js');
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import type { LanguageTag } from '@logto/language-kit';
|
|||
import { builtInLanguages } from '@logto/phrases-ui';
|
||||
import type { CreateSignInExperience, SignInExperience } from '@logto/schemas';
|
||||
import { BrandingStyle } from '@logto/schemas';
|
||||
import { mockEsm } from '@logto/shared/esm';
|
||||
|
||||
import {
|
||||
socialTarget01,
|
||||
|
@ -10,42 +11,36 @@ import {
|
|||
mockSignInExperience,
|
||||
mockSocialConnectors,
|
||||
} from '#src/__mocks__/index.js';
|
||||
import type { LogtoConnector } from '#src/connectors/types.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import {
|
||||
|
||||
const { jest } = import.meta;
|
||||
const allCustomLanguageTags: LanguageTag[] = [];
|
||||
|
||||
const { findAllCustomLanguageTags } = mockEsm('#src/queries/custom-phrase.js', () => ({
|
||||
findAllCustomLanguageTags: jest.fn(async () => allCustomLanguageTags),
|
||||
}));
|
||||
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<CreateSignInExperience>): Promise<SignInExperience> => ({
|
||||
...mockSignInExperience,
|
||||
...data,
|
||||
})
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
validateBranding,
|
||||
validateTermsOfUse,
|
||||
validateLanguageInfo,
|
||||
removeUnavailableSocialConnectorTargets,
|
||||
} from '#src/lib/sign-in-experience/index.js';
|
||||
import { updateDefaultSignInExperience } from '#src/queries/sign-in-experience.js';
|
||||
|
||||
const allCustomLanguageTags: LanguageTag[] = [];
|
||||
const findAllCustomLanguageTags = jest.fn(async () => allCustomLanguageTags);
|
||||
const getLogtoConnectorsPlaceHolder = jest.fn() as jest.MockedFunction<
|
||||
() => Promise<LogtoConnector[]>
|
||||
>;
|
||||
const findDefaultSignInExperience = jest.fn() as jest.MockedFunction<
|
||||
() => Promise<SignInExperience>
|
||||
>;
|
||||
|
||||
jest.mock('#src/queries/custom-phrase.js', () => ({
|
||||
findAllCustomLanguageTags: async () => findAllCustomLanguageTags(),
|
||||
}));
|
||||
|
||||
jest.mock('#src/connectors.js', () => ({
|
||||
getLogtoConnectors: async () => getLogtoConnectorsPlaceHolder(),
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/sign-in-experience.js', () => ({
|
||||
findDefaultSignInExperience: async () => findDefaultSignInExperience(),
|
||||
updateDefaultSignInExperience: jest.fn(
|
||||
async (data: Partial<CreateSignInExperience>): Promise<SignInExperience> => ({
|
||||
...mockSignInExperience,
|
||||
...data,
|
||||
})
|
||||
),
|
||||
}));
|
||||
} = await import('./index.js');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -163,7 +158,7 @@ describe('remove unavailable social connector targets', () => {
|
|||
...mockSignInExperience,
|
||||
socialSignInConnectorTargets: mockSocialConnectorTargets,
|
||||
});
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce(mockSocialConnectors);
|
||||
getLogtoConnectors.mockResolvedValueOnce(mockSocialConnectors);
|
||||
expect(mockSocialConnectorTargets).toEqual([socialTarget01, socialTarget02]);
|
||||
await removeUnavailableSocialConnectorTargets();
|
||||
expect(updateDefaultSignInExperience).toBeCalledWith({
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import { ConnectorType, SignInIdentifier } from '@logto/schemas';
|
||||
import { mockEsmWithActual } from '@logto/shared/esm';
|
||||
|
||||
import { mockAliyunDmConnector, mockAliyunSmsConnector, mockSignUp } from '#src/__mocks__/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
|
||||
import { validateSignUp } from './sign-up.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const enabledConnectors = [mockAliyunDmConnector, mockAliyunSmsConnector];
|
||||
|
||||
jest.mock('#src/lib/session.js', () => ({
|
||||
...jest.requireActual('#src/lib/session.js'),
|
||||
await mockEsmWithActual('#src/lib/session.js', () => ({
|
||||
getApplicationIdFromInteraction: jest.fn(),
|
||||
}));
|
||||
|
||||
const { validateSignUp } = await import('./sign-up.js');
|
||||
|
||||
describe('validate sign-up', () => {
|
||||
describe('There must be at least one connector for the specific identifier.', () => {
|
||||
test('should throw when there is no email connector and identifier is email', async () => {
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
import { UsersPasswordEncryptionMethod } from '@logto/schemas';
|
||||
import { mockEsmWithActual } from '@logto/shared/esm';
|
||||
|
||||
import { hasUserWithId, updateUserById } from '#src/queries/user.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
import { encryptUserPassword, generateUserId } from './user.js';
|
||||
const { updateUserById, hasUserWithId } = await mockEsmWithActual('#src/queries/user.js', () => ({
|
||||
updateUserById: jest.fn(),
|
||||
hasUserWithId: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/user.js');
|
||||
const { encryptUserPassword, generateUserId } = await import('./user.js');
|
||||
|
||||
describe('generateUserId()', () => {
|
||||
afterEach(() => {
|
||||
(hasUserWithId as jest.MockedFunction<typeof hasUserWithId>).mockClear();
|
||||
hasUserWithId.mockClear();
|
||||
});
|
||||
|
||||
it('generates user ID with correct length when no conflict found', async () => {
|
||||
const mockedHasUserWithId = (
|
||||
hasUserWithId as jest.MockedFunction<typeof hasUserWithId>
|
||||
).mockImplementationOnce(async () => false);
|
||||
const mockedHasUserWithId = hasUserWithId.mockImplementationOnce(async () => false);
|
||||
|
||||
await expect(generateUserId()).resolves.toHaveLength(12);
|
||||
expect(mockedHasUserWithId).toBeCalledTimes(1);
|
||||
|
@ -23,9 +25,7 @@ describe('generateUserId()', () => {
|
|||
it('generates user ID with correct length when retry limit is not reached', async () => {
|
||||
// eslint-disable-next-line @silverhand/fp/no-let
|
||||
let tried = 0;
|
||||
const mockedHasUserWithId = (
|
||||
hasUserWithId as jest.MockedFunction<typeof hasUserWithId>
|
||||
).mockImplementation(async () => {
|
||||
const mockedHasUserWithId = hasUserWithId.mockImplementation(async () => {
|
||||
if (tried) {
|
||||
return false;
|
||||
}
|
||||
|
@ -41,9 +41,7 @@ describe('generateUserId()', () => {
|
|||
});
|
||||
|
||||
it('rejects with correct error message when retry limit is reached', async () => {
|
||||
const mockedHasUserWithId = (
|
||||
hasUserWithId as jest.MockedFunction<typeof hasUserWithId>
|
||||
).mockImplementation(async () => true);
|
||||
const mockedHasUserWithId = hasUserWithId.mockImplementation(async () => true);
|
||||
|
||||
await expect(generateUserId(10)).rejects.toThrow(
|
||||
'Cannot generate user ID in reasonable retries'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { UserRole } from '@logto/schemas';
|
||||
import { jwtVerify } from 'jose';
|
||||
import { mockEsm, pickDefault } from '@logto/shared/esm';
|
||||
import type { Context } from 'koa';
|
||||
import type { IRouterParamContext } from 'koa-router';
|
||||
|
||||
|
@ -8,12 +8,15 @@ import RequestError from '#src/errors/RequestError/index.js';
|
|||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import type { WithAuthContext } from './koa-auth.js';
|
||||
import koaAuth from './koa-auth.js';
|
||||
|
||||
jest.mock('jose', () => ({
|
||||
jwtVerify: jest.fn(() => ({ payload: { sub: 'fooUser', role_names: ['admin'] } })),
|
||||
const { jest } = import.meta;
|
||||
|
||||
const { jwtVerify } = mockEsm('jose', () => ({
|
||||
jwtVerify: jest.fn().mockReturnValue({ payload: { sub: 'fooUser', role_names: ['admin'] } }),
|
||||
}));
|
||||
|
||||
const koaAuth = await pickDefault(import('./koa-auth.js'));
|
||||
|
||||
describe('koaAuth middleware', () => {
|
||||
const baseCtx = createContextWithRouteParameters();
|
||||
|
||||
|
@ -136,8 +139,7 @@ describe('koaAuth middleware', () => {
|
|||
});
|
||||
|
||||
it('expect to throw if jwt sub is missing', async () => {
|
||||
const mockJwtVerify = jwtVerify as jest.Mock;
|
||||
mockJwtVerify.mockImplementationOnce(() => ({ payload: {} }));
|
||||
jwtVerify.mockImplementationOnce(() => ({ payload: {} }));
|
||||
|
||||
ctx.request = {
|
||||
...ctx.request,
|
||||
|
@ -150,8 +152,7 @@ describe('koaAuth middleware', () => {
|
|||
});
|
||||
|
||||
it('expect to have `client` type per jwt verify result', async () => {
|
||||
const mockJwtVerify = jwtVerify as jest.Mock;
|
||||
mockJwtVerify.mockImplementationOnce(() => ({ payload: { sub: 'bar', client_id: 'bar' } }));
|
||||
jwtVerify.mockImplementationOnce(() => ({ payload: { sub: 'bar', client_id: 'bar' } }));
|
||||
|
||||
ctx.request = {
|
||||
...ctx.request,
|
||||
|
@ -165,8 +166,7 @@ describe('koaAuth middleware', () => {
|
|||
});
|
||||
|
||||
it('expect to throw if jwt role_names is missing', async () => {
|
||||
const mockJwtVerify = jwtVerify as jest.Mock;
|
||||
mockJwtVerify.mockImplementationOnce(() => ({ payload: { sub: 'fooUser' } }));
|
||||
jwtVerify.mockImplementationOnce(() => ({ payload: { sub: 'fooUser' } }));
|
||||
|
||||
ctx.request = {
|
||||
...ctx.request,
|
||||
|
@ -179,8 +179,7 @@ describe('koaAuth middleware', () => {
|
|||
});
|
||||
|
||||
it('expect to throw if jwt role_names does not include admin', async () => {
|
||||
const mockJwtVerify = jwtVerify as jest.Mock;
|
||||
mockJwtVerify.mockImplementationOnce(() => ({
|
||||
jwtVerify.mockImplementationOnce(() => ({
|
||||
payload: { sub: 'fooUser', role_names: ['foo'] },
|
||||
}));
|
||||
|
||||
|
@ -195,8 +194,7 @@ describe('koaAuth middleware', () => {
|
|||
});
|
||||
|
||||
it('expect to throw unauthorized error if unknown error occurs', async () => {
|
||||
const mockJwtVerify = jwtVerify as jest.Mock;
|
||||
mockJwtVerify.mockImplementationOnce(() => {
|
||||
jwtVerify.mockImplementationOnce(() => {
|
||||
throw new Error('unknown error');
|
||||
});
|
||||
ctx.request = {
|
||||
|
|
|
@ -5,6 +5,8 @@ import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
|||
|
||||
import koaConnectorErrorHandler from './koa-connector-error-handler.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
describe('koaConnectorErrorHandler middleware', () => {
|
||||
const next = jest.fn();
|
||||
const ctx = createContextWithRouteParameters();
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { createMockContext } from '@shopify/jest-koa-mocks';
|
||||
import createHttpError from 'http-errors';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js';
|
||||
|
||||
import koaErrorHandler from './koa-error-handler.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
describe('koaErrorHandler middleware', () => {
|
||||
const mockBody = { data: 'foo' };
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { mockEsmDefault } from '@logto/shared/esm';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { emptyMiddleware, createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import koaGuard, { isGuardMiddleware } from './koa-guard.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('koa-body', () => emptyMiddleware);
|
||||
mockEsmDefault('koa-body', () => emptyMiddleware);
|
||||
const { default: koaGuard, isGuardMiddleware } = await import('./koa-guard.js');
|
||||
|
||||
describe('koaGuardMiddleware', () => {
|
||||
describe('isGuardMiddleware', () => {
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { mockEsmDefault, pickDefault } from '@logto/shared/esm';
|
||||
import i18next from 'i18next';
|
||||
|
||||
import initI18n from '#src/i18n/init.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import koaI18next from './koa-i18next.js';
|
||||
const { jest } = import.meta;
|
||||
const mockLanguage = () => ['zh-cn'];
|
||||
mockEsmDefault('#src/i18n/detect-language.js', () => mockLanguage);
|
||||
|
||||
// Can not access outter scope function in jest mock
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
jest.mock('#src/i18n/detect-language.js', () => () => ['zh-cn']);
|
||||
const initI18n = await pickDefault(import('#src/i18n/init.js'));
|
||||
const koaI18next = await pickDefault(import('./koa-i18next.js'));
|
||||
const changLanguageSpy = jest.spyOn(i18next, 'changeLanguage');
|
||||
|
||||
describe('koaI18next', () => {
|
||||
|
|
|
@ -4,13 +4,10 @@ import koaLogSession from '#src/middleware/koa-log-session.js';
|
|||
import type { WithLogContext } from '#src/middleware/koa-log.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
const interactionDetails: jest.MockedFunction<() => Promise<unknown>> = jest.fn(async () => ({}));
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails,
|
||||
})),
|
||||
}));
|
||||
const provider = new Provider('https://logto.test');
|
||||
const interactionDetails = jest.spyOn(provider, 'interactionDetails');
|
||||
|
||||
describe('koaLogSession', () => {
|
||||
const sessionId = 'sessionId';
|
||||
|
@ -19,6 +16,7 @@ describe('koaLogSession', () => {
|
|||
const log = jest.fn();
|
||||
const next = jest.fn();
|
||||
|
||||
// @ts-expect-error for testing
|
||||
interactionDetails.mockResolvedValue({
|
||||
jti: sessionId,
|
||||
params: {
|
||||
|
@ -37,7 +35,7 @@ describe('koaLogSession', () => {
|
|||
log,
|
||||
};
|
||||
|
||||
await expect(koaLogSession(new Provider(''))(ctx, next)).resolves.not.toThrow();
|
||||
await expect(koaLogSession(provider)(ctx, next)).resolves.not.toThrow();
|
||||
expect(interactionDetails).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -48,7 +46,7 @@ describe('koaLogSession', () => {
|
|||
log,
|
||||
};
|
||||
|
||||
await expect(koaLogSession(new Provider(''))(ctx, next)).resolves.not.toThrow();
|
||||
await expect(koaLogSession(provider)(ctx, next)).resolves.not.toThrow();
|
||||
expect(addLogContext).toHaveBeenCalledWith({ sessionId, applicationId });
|
||||
});
|
||||
|
||||
|
@ -59,7 +57,7 @@ describe('koaLogSession', () => {
|
|||
log,
|
||||
};
|
||||
|
||||
await expect(koaLogSession(new Provider(''))(ctx, next)).resolves.not.toThrow();
|
||||
await expect(koaLogSession(provider)(ctx, next)).resolves.not.toThrow();
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -74,6 +72,6 @@ describe('koaLogSession', () => {
|
|||
throw new Error('message');
|
||||
});
|
||||
|
||||
await expect(koaLogSession(new Provider(''))(ctx, next)).resolves.not.toThrow();
|
||||
await expect(koaLogSession(provider)(ctx, next)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,29 +1,31 @@
|
|||
import type { LogPayload } from '@logto/schemas';
|
||||
import { LogResult } from '@logto/schemas';
|
||||
import { mockEsm, pickDefault } from '@logto/shared/esm';
|
||||
import i18next from 'i18next';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { insertLog } from '#src/queries/log.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import type { WithLogContext } from './koa-log.js';
|
||||
import koaLog from './koa-log.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const nanoIdMock = 'mockId';
|
||||
|
||||
const addLogContext = jest.fn();
|
||||
const log = jest.fn();
|
||||
|
||||
jest.mock('#src/queries/log.js', () => ({
|
||||
insertLog: jest.fn(async () => 0),
|
||||
const { insertLog } = mockEsm('#src/queries/log.js', () => ({
|
||||
insertLog: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('nanoid', () => ({
|
||||
nanoid: jest.fn(() => nanoIdMock),
|
||||
mockEsm('nanoid', () => ({
|
||||
nanoid: () => nanoIdMock,
|
||||
}));
|
||||
|
||||
const koaLog = await pickDefault(import('./koa-log.js'));
|
||||
|
||||
describe('koaLog middleware', () => {
|
||||
const insertLogMock = insertLog as jest.Mock;
|
||||
const type = 'SignInUsernamePassword';
|
||||
const mockPayload: LogPayload = {
|
||||
userId: 'foo',
|
||||
|
@ -54,7 +56,7 @@ describe('koaLog middleware', () => {
|
|||
};
|
||||
await koaLog()(ctx, next);
|
||||
|
||||
expect(insertLogMock).toBeCalledWith({
|
||||
expect(insertLog).toBeCalledWith({
|
||||
id: nanoIdMock,
|
||||
type,
|
||||
payload: {
|
||||
|
@ -79,7 +81,7 @@ describe('koaLog middleware', () => {
|
|||
// eslint-disable-next-line unicorn/consistent-function-scoping, @typescript-eslint/no-empty-function
|
||||
const next = async () => {};
|
||||
await koaLog()(ctx, next);
|
||||
expect(insertLogMock).not.toBeCalled();
|
||||
expect(insertLog).not.toBeCalled();
|
||||
});
|
||||
|
||||
describe('should insert an error log with the error message when next() throws an error', () => {
|
||||
|
@ -101,7 +103,7 @@ describe('koaLog middleware', () => {
|
|||
};
|
||||
await expect(koaLog()(ctx, next)).rejects.toMatchError(error);
|
||||
|
||||
expect(insertLogMock).toBeCalledWith({
|
||||
expect(insertLog).toBeCalledWith({
|
||||
id: nanoIdMock,
|
||||
type,
|
||||
payload: {
|
||||
|
@ -135,7 +137,7 @@ describe('koaLog middleware', () => {
|
|||
};
|
||||
await expect(koaLog()(ctx, next)).rejects.toMatchError(error);
|
||||
|
||||
expect(insertLogMock).toBeCalledWith({
|
||||
expect(insertLog).toBeCalledWith({
|
||||
id: nanoIdMock,
|
||||
type,
|
||||
payload: {
|
||||
|
|
|
@ -5,6 +5,8 @@ import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
|||
|
||||
import koaOIDCErrorHandler from './koa-oidc-error-handler.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
describe('koaOIDCErrorHandler middleware', () => {
|
||||
const next = jest.fn();
|
||||
const ctx = createContextWithRouteParameters();
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { createMockContext } from '@shopify/jest-koa-mocks';
|
||||
import type { Context } from 'koa';
|
||||
|
||||
import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js';
|
||||
|
||||
import type { WithPaginationContext } from './koa-pagination.js';
|
||||
import koaPagination from './koa-pagination.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const next = jest.fn();
|
||||
const setHeader = jest.fn();
|
||||
const links = new Set<string>();
|
||||
|
|
|
@ -2,6 +2,8 @@ import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
|||
|
||||
import koaRootProxy from './koa-root-proxy.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
describe('koaRootProxy', () => {
|
||||
const next = jest.fn();
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
|||
|
||||
import koaSlonikErrorHandler from './koa-slonik-error-handler.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
describe('koaSlonikErrorHandler middleware', () => {
|
||||
const next = jest.fn();
|
||||
const ctx = createContextWithRouteParameters();
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import { mockEsmDefault, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import envSet, { MountedApps } from '#src/env-set/index.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import koaSpaProxy from './koa-spa-proxy.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
const mockProxyMiddleware = jest.fn();
|
||||
const mockStaticMiddleware = jest.fn();
|
||||
|
||||
jest.mock('fs/promises', () => ({
|
||||
...jest.requireActual('fs/promises'),
|
||||
mockEsmDefault('fs/promises', () => ({
|
||||
readdir: jest.fn().mockResolvedValue(['sign-in']),
|
||||
}));
|
||||
|
||||
jest.mock('koa-proxies', () => jest.fn(() => mockProxyMiddleware));
|
||||
jest.mock('#src/middleware/koa-serve-static.js', () => jest.fn(() => mockStaticMiddleware));
|
||||
mockEsmDefault('koa-proxies', () => jest.fn(() => mockProxyMiddleware));
|
||||
mockEsmDefault('#src/middleware/koa-serve-static.js', () => jest.fn(() => mockStaticMiddleware));
|
||||
|
||||
const koaSpaProxy = await pickDefault(import('./koa-spa-proxy.js'));
|
||||
|
||||
describe('koaSpaProxy middleware', () => {
|
||||
const envBackup = process.env;
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
import { mockEsmWithActual } from '@logto/shared/esm';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import { MountedApps } from '#src/env-set/index.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import koaSpaSessionGuard, { sessionNotFoundPath, guardedPath } from './koa-spa-session-guard.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('fs/promises', () => ({
|
||||
...jest.requireActual('fs/promises'),
|
||||
await mockEsmWithActual('fs/promises', () => ({
|
||||
readdir: jest.fn().mockResolvedValue(['index.js']),
|
||||
}));
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
const {
|
||||
default: koaSpaSessionGuard,
|
||||
sessionNotFoundPath,
|
||||
guardedPath,
|
||||
} = await import('./koa-spa-session-guard.js');
|
||||
|
||||
describe('koaSpaSessionGuard', () => {
|
||||
const envBackup = process.env;
|
||||
const provider = new Provider('https://logto.test');
|
||||
const interactionDetails = jest.spyOn(provider, 'interactionDetails');
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...envBackup };
|
||||
|
@ -33,7 +35,6 @@ describe('koaSpaSessionGuard', () => {
|
|||
for (const app of Object.values(MountedApps)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
it(`${app} path should not redirect`, async () => {
|
||||
const provider = new Provider('');
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: `/${app}/foo`,
|
||||
});
|
||||
|
@ -45,9 +46,7 @@ describe('koaSpaSessionGuard', () => {
|
|||
}
|
||||
|
||||
it(`should not redirect for path ${sessionNotFoundPath}`, async () => {
|
||||
const provider = new Provider('');
|
||||
|
||||
(provider.interactionDetails as jest.Mock).mockRejectedValue(new Error('session not found'));
|
||||
interactionDetails.mockRejectedValue(new Error('session not found'));
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: `${sessionNotFoundPath}`,
|
||||
});
|
||||
|
@ -56,9 +55,7 @@ describe('koaSpaSessionGuard', () => {
|
|||
});
|
||||
|
||||
it(`should not redirect for path /callback`, async () => {
|
||||
const provider = new Provider('');
|
||||
|
||||
(provider.interactionDetails as jest.Mock).mockRejectedValue(new Error('session not found'));
|
||||
interactionDetails.mockRejectedValue(new Error('session not found'));
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: '/callback/github',
|
||||
});
|
||||
|
@ -67,7 +64,8 @@ describe('koaSpaSessionGuard', () => {
|
|||
});
|
||||
|
||||
it('should not redirect if session found', async () => {
|
||||
const provider = new Provider('');
|
||||
// @ts-expect-error for testing
|
||||
interactionDetails.mockResolvedValue({});
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: `/sign-in`,
|
||||
});
|
||||
|
@ -78,9 +76,7 @@ describe('koaSpaSessionGuard', () => {
|
|||
for (const path of guardedPath) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
it(`should redirect if session not found for ${path}`, async () => {
|
||||
const provider = new Provider('');
|
||||
|
||||
(provider.interactionDetails as jest.Mock).mockRejectedValue(new Error('session not found'));
|
||||
interactionDetails.mockRejectedValue(new Error('session not found'));
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: `${path}/foo`,
|
||||
});
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import { mockEsm, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import envSet, { MountedApps } from '#src/env-set/index.js';
|
||||
import { hasActiveUsers } from '#src/queries/user.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import koaWelcomeProxy from './koa-welcome-proxy.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('#src/queries/user.js', () => ({
|
||||
const { hasActiveUsers } = mockEsm('#src/queries/user.js', () => ({
|
||||
hasActiveUsers: jest.fn(),
|
||||
}));
|
||||
|
||||
const koaWelcomeProxy = await pickDefault(import('./koa-welcome-proxy.js'));
|
||||
|
||||
describe('koaWelcomeProxy', () => {
|
||||
const next = jest.fn();
|
||||
|
||||
|
@ -18,7 +21,7 @@ describe('koaWelcomeProxy', () => {
|
|||
|
||||
it('should redirect to admin console if has AdminUsers', async () => {
|
||||
const { endpoint } = envSet.values;
|
||||
(hasActiveUsers as jest.Mock).mockResolvedValue(true);
|
||||
hasActiveUsers.mockResolvedValue(true);
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: `/${MountedApps.Welcome}`,
|
||||
});
|
||||
|
@ -31,7 +34,7 @@ describe('koaWelcomeProxy', () => {
|
|||
|
||||
it('should redirect to welcome page if has no Users', async () => {
|
||||
const { endpoint } = envSet.values;
|
||||
(hasActiveUsers as jest.Mock).mockResolvedValue(false);
|
||||
hasActiveUsers.mockResolvedValue(false);
|
||||
const ctx = createContextWithRouteParameters({
|
||||
url: `/${MountedApps.Welcome}`,
|
||||
});
|
||||
|
|
|
@ -1,24 +1,18 @@
|
|||
import type { Application } from '@logto/schemas';
|
||||
import { mockEsm } from '@logto/shared/esm';
|
||||
import snakecaseKeys from 'snakecase-keys';
|
||||
|
||||
import { mockApplication } from '#src/__mocks__/index.js';
|
||||
import {
|
||||
consumeInstanceById,
|
||||
destroyInstanceById,
|
||||
findPayloadById,
|
||||
findPayloadByPayloadField,
|
||||
revokeInstanceByGrantId,
|
||||
upsertInstance,
|
||||
} from '#src/queries/oidc-model-instance.js';
|
||||
|
||||
import postgresAdapter from './adapter.js';
|
||||
import { getConstantClientMetadata } from './utils.js';
|
||||
|
||||
jest.mock('#src/queries/application.js', () => ({
|
||||
const { jest } = import.meta;
|
||||
|
||||
mockEsm('#src/queries/application.js', () => ({
|
||||
findApplicationById: jest.fn(async (): Promise<Application> => mockApplication),
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/oidc-model-instance.js', () => ({
|
||||
mockEsm('#src/queries/oidc-model-instance.js', () => ({
|
||||
upsertInstance: jest.fn(),
|
||||
findPayloadById: jest.fn(),
|
||||
findPayloadByPayloadField: jest.fn(),
|
||||
|
@ -27,15 +21,25 @@ jest.mock('#src/queries/oidc-model-instance.js', () => ({
|
|||
revokeInstanceByGrantId: jest.fn(),
|
||||
}));
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
jest.mock(
|
||||
mockEsm(
|
||||
'date-fns',
|
||||
jest.fn(() => ({
|
||||
addSeconds: jest.fn((_: Date, seconds: number) => new Date(now + seconds * 1000)),
|
||||
}))
|
||||
);
|
||||
|
||||
const { default: postgresAdapter } = await import('./adapter.js');
|
||||
const {
|
||||
consumeInstanceById,
|
||||
destroyInstanceById,
|
||||
findPayloadById,
|
||||
findPayloadByPayloadField,
|
||||
revokeInstanceByGrantId,
|
||||
upsertInstance,
|
||||
} = await import('#src/queries/oidc-model-instance.js');
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
describe('postgres Adapter', () => {
|
||||
it('Client Modal', async () => {
|
||||
const rejectError = new Error('Not implemented');
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
deleteApplicationById,
|
||||
} from './application.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
|
||||
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
updateConnector,
|
||||
} from './connector.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
|
||||
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
|
||||
|
|
|
@ -1,21 +1,14 @@
|
|||
import type { CreateOidcModelInstance } from '@logto/schemas';
|
||||
import { OidcModelInstances } from '@logto/schemas';
|
||||
import { convertToIdentifiers } from '@logto/shared';
|
||||
import { mockEsmWithActual } from '@logto/shared/esm';
|
||||
import { createMockPool, createMockQueryResult, sql } from 'slonik';
|
||||
|
||||
import envSet from '#src/env-set/index.js';
|
||||
import type { QueryType } from '#src/utils/test-utils.js';
|
||||
import { expectSqlAssert } from '#src/utils/test-utils.js';
|
||||
|
||||
import {
|
||||
upsertInstance,
|
||||
findPayloadById,
|
||||
findPayloadByPayloadField,
|
||||
consumeInstanceById,
|
||||
destroyInstanceById,
|
||||
revokeInstanceByGrantId,
|
||||
} from './oidc-model-instance.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
|
||||
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
|
||||
|
@ -26,11 +19,19 @@ jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
|
|||
})
|
||||
);
|
||||
|
||||
jest.mock('@logto/shared', () => ({
|
||||
...jest.requireActual('@logto/shared'),
|
||||
await mockEsmWithActual('@logto/shared', () => ({
|
||||
convertToTimestamp: () => 100,
|
||||
}));
|
||||
|
||||
const {
|
||||
upsertInstance,
|
||||
findPayloadById,
|
||||
findPayloadByPayloadField,
|
||||
consumeInstanceById,
|
||||
destroyInstanceById,
|
||||
revokeInstanceByGrantId,
|
||||
} = await import('./oidc-model-instance.js');
|
||||
|
||||
describe('oidc-model-instance query', () => {
|
||||
const { table, fields } = convertToIdentifiers(OidcModelInstances);
|
||||
const expiresAt = Date.now();
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
deletePasscodesByIds,
|
||||
} from './passcode.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
|
||||
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
deleteResourceById,
|
||||
} from './resource.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
|
||||
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
|
||||
|
|
|
@ -9,6 +9,7 @@ import { expectSqlAssert } from '#src/utils/test-utils.js';
|
|||
|
||||
import { findAllRoles, findRolesByRoleNames } from './roles.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
|
||||
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
|
||||
|
|
|
@ -9,6 +9,7 @@ import { expectSqlAssert } from '#src/utils/test-utils.js';
|
|||
|
||||
import { defaultSettingId, getSetting, updateSetting } from './setting.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
|
||||
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
updateDefaultSignInExperience,
|
||||
} from './sign-in-experience.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
|
||||
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
deleteUserIdentity,
|
||||
} from './user.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
|
||||
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { CreateUser, Role, User } from '@logto/schemas';
|
||||
import { userInfoSelectFields } from '@logto/schemas';
|
||||
import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
import pick from 'lodash.pick';
|
||||
|
||||
import {
|
||||
|
@ -8,19 +9,9 @@ import {
|
|||
mockUserListResponse,
|
||||
mockUserResponse,
|
||||
} from '#src/__mocks__/index.js';
|
||||
import { encryptUserPassword } from '#src/lib/user.js';
|
||||
import { findRolesByRoleNames } from '#src/queries/roles.js';
|
||||
import {
|
||||
hasUser,
|
||||
findUserById,
|
||||
updateUserById,
|
||||
deleteUserIdentity,
|
||||
deleteUserById,
|
||||
} from '#src/queries/user.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
import adminUserRoutes from './admin-user.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const filterUsersWithSearch = (users: User[], search: string) =>
|
||||
users.filter((user) =>
|
||||
[user.username, user.primaryEmail, user.primaryPhone, user.name].some((value) =>
|
||||
|
@ -28,45 +19,44 @@ const filterUsersWithSearch = (users: User[], search: string) =>
|
|||
)
|
||||
);
|
||||
|
||||
const mockFindDefaultSignInExperience = jest.fn(async () => ({
|
||||
signUp: {
|
||||
identifiers: [],
|
||||
password: false,
|
||||
verify: false,
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/sign-in-experience.js', () => ({
|
||||
findDefaultSignInExperience: jest.fn(async () => mockFindDefaultSignInExperience()),
|
||||
mockEsm('#src/queries/sign-in-experience.js', () => ({
|
||||
findDefaultSignInExperience: jest.fn(async () => ({
|
||||
signUp: {
|
||||
identifiers: [],
|
||||
password: false,
|
||||
verify: false,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
const mockHasUser = jest.fn(async () => false);
|
||||
const mockHasUserWithEmail = jest.fn(async () => false);
|
||||
const mockHasUserWithPhone = jest.fn(async () => false);
|
||||
jest.mock('#src/queries/user.js', () => ({
|
||||
countUsers: jest.fn(async (search) => ({
|
||||
count: search ? filterUsersWithSearch(mockUserList, search).length : mockUserList.length,
|
||||
})),
|
||||
findUsers: jest.fn(
|
||||
async (limit, offset, search): Promise<User[]> =>
|
||||
search ? filterUsersWithSearch(mockUserList, search) : mockUserList
|
||||
),
|
||||
findUserById: jest.fn(async (): Promise<User> => mockUser),
|
||||
hasUser: jest.fn(async () => mockHasUser()),
|
||||
hasUserWithEmail: jest.fn(async () => mockHasUserWithEmail()),
|
||||
hasUserWithPhone: jest.fn(async () => mockHasUserWithPhone()),
|
||||
updateUserById: jest.fn(
|
||||
async (_, data: Partial<CreateUser>): Promise<User> => ({
|
||||
...mockUser,
|
||||
...data,
|
||||
})
|
||||
),
|
||||
deleteUserById: jest.fn(),
|
||||
deleteUserIdentity: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('#src/lib/user.js', () => ({
|
||||
...jest.requireActual('#src/lib/user.js'),
|
||||
const { hasUser, findUserById, updateUserById, deleteUserIdentity, deleteUserById } =
|
||||
await mockEsmWithActual('#src/queries/user.js', () => ({
|
||||
countUsers: jest.fn(async (search) => ({
|
||||
count: search ? filterUsersWithSearch(mockUserList, search).length : mockUserList.length,
|
||||
})),
|
||||
findUsers: jest.fn(
|
||||
async (limit, offset, search): Promise<User[]> =>
|
||||
search ? filterUsersWithSearch(mockUserList, search) : mockUserList
|
||||
),
|
||||
findUserById: jest.fn(async (): Promise<User> => mockUser),
|
||||
hasUser: jest.fn(async () => mockHasUser()),
|
||||
hasUserWithEmail: jest.fn(async () => mockHasUserWithEmail()),
|
||||
hasUserWithPhone: jest.fn(async () => mockHasUserWithPhone()),
|
||||
updateUserById: jest.fn(
|
||||
async (_, data: Partial<CreateUser>): Promise<User> => ({
|
||||
...mockUser,
|
||||
...data,
|
||||
})
|
||||
),
|
||||
deleteUserById: jest.fn(),
|
||||
deleteUserIdentity: jest.fn(),
|
||||
}));
|
||||
|
||||
const { encryptUserPassword } = await mockEsmWithActual('#src/lib/user.js', () => ({
|
||||
generateUserId: jest.fn(() => 'fooId'),
|
||||
encryptUserPassword: jest.fn(() => ({
|
||||
passwordEncrypted: 'password',
|
||||
|
@ -80,18 +70,18 @@ jest.mock('#src/lib/user.js', () => ({
|
|||
),
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/roles.js', () => ({
|
||||
const { findRolesByRoleNames } = mockEsm('#src/queries/roles.js', () => ({
|
||||
findRolesByRoleNames: jest.fn(
|
||||
async (): Promise<Role[]> => [{ id: 'role_id', name: 'admin', description: 'none' }]
|
||||
),
|
||||
}));
|
||||
|
||||
const revokeInstanceByUserId = jest.fn();
|
||||
jest.mock('#src/queries/oidc-model-instance.js', () => ({
|
||||
revokeInstanceByUserId: async (modelName: string, userId: string) =>
|
||||
revokeInstanceByUserId(modelName, userId),
|
||||
const { revokeInstanceByUserId } = mockEsm('#src/queries/oidc-model-instance.js', () => ({
|
||||
revokeInstanceByUserId: jest.fn(),
|
||||
}));
|
||||
|
||||
const adminUserRoutes = await pickDefault(import('./admin-user.js'));
|
||||
|
||||
describe('adminUserRoutes', () => {
|
||||
const userRequest = createRequester({ authedRoutes: adminUserRoutes });
|
||||
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import type { Application, CreateApplication } from '@logto/schemas';
|
||||
import { ApplicationType } from '@logto/schemas';
|
||||
import { mockEsm, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockApplication } from '#src/__mocks__/index.js';
|
||||
import { findApplicationById } from '#src/queries/application.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
import applicationRoutes from './application.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('#src/queries/application.js', () => ({
|
||||
const { findApplicationById } = mockEsm('#src/queries/application.js', () => ({
|
||||
findTotalNumberOfApplications: jest.fn(async () => ({ count: 10 })),
|
||||
findAllApplications: jest.fn(async () => [mockApplication]),
|
||||
findApplicationById: jest.fn(async () => mockApplication),
|
||||
|
@ -30,23 +29,21 @@ jest.mock('#src/queries/application.js', () => ({
|
|||
),
|
||||
}));
|
||||
|
||||
jest.mock('@logto/shared', () => ({
|
||||
mockEsm('@logto/shared', () => ({
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
buildIdGenerator: jest.fn(() => () => 'randomId'),
|
||||
buildApplicationSecret: jest.fn(() => 'randomId'),
|
||||
}));
|
||||
|
||||
const { createRequester } = await import('#src/utils/test-utils.js');
|
||||
const applicationRoutes = await pickDefault(import('./application.js'));
|
||||
|
||||
const customClientMetadata = {
|
||||
corsAllowedOrigins: ['http://localhost:5000', 'http://localhost:5001', 'https://silverhand.com'],
|
||||
idTokenTtl: 999_999,
|
||||
refreshTokenTtl: 100_000_000,
|
||||
};
|
||||
|
||||
const customOidcClientMetadata = {
|
||||
redirectUris: [],
|
||||
postLogoutRedirectUris: [],
|
||||
};
|
||||
|
||||
describe('application route', () => {
|
||||
const applicationRequest = createRequester({ authedRoutes: applicationRoutes });
|
||||
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
import { mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import * as functions from '#src/middleware/koa-auth.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
import authnRoutes from './authn.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
const { verifyBearerTokenFromRequest } = await mockEsmWithActual(
|
||||
'#src/middleware/koa-auth.js',
|
||||
() => ({
|
||||
verifyBearerTokenFromRequest: jest.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
const request = createRequester({
|
||||
anonymousRoutes: await pickDefault(import('#src/routes/authn.js')),
|
||||
});
|
||||
|
||||
describe('authn route for Hasura', () => {
|
||||
const request = createRequester({ anonymousRoutes: authnRoutes });
|
||||
const mockUserId = 'foo';
|
||||
const mockExpectedRole = 'some_role';
|
||||
const mockUnauthorizedRole = 'V';
|
||||
|
@ -17,7 +28,7 @@ describe('authn route for Hasura', () => {
|
|||
|
||||
describe('with successful verification', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(functions, 'verifyBearerTokenFromRequest').mockResolvedValue({
|
||||
verifyBearerTokenFromRequest.mockResolvedValue({
|
||||
clientId: 'ok',
|
||||
sub: mockUserId,
|
||||
roleNames: [mockExpectedRole],
|
||||
|
@ -59,15 +70,13 @@ describe('authn route for Hasura', () => {
|
|||
|
||||
describe('with failed verification', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(functions, 'verifyBearerTokenFromRequest')
|
||||
.mockImplementation(async (_, resource) => {
|
||||
if (resource) {
|
||||
throw new RequestError({ code: 'auth.jwt_sub_missing', status: 401 });
|
||||
}
|
||||
verifyBearerTokenFromRequest.mockImplementation(async (_, resource) => {
|
||||
if (resource) {
|
||||
throw new RequestError({ code: 'auth.jwt_sub_missing', status: 401 });
|
||||
}
|
||||
|
||||
return { clientId: 'not ok', sub: mockUserId };
|
||||
});
|
||||
return { clientId: 'not ok', sub: mockUserId };
|
||||
});
|
||||
});
|
||||
|
||||
it('throws 401 if no unauthorized role presents', async () => {
|
||||
|
@ -91,9 +100,10 @@ describe('authn route for Hasura', () => {
|
|||
});
|
||||
|
||||
it('falls back to unauthorized role if JWT is invalid', async () => {
|
||||
jest
|
||||
.spyOn(functions, 'verifyBearerTokenFromRequest')
|
||||
.mockRejectedValue(new RequestError({ code: 'auth.jwt_sub_missing', status: 401 }));
|
||||
verifyBearerTokenFromRequest.mockRejectedValue(
|
||||
new RequestError({ code: 'auth.jwt_sub_missing', status: 401 })
|
||||
);
|
||||
|
||||
const response = await request
|
||||
.get('/authn/hasura')
|
||||
.query({ resource: 'https://api.logto.io', unauthorizedRole: mockUnauthorizedRole });
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import type { EmailConnector, SmsConnector } from '@logto/connector-kit';
|
||||
import { ConnectorPlatform, MessageTypes } from '@logto/connector-kit';
|
||||
import { ConnectorType } from '@logto/schemas';
|
||||
import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
import { any } from 'zod';
|
||||
|
||||
import {
|
||||
|
@ -16,36 +17,30 @@ import {
|
|||
mockLogtoConnector,
|
||||
} from '#src/__mocks__/index.js';
|
||||
import { defaultConnectorMethods } from '#src/connectors/consts.js';
|
||||
import type { ConnectorFactory, LogtoConnector } from '#src/connectors/types.js';
|
||||
import type { LogtoConnector } from '#src/connectors/types.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { removeUnavailableSocialConnectorTargets } from '#src/lib/sign-in-experience/index.js';
|
||||
import {
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
mockEsm('#src/lib/connector.js', () => ({
|
||||
checkSocialConnectorTargetAndPlatformUniqueness: jest.fn(),
|
||||
}));
|
||||
|
||||
const { removeUnavailableSocialConnectorTargets } = mockEsm(
|
||||
'#src/lib/sign-in-experience/index.js',
|
||||
() => ({
|
||||
removeUnavailableSocialConnectorTargets: jest.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
findConnectorById,
|
||||
countConnectorByConnectorId,
|
||||
deleteConnectorById,
|
||||
deleteConnectorByIds,
|
||||
} from '#src/queries/connector.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
import connectorRoutes from './connector.js';
|
||||
|
||||
const loadConnectorFactoriesPlaceHolder = jest.fn() as jest.MockedFunction<
|
||||
() => Promise<ConnectorFactory[]>
|
||||
>;
|
||||
const getLogtoConnectorsPlaceHolder = jest.fn() as jest.MockedFunction<
|
||||
() => Promise<LogtoConnector[]>
|
||||
>;
|
||||
|
||||
jest.mock('#src/lib/connector.js', () => ({
|
||||
checkSocialConnectorTargetAndPlatformUniqueness: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('#src/lib/sign-in-experience/index.js', () => ({
|
||||
removeUnavailableSocialConnectorTargets: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/connector.js', () => ({
|
||||
} = await mockEsmWithActual('#src/queries/connector.js', () => ({
|
||||
findConnectorById: jest.fn(),
|
||||
countConnectorByConnectorId: jest.fn(),
|
||||
deleteConnectorById: jest.fn(),
|
||||
|
@ -53,11 +48,13 @@ jest.mock('#src/queries/connector.js', () => ({
|
|||
insertConnector: jest.fn(async (body: unknown) => body),
|
||||
}));
|
||||
|
||||
jest.mock('#src/connectors/index.js', () => ({
|
||||
loadConnectorFactories: async () => loadConnectorFactoriesPlaceHolder(),
|
||||
getLogtoConnectors: async () => getLogtoConnectorsPlaceHolder(),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
const getLogtoConnectors = jest.fn<Promise<LogtoConnector[]>, []>();
|
||||
const { loadConnectorFactories } = mockEsm('#src/connectors/index.js', () => ({
|
||||
loadConnectorFactories: jest.fn(),
|
||||
getLogtoConnectors,
|
||||
getLogtoConnectorById: async (connectorId: string) => {
|
||||
const connectors = await getLogtoConnectorsPlaceHolder();
|
||||
const connectors = await getLogtoConnectors();
|
||||
const connector = connectors.find(({ dbEntry }) => dbEntry.id === connectorId);
|
||||
assertThat(
|
||||
connector,
|
||||
|
@ -71,6 +68,7 @@ jest.mock('#src/connectors/index.js', () => ({
|
|||
return connector;
|
||||
},
|
||||
}));
|
||||
const connectorRoutes = await pickDefault(import('./connector.js'));
|
||||
|
||||
describe('connector route', () => {
|
||||
const connectorRequest = createRequester({ authedRoutes: connectorRoutes });
|
||||
|
@ -81,13 +79,13 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('throws if more than one email connector exists', async () => {
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce(mockLogtoConnectorList);
|
||||
getLogtoConnectors.mockResolvedValueOnce(mockLogtoConnectorList);
|
||||
const response = await connectorRequest.get('/connectors').send({});
|
||||
expect(response).toHaveProperty('statusCode', 400);
|
||||
});
|
||||
|
||||
it('throws if more than one SMS connector exists', async () => {
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce(
|
||||
getLogtoConnectors.mockResolvedValueOnce(
|
||||
mockLogtoConnectorList.filter((connector) => connector.type !== ConnectorType.Email)
|
||||
);
|
||||
const response = await connectorRequest.get('/connectors').send({});
|
||||
|
@ -95,7 +93,7 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('shows all connectors', async () => {
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce(
|
||||
getLogtoConnectors.mockResolvedValueOnce(
|
||||
mockLogtoConnectorList.filter((connector) => connector.type === ConnectorType.Social)
|
||||
);
|
||||
const response = await connectorRequest.get('/connectors').send({});
|
||||
|
@ -105,7 +103,7 @@ describe('connector route', () => {
|
|||
|
||||
describe('GET /connector-factories', () => {
|
||||
it('show all connector factories', async () => {
|
||||
(loadConnectorFactoriesPlaceHolder as jest.Mock).mockResolvedValueOnce([
|
||||
loadConnectorFactories.mockResolvedValueOnce([
|
||||
{ ...mockConnectorFactory, metadata: mockMetadata0, type: ConnectorType.Sms },
|
||||
{ ...mockConnectorFactory, metadata: mockMetadata1, type: ConnectorType.Social },
|
||||
{ ...mockConnectorFactory, metadata: mockMetadata2, type: ConnectorType.Email },
|
||||
|
@ -128,39 +126,38 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('throws when connector can not be found by given connectorId (locally)', async () => {
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce(mockLogtoConnectorList.slice(2));
|
||||
getLogtoConnectors.mockResolvedValueOnce(mockLogtoConnectorList.slice(2));
|
||||
const response = await connectorRequest.get('/connectors/findConnector').send({});
|
||||
expect(response).toHaveProperty('statusCode', 404);
|
||||
});
|
||||
|
||||
it('throws when connector can not be found by given connectorId (remotely)', async () => {
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([]);
|
||||
getLogtoConnectors.mockResolvedValueOnce([]);
|
||||
const response = await connectorRequest.get('/connectors/id0').send({});
|
||||
expect(response).toHaveProperty('statusCode', 404);
|
||||
});
|
||||
|
||||
it('shows found connector information', async () => {
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce(mockLogtoConnectorList);
|
||||
getLogtoConnectors.mockResolvedValueOnce(mockLogtoConnectorList);
|
||||
const response = await connectorRequest.get('/connectors/id0').send({});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /connectors', () => {
|
||||
const mockedCountConnectorByConnectorId = countConnectorByConnectorId as jest.Mock;
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should post a new connector record', async () => {
|
||||
loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
|
||||
loadConnectorFactories.mockResolvedValueOnce([
|
||||
{
|
||||
...mockConnectorFactory,
|
||||
metadata: { ...mockConnectorFactory.metadata, id: 'connectorId' },
|
||||
},
|
||||
]);
|
||||
mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([
|
||||
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
||||
getLogtoConnectors.mockResolvedValueOnce([
|
||||
{
|
||||
dbEntry: { ...mockConnector, connectorId: 'id0' },
|
||||
metadata: { ...mockMetadata, id: 'id0' },
|
||||
|
@ -185,13 +182,13 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('throws when connector factory not found', async () => {
|
||||
loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
|
||||
loadConnectorFactories.mockResolvedValueOnce([
|
||||
{
|
||||
...mockConnectorFactory,
|
||||
metadata: { ...mockConnectorFactory.metadata, id: 'connectorId' },
|
||||
},
|
||||
]);
|
||||
mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
||||
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
||||
const response = await connectorRequest.post('/connectors').send({
|
||||
connectorId: 'id0',
|
||||
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
|
||||
|
@ -200,7 +197,7 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('should post a new record when add more than 1 instance with connector factory', async () => {
|
||||
loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
|
||||
loadConnectorFactories.mockResolvedValueOnce([
|
||||
{
|
||||
...mockConnectorFactory,
|
||||
metadata: {
|
||||
|
@ -211,8 +208,8 @@ describe('connector route', () => {
|
|||
},
|
||||
},
|
||||
]);
|
||||
mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 1 });
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([
|
||||
countConnectorByConnectorId.mockResolvedValueOnce({ count: 1 });
|
||||
getLogtoConnectors.mockResolvedValueOnce([
|
||||
{
|
||||
dbEntry: { ...mockConnector, connectorId: 'id0' },
|
||||
metadata: { ...mockMetadata, id: 'id0', platform: ConnectorPlatform.Universal },
|
||||
|
@ -239,13 +236,13 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('throws when add more than 1 instance with non-connector factory', async () => {
|
||||
loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
|
||||
loadConnectorFactories.mockResolvedValueOnce([
|
||||
{
|
||||
...mockConnectorFactory,
|
||||
metadata: { ...mockConnectorFactory.metadata, id: 'id0' },
|
||||
},
|
||||
]);
|
||||
mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 1 });
|
||||
countConnectorByConnectorId.mockResolvedValueOnce({ count: 1 });
|
||||
const response = await connectorRequest.post('/connectors').send({
|
||||
connectorId: 'id0',
|
||||
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
|
||||
|
@ -254,14 +251,14 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('should add a new record and delete old records with same connector type when add passwordless connectors', async () => {
|
||||
loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
|
||||
loadConnectorFactories.mockResolvedValueOnce([
|
||||
{
|
||||
...mockConnectorFactory,
|
||||
type: ConnectorType.Sms,
|
||||
metadata: { ...mockConnectorFactory.metadata, id: 'id0', isStandard: true },
|
||||
},
|
||||
]);
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([
|
||||
getLogtoConnectors.mockResolvedValueOnce([
|
||||
{
|
||||
dbEntry: { ...mockConnector, connectorId: 'id0' },
|
||||
metadata: { ...mockMetadata, id: 'id0' },
|
||||
|
@ -289,7 +286,7 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('throws when add more than 1 social connector instance with same target and platform (add from standard connector)', async () => {
|
||||
loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
|
||||
loadConnectorFactories.mockResolvedValueOnce([
|
||||
{
|
||||
...mockConnectorFactory,
|
||||
metadata: {
|
||||
|
@ -300,8 +297,8 @@ describe('connector route', () => {
|
|||
},
|
||||
},
|
||||
]);
|
||||
mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([
|
||||
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
||||
getLogtoConnectors.mockResolvedValueOnce([
|
||||
{
|
||||
dbEntry: { ...mockConnector, connectorId: 'id0', metadata: { target: 'target' } },
|
||||
metadata: {
|
||||
|
@ -322,7 +319,7 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('throws when add more than 1 social connector instance with same target and platform (add social connector)', async () => {
|
||||
loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
|
||||
loadConnectorFactories.mockResolvedValueOnce([
|
||||
{
|
||||
...mockConnectorFactory,
|
||||
metadata: {
|
||||
|
@ -334,8 +331,8 @@ describe('connector route', () => {
|
|||
},
|
||||
},
|
||||
]);
|
||||
mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([
|
||||
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
||||
getLogtoConnectors.mockResolvedValueOnce([
|
||||
{
|
||||
dbEntry: { ...mockConnector, connectorId: 'id0', metadata: { target: 'target' } },
|
||||
metadata: {
|
||||
|
@ -373,7 +370,7 @@ describe('connector route', () => {
|
|||
...defaultConnectorMethods,
|
||||
sendMessage,
|
||||
};
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([mockedSmsConnector]);
|
||||
getLogtoConnectors.mockResolvedValueOnce([mockedSmsConnector]);
|
||||
const response = await connectorRequest
|
||||
.post('/connectors/id/test')
|
||||
.send({ phone: '12345678901', config: { test: 123 } });
|
||||
|
@ -401,7 +398,7 @@ describe('connector route', () => {
|
|||
...defaultConnectorMethods,
|
||||
sendMessage,
|
||||
};
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([mockedEmailConnector]);
|
||||
getLogtoConnectors.mockResolvedValueOnce([mockedEmailConnector]);
|
||||
const response = await connectorRequest
|
||||
.post('/connectors/id/test')
|
||||
.send({ email: 'test@email.com', config: { test: 123 } });
|
||||
|
@ -425,7 +422,7 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('should throw when sms connector is not found', async () => {
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([]);
|
||||
getLogtoConnectors.mockResolvedValueOnce([]);
|
||||
const response = await connectorRequest
|
||||
.post('/connectors/id/test')
|
||||
.send({ phone: '12345678901' });
|
||||
|
@ -433,7 +430,7 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('should throw when email connector is not found', async () => {
|
||||
getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([]);
|
||||
getLogtoConnectors.mockResolvedValueOnce([]);
|
||||
const response = await connectorRequest
|
||||
.post('/connectors/id/test')
|
||||
.send({ email: 'test@email.com' });
|
||||
|
@ -445,21 +442,22 @@ describe('connector route', () => {
|
|||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('delete connector instance and remove unavailable social connector targets', async () => {
|
||||
(findConnectorById as jest.Mock).mockResolvedValueOnce(mockConnector);
|
||||
loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([mockConnectorFactory]);
|
||||
findConnectorById.mockResolvedValueOnce(mockConnector);
|
||||
loadConnectorFactories.mockResolvedValueOnce([mockConnectorFactory]);
|
||||
await connectorRequest.delete('/connectors/id').send({});
|
||||
expect(deleteConnectorById).toHaveBeenCalledTimes(1);
|
||||
expect(removeUnavailableSocialConnectorTargets).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('delete connector instance (connector factory is not social type)', async () => {
|
||||
(findConnectorById as jest.Mock).mockResolvedValueOnce(mockConnector);
|
||||
loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
|
||||
findConnectorById.mockResolvedValueOnce(mockConnector);
|
||||
loadConnectorFactories.mockResolvedValueOnce([
|
||||
{ ...mockConnectorFactory, type: ConnectorType.Sms },
|
||||
]);
|
||||
await connectorRequest.delete('/connectors/id').send({});
|
||||
|
@ -468,8 +466,8 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('delete connector instance (connector factory is not found)', async () => {
|
||||
(findConnectorById as jest.Mock).mockResolvedValueOnce(mockConnector);
|
||||
loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([]);
|
||||
findConnectorById.mockResolvedValueOnce(mockConnector);
|
||||
loadConnectorFactories.mockResolvedValueOnce([]);
|
||||
await connectorRequest.delete('/connectors/id').send({});
|
||||
expect(deleteConnectorById).toHaveBeenCalledTimes(1);
|
||||
expect(removeUnavailableSocialConnectorTargets).toHaveBeenCalledTimes(0);
|
||||
|
@ -477,7 +475,7 @@ describe('connector route', () => {
|
|||
|
||||
it('throws when connector not exists with `id`', async () => {
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
(findConnectorById as jest.Mock).mockResolvedValueOnce(undefined);
|
||||
findConnectorById.mockResolvedValueOnce(undefined);
|
||||
const response = await connectorRequest.delete('/connectors/id').send({});
|
||||
expect(response).toHaveProperty('statusCode', 500);
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
|
||||
import { ConnectorType } from '@logto/schemas';
|
||||
import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import {
|
||||
mockMetadata,
|
||||
|
@ -9,17 +10,14 @@ import {
|
|||
} from '#src/__mocks__/index.js';
|
||||
import type { LogtoConnector } from '#src/connectors/types.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { updateConnector } from '#src/queries/connector.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
import connectorRoutes from './connector.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
const getLogtoConnectorsPlaceholder = jest.fn() as jest.MockedFunction<
|
||||
() => Promise<LogtoConnector[]>
|
||||
>;
|
||||
const getLogtoConnectorByIdPlaceholder = jest.fn(async (connectorId: string) => {
|
||||
const connectors = await getLogtoConnectorsPlaceholder();
|
||||
const getLogtoConnectors = jest.fn() as jest.MockedFunction<() => Promise<LogtoConnector[]>>;
|
||||
const getLogtoConnectorById = jest.fn(async (connectorId: string) => {
|
||||
const connectors = await getLogtoConnectors();
|
||||
const connector = connectors.find(({ dbEntry }) => dbEntry.id === connectorId);
|
||||
|
||||
assertThat(
|
||||
|
@ -36,22 +34,25 @@ const getLogtoConnectorByIdPlaceholder = jest.fn(async (connectorId: string) =>
|
|||
sendMessage: sendMessagePlaceHolder,
|
||||
};
|
||||
}) as jest.MockedFunction<(connectorId: string) => Promise<LogtoConnector>>;
|
||||
const mockedUpdateConnector = updateConnector as jest.Mock;
|
||||
|
||||
const sendMessagePlaceHolder = jest.fn();
|
||||
|
||||
jest.mock('#src/queries/connector.js', () => ({
|
||||
const { updateConnector } = await mockEsmWithActual('#src/queries/connector.js', () => ({
|
||||
updateConnector: jest.fn(),
|
||||
}));
|
||||
jest.mock('#src/connectors.js', () => ({
|
||||
getLogtoConnectors: async () => getLogtoConnectorsPlaceholder(),
|
||||
getLogtoConnectorById: async (connectorId: string) =>
|
||||
getLogtoConnectorByIdPlaceholder(connectorId),
|
||||
|
||||
await mockEsmWithActual('#src/connectors.js', () => ({
|
||||
getLogtoConnectors,
|
||||
getLogtoConnectorById,
|
||||
}));
|
||||
jest.mock('#src/lib/sign-in-experience.js', () => ({
|
||||
|
||||
mockEsm('#src/lib/sign-in-experience.js', () => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
removeUnavailableSocialConnectorTargets: async () => {},
|
||||
}));
|
||||
|
||||
const connectorRoutes = await pickDefault(import('./connector.js'));
|
||||
|
||||
describe('connector PATCH routes', () => {
|
||||
const connectorRequest = createRequester({ authedRoutes: connectorRoutes });
|
||||
|
||||
|
@ -61,19 +62,19 @@ describe('connector PATCH routes', () => {
|
|||
});
|
||||
|
||||
it('throws when connector can not be found by given connectorId (locally)', async () => {
|
||||
getLogtoConnectorsPlaceholder.mockResolvedValueOnce(mockLogtoConnectorList.slice(0, 1));
|
||||
getLogtoConnectors.mockResolvedValueOnce(mockLogtoConnectorList.slice(0, 1));
|
||||
const response = await connectorRequest.patch('/connectors/findConnector').send({});
|
||||
expect(response).toHaveProperty('statusCode', 404);
|
||||
});
|
||||
|
||||
it('throws when connector can not be found by given connectorId (remotely)', async () => {
|
||||
getLogtoConnectorsPlaceholder.mockResolvedValueOnce([]);
|
||||
getLogtoConnectors.mockResolvedValueOnce([]);
|
||||
const response = await connectorRequest.patch('/connectors/id0').send({});
|
||||
expect(response).toHaveProperty('statusCode', 404);
|
||||
});
|
||||
|
||||
it('config validation fails', async () => {
|
||||
getLogtoConnectorsPlaceholder.mockResolvedValueOnce([
|
||||
getLogtoConnectors.mockResolvedValueOnce([
|
||||
{
|
||||
dbEntry: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
|
@ -91,7 +92,7 @@ describe('connector PATCH routes', () => {
|
|||
});
|
||||
|
||||
it('throws when trying to update target', async () => {
|
||||
getLogtoConnectorsPlaceholder.mockResolvedValue([
|
||||
getLogtoConnectors.mockResolvedValue([
|
||||
{
|
||||
dbEntry: mockConnector,
|
||||
metadata: { ...mockMetadata, isStandard: true },
|
||||
|
@ -108,7 +109,7 @@ describe('connector PATCH routes', () => {
|
|||
});
|
||||
|
||||
it('successfully updates connector configs', async () => {
|
||||
getLogtoConnectorsPlaceholder.mockResolvedValue([
|
||||
getLogtoConnectors.mockResolvedValue([
|
||||
{
|
||||
dbEntry: mockConnector,
|
||||
metadata: { ...mockMetadata, isStandard: true },
|
||||
|
@ -116,7 +117,7 @@ describe('connector PATCH routes', () => {
|
|||
...mockLogtoConnector,
|
||||
},
|
||||
]);
|
||||
mockedUpdateConnector.mockResolvedValueOnce({
|
||||
updateConnector.mockResolvedValueOnce({
|
||||
...mockConnector,
|
||||
metadata: {
|
||||
target: 'target',
|
||||
|
@ -149,7 +150,7 @@ describe('connector PATCH routes', () => {
|
|||
});
|
||||
|
||||
it('successfully clear connector config metadata', async () => {
|
||||
getLogtoConnectorsPlaceholder.mockResolvedValueOnce([
|
||||
getLogtoConnectors.mockResolvedValueOnce([
|
||||
{
|
||||
dbEntry: mockConnector,
|
||||
metadata: { ...mockMetadata, isStandard: true },
|
||||
|
@ -157,7 +158,7 @@ describe('connector PATCH routes', () => {
|
|||
...mockLogtoConnector,
|
||||
},
|
||||
]);
|
||||
mockedUpdateConnector.mockResolvedValueOnce({
|
||||
updateConnector.mockResolvedValueOnce({
|
||||
...mockConnector,
|
||||
metadata: {
|
||||
target: '',
|
||||
|
@ -186,7 +187,7 @@ describe('connector PATCH routes', () => {
|
|||
});
|
||||
|
||||
it('throws when set syncProfile to `true` and with non-social connector', async () => {
|
||||
getLogtoConnectorsPlaceholder.mockResolvedValueOnce([
|
||||
getLogtoConnectors.mockResolvedValueOnce([
|
||||
{
|
||||
dbEntry: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
|
@ -200,7 +201,7 @@ describe('connector PATCH routes', () => {
|
|||
});
|
||||
|
||||
it('successfully set syncProfile to `true` and with social connector', async () => {
|
||||
getLogtoConnectorsPlaceholder.mockResolvedValue([
|
||||
getLogtoConnectors.mockResolvedValue([
|
||||
{
|
||||
dbEntry: { ...mockConnector, syncProfile: false },
|
||||
metadata: mockMetadata,
|
||||
|
@ -220,7 +221,7 @@ describe('connector PATCH routes', () => {
|
|||
});
|
||||
|
||||
it('successfully set syncProfile to `false`', async () => {
|
||||
getLogtoConnectorsPlaceholder.mockResolvedValue([
|
||||
getLogtoConnectors.mockResolvedValue([
|
||||
{
|
||||
dbEntry: { ...mockConnector, syncProfile: false },
|
||||
metadata: mockMetadata,
|
||||
|
|
|
@ -1,73 +1,66 @@
|
|||
import en from '@logto/phrases-ui/lib/locales/en.js';
|
||||
import type { CustomPhrase, SignInExperience, Translation } from '@logto/schemas';
|
||||
import type { CustomPhrase, SignInExperience } from '@logto/schemas';
|
||||
import { mockEsm, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockZhCnCustomPhrase, trTrTag, zhCnTag } from '#src/__mocks__/custom-phrase.js';
|
||||
import { mockSignInExperience } from '#src/__mocks__/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import customPhraseRoutes from '#src/routes/custom-phrase.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const mockLanguageTag = zhCnTag;
|
||||
const mockPhrase = mockZhCnCustomPhrase;
|
||||
const mockCustomPhrases: Record<string, CustomPhrase> = {
|
||||
[mockLanguageTag]: mockPhrase,
|
||||
};
|
||||
|
||||
const deleteCustomPhraseByLanguageTag = jest.fn(async (languageTag: string) => {
|
||||
if (!mockCustomPhrases[languageTag]) {
|
||||
throw new RequestError({ code: 'entity.not_found', status: 404 });
|
||||
}
|
||||
});
|
||||
const {
|
||||
deleteCustomPhraseByLanguageTag,
|
||||
findAllCustomPhrases,
|
||||
findCustomPhraseByLanguageTag,
|
||||
upsertCustomPhrase,
|
||||
} = mockEsm('#src/queries/custom-phrase.js', () => ({
|
||||
deleteCustomPhraseByLanguageTag: jest.fn(async (languageTag: string) => {
|
||||
if (!mockCustomPhrases[languageTag]) {
|
||||
throw new RequestError({ code: 'entity.not_found', status: 404 });
|
||||
}
|
||||
}),
|
||||
findAllCustomPhrases: jest.fn(async (): Promise<CustomPhrase[]> => []),
|
||||
findCustomPhraseByLanguageTag: jest.fn(async (languageTag: string) => {
|
||||
const mockCustomPhrase = mockCustomPhrases[languageTag];
|
||||
|
||||
const findCustomPhraseByLanguageTag = jest.fn(async (languageTag: string) => {
|
||||
const mockCustomPhrase = mockCustomPhrases[languageTag];
|
||||
if (!mockCustomPhrase) {
|
||||
throw new RequestError({ code: 'entity.not_found', status: 404 });
|
||||
}
|
||||
|
||||
if (!mockCustomPhrase) {
|
||||
throw new RequestError({ code: 'entity.not_found', status: 404 });
|
||||
}
|
||||
|
||||
return mockCustomPhrase;
|
||||
});
|
||||
|
||||
const findAllCustomPhrases = jest.fn(async (): Promise<CustomPhrase[]> => []);
|
||||
|
||||
const upsertCustomPhrase = jest.fn(async (customPhrase: CustomPhrase) => mockPhrase);
|
||||
|
||||
jest.mock('#src/queries/custom-phrase.js', () => ({
|
||||
deleteCustomPhraseByLanguageTag: async (tag: string) => deleteCustomPhraseByLanguageTag(tag),
|
||||
findAllCustomPhrases: async () => findAllCustomPhrases(),
|
||||
findCustomPhraseByLanguageTag: async (tag: string) => findCustomPhraseByLanguageTag(tag),
|
||||
upsertCustomPhrase: async (customPhrase: CustomPhrase) => upsertCustomPhrase(customPhrase),
|
||||
return mockCustomPhrase;
|
||||
}),
|
||||
upsertCustomPhrase: jest.fn(async () => mockPhrase),
|
||||
}));
|
||||
|
||||
const isStrictlyPartial = jest.fn(
|
||||
(fullTranslation: Translation, partialTranslation: Partial<Translation>) => true
|
||||
);
|
||||
|
||||
jest.mock('#src/utils/translation.js', () => ({
|
||||
isStrictlyPartial: (fullTranslation: Translation, partialTranslation: Translation) =>
|
||||
isStrictlyPartial(fullTranslation, partialTranslation),
|
||||
const { isStrictlyPartial } = mockEsm('#src/utils/translation.js', () => ({
|
||||
isStrictlyPartial: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
const mockFallbackLanguage = trTrTag;
|
||||
|
||||
const findDefaultSignInExperience = jest.fn(
|
||||
async (): Promise<SignInExperience> => ({
|
||||
...mockSignInExperience,
|
||||
languageInfo: {
|
||||
autoDetect: true,
|
||||
fallbackLanguage: mockFallbackLanguage,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock('#src/queries/sign-in-experience.js', () => ({
|
||||
findDefaultSignInExperience: async () => findDefaultSignInExperience(),
|
||||
mockEsm('#src/queries/sign-in-experience.js', () => ({
|
||||
findDefaultSignInExperience: jest.fn(
|
||||
async (): Promise<SignInExperience> => ({
|
||||
...mockSignInExperience,
|
||||
languageInfo: {
|
||||
autoDetect: true,
|
||||
fallbackLanguage: mockFallbackLanguage,
|
||||
},
|
||||
})
|
||||
),
|
||||
}));
|
||||
|
||||
describe('customPhraseRoutes', () => {
|
||||
const customPhraseRequest = createRequester({ authedRoutes: customPhraseRoutes });
|
||||
const customPhraseRoutes = await pickDefault(import('./custom-phrase.js'));
|
||||
const customPhraseRequest = createRequester({ authedRoutes: customPhraseRoutes });
|
||||
|
||||
describe('customPhraseRoutes', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
|
|
@ -1,25 +1,20 @@
|
|||
// The FP version works better for `format()`
|
||||
/* eslint-disable import/no-duplicates */
|
||||
import { mockEsm, pickDefault } from '@logto/shared/esm';
|
||||
import { endOfDay, subDays } from 'date-fns';
|
||||
import { format } from 'date-fns/fp';
|
||||
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
/* eslint-enable import/no-duplicates */
|
||||
|
||||
import dashboardRoutes from '#src/routes/dashboard.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
const totalUserCount = 1000;
|
||||
const countUsers = jest.fn(async () => ({ count: totalUserCount }));
|
||||
const getDailyNewUserCountsByTimeInterval = jest.fn(
|
||||
async (startTimeExclusive: number, endTimeInclusive: number) => mockDailyNewUserCounts
|
||||
);
|
||||
const formatToQueryDate = format('yyyy-MM-dd');
|
||||
|
||||
jest.mock('#src/queries/user.js', () => ({
|
||||
countUsers: async () => countUsers(),
|
||||
getDailyNewUserCountsByTimeInterval: async (
|
||||
startTimeExclusive: number,
|
||||
endTimeInclusive: number
|
||||
) => getDailyNewUserCountsByTimeInterval(startTimeExclusive, endTimeInclusive),
|
||||
const { countUsers, getDailyNewUserCountsByTimeInterval } = mockEsm('#src/queries/user.js', () => ({
|
||||
countUsers: jest.fn(async () => ({ count: totalUserCount })),
|
||||
getDailyNewUserCountsByTimeInterval: jest.fn(async () => mockDailyNewUserCounts),
|
||||
}));
|
||||
|
||||
const mockDailyNewUserCounts = [
|
||||
|
@ -44,21 +39,14 @@ const mockDailyActiveUserCounts = [
|
|||
|
||||
const mockActiveUserCount = 1000;
|
||||
|
||||
const getDailyActiveUserCountsByTimeInterval = jest.fn(
|
||||
async (startTimeExclusive: number, endTimeInclusive: number) => mockDailyActiveUserCounts
|
||||
const { getDailyActiveUserCountsByTimeInterval, countActiveUsersByTimeInterval } = mockEsm(
|
||||
'#src/queries/log.js',
|
||||
() => ({
|
||||
getDailyActiveUserCountsByTimeInterval: jest.fn().mockResolvedValue(mockDailyActiveUserCounts),
|
||||
countActiveUsersByTimeInterval: jest.fn().mockResolvedValue({ count: mockActiveUserCount }),
|
||||
})
|
||||
);
|
||||
const countActiveUsersByTimeInterval = jest.fn(
|
||||
async (startTimeExclusive: number, endTimeInclusive: number) => ({ count: mockActiveUserCount })
|
||||
);
|
||||
|
||||
jest.mock('#src/queries/log.js', () => ({
|
||||
getDailyActiveUserCountsByTimeInterval: async (
|
||||
startTimeExclusive: number,
|
||||
endTimeInclusive: number
|
||||
) => getDailyActiveUserCountsByTimeInterval(startTimeExclusive, endTimeInclusive),
|
||||
countActiveUsersByTimeInterval: async (startTimeExclusive: number, endTimeInclusive: number) =>
|
||||
countActiveUsersByTimeInterval(startTimeExclusive, endTimeInclusive),
|
||||
}));
|
||||
const dashboardRoutes = await pickDefault(import('./dashboard.js'));
|
||||
|
||||
describe('dashboardRoutes', () => {
|
||||
const logRequest = createRequester({ authedRoutes: dashboardRoutes });
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { Event } from '@logto/schemas';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { mockEsm, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { getLogtoConnectorById } from '#src/connectors/index.js';
|
||||
import { assignInteractionResults } from '#src/lib/session.js';
|
||||
import { encryptUserPassword, generateUserId, insertUser } from '#src/lib/user.js';
|
||||
import { updateUserById } from '#src/queries/user.js';
|
||||
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import type {
|
||||
|
@ -14,19 +11,20 @@ import type {
|
|||
VerifiedSignInInteractionResult,
|
||||
VerifiedForgotPasswordInteractionResult,
|
||||
} from '../types/index.js';
|
||||
import submitInteraction from './submit-interaction.js';
|
||||
|
||||
jest.mock('#src/connectors/index.js', () => ({
|
||||
const { jest } = import.meta;
|
||||
|
||||
const { getLogtoConnectorById } = mockEsm('#src/connectors/index.js', () => ({
|
||||
getLogtoConnectorById: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ metadata: { target: 'logto' }, dbEntry: { syncProfile: true } }),
|
||||
}));
|
||||
|
||||
jest.mock('#src/lib/session.js', () => ({
|
||||
const { assignInteractionResults } = mockEsm('#src/lib/session.js', () => ({
|
||||
assignInteractionResults: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('#src/lib/user.js', () => ({
|
||||
const { encryptUserPassword, generateUserId, insertUser } = mockEsm('#src/lib/user.js', () => ({
|
||||
encryptUserPassword: jest.fn().mockResolvedValue({
|
||||
passwordEncrypted: 'passwordEncrypted',
|
||||
passwordEncryptionMethod: 'plain',
|
||||
|
@ -35,25 +33,21 @@ jest.mock('#src/lib/user.js', () => ({
|
|||
insertUser: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/user.js', () => ({
|
||||
mockEsm('#src/queries/user.js', () => ({
|
||||
findUserById: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ identities: { google: { userId: 'googleId', details: {} } } }),
|
||||
updateUserById: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })),
|
||||
})),
|
||||
}));
|
||||
|
||||
const { updateUserById } = await import('#src/queries/user.js');
|
||||
const submitInteraction = await pickDefault(import('./submit-interaction.js'));
|
||||
const now = Date.now();
|
||||
|
||||
jest.useFakeTimers().setSystemTime(now);
|
||||
|
||||
describe('submit action', () => {
|
||||
const provider = new Provider('');
|
||||
const provider = createMockProvider();
|
||||
const log = jest.fn();
|
||||
const ctx: InteractionContext = {
|
||||
...createContextWithRouteParameters(),
|
||||
|
@ -116,7 +110,7 @@ describe('submit action', () => {
|
|||
});
|
||||
|
||||
it('sign-in', async () => {
|
||||
(getLogtoConnectorById as jest.Mock).mockResolvedValueOnce({
|
||||
getLogtoConnectorById.mockResolvedValueOnce({
|
||||
metadata: { target: 'logto' },
|
||||
dbEntry: { syncProfile: false },
|
||||
});
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
import { ConnectorType } from '@logto/connector-kit';
|
||||
import { Event } from '@logto/schemas';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { demoAppApplicationId } from '@logto/schemas/lib/seeds/application.js';
|
||||
import { mockEsm, mockEsmDefault, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
import submitInteraction from './actions/submit-interaction.js';
|
||||
import interactionRoutes, { verificationPrefix, interactionPrefix } from './index.js';
|
||||
import type { InteractionContext } from './types/index.js';
|
||||
import { getInteractionStorage } from './utils/interaction.js';
|
||||
import { sendPasscodeToIdentifier } from './utils/passcode-validation.js';
|
||||
import {
|
||||
verifyIdentifier,
|
||||
verifyProfile,
|
||||
validateMandatoryUserProfile,
|
||||
} from './verifications/index.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
// FIXME @Darcy: no more `enabled` for `connectors` table
|
||||
const getLogtoConnectorByIdHelper = jest.fn(async (connectorId: string) => {
|
||||
|
@ -36,7 +31,7 @@ const getLogtoConnectorByIdHelper = jest.fn(async (connectorId: string) => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('#src/connectors.js', () => ({
|
||||
await mockEsmWithActual('#src/connectors/index.js', () => ({
|
||||
getLogtoConnectorById: jest.fn(async (connectorId: string) => {
|
||||
const connector = await getLogtoConnectorByIdHelper(connectorId);
|
||||
|
||||
|
@ -51,57 +46,67 @@ jest.mock('#src/connectors.js', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
jest.mock('./utils/passcode-validation.js', () => ({
|
||||
sendPasscodeToIdentifier: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails: jest.fn().mockResolvedValue({
|
||||
jti: 'jti',
|
||||
result: {},
|
||||
params: {
|
||||
client_id: 'demo_app',
|
||||
},
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('#src/lib/sign-in-experience/index.js', () => ({
|
||||
getSignInExperienceForApplication: jest.fn().mockResolvedValue(mockSignInExperience),
|
||||
}));
|
||||
|
||||
jest.mock('./verifications/index.js', () => ({
|
||||
verifyIdentifier: jest.fn(),
|
||||
verifyProfile: jest.fn(),
|
||||
validateMandatoryUserProfile: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./actions/submit-interaction.js', () =>
|
||||
jest.fn((_interaction, ctx: InteractionContext) => {
|
||||
ctx.body = { redirectUri: 'logto.io' };
|
||||
const { sendPasscodeToIdentifier } = await mockEsmWithActual(
|
||||
'./utils/passcode-validation.js',
|
||||
() => ({
|
||||
sendPasscodeToIdentifier: jest.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock('./utils/interaction.js', () => ({
|
||||
mockEsm('#src/lib/sign-in-experience/index.js', () => ({
|
||||
getSignInExperienceForApplication: jest.fn().mockResolvedValue(mockSignInExperience),
|
||||
}));
|
||||
|
||||
const { verifyIdentifier, verifyProfile, validateMandatoryUserProfile } = mockEsm(
|
||||
'./verifications/index.js',
|
||||
() => ({
|
||||
verifyIdentifier: jest.fn(),
|
||||
verifyProfile: jest.fn(),
|
||||
validateMandatoryUserProfile: jest.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
const { default: submitInteraction } = mockEsm('./actions/submit-interaction.js', () => ({
|
||||
default: jest.fn((_interaction, ctx: InteractionContext) => {
|
||||
ctx.body = { redirectUri: 'logto.io' };
|
||||
}),
|
||||
}));
|
||||
|
||||
const { getInteractionStorage } = mockEsm('./utils/interaction.js', () => ({
|
||||
getInteractionStorage: jest.fn(),
|
||||
}));
|
||||
|
||||
const log = jest.fn();
|
||||
|
||||
const koaInteractionBodyGuardSpy = jest.spyOn(
|
||||
jest.requireActual('./middleware/koa-interaction-body-guard.js'),
|
||||
'default'
|
||||
const koaInteractionBodyGuard = await pickDefault(
|
||||
import('./middleware/koa-interaction-body-guard.js')
|
||||
);
|
||||
const koaSessionSignInExperienceGuardSpy = jest.spyOn(
|
||||
jest.requireActual('./middleware/koa-session-sign-in-experience-guard.js'),
|
||||
'default'
|
||||
const koaSessionSignInExperienceGuard = await pickDefault(
|
||||
import('./middleware/koa-session-sign-in-experience-guard.js')
|
||||
);
|
||||
|
||||
const koaInteractionBodyGuardSpy = mockEsmDefault(
|
||||
'./middleware/koa-interaction-body-guard.js',
|
||||
() => jest.fn(koaInteractionBodyGuard)
|
||||
);
|
||||
|
||||
const koaSessionSignInExperienceGuardSpy = mockEsmDefault(
|
||||
'./middleware/koa-session-sign-in-experience-guard.js',
|
||||
() => jest.fn(koaSessionSignInExperienceGuard)
|
||||
);
|
||||
|
||||
const {
|
||||
default: interactionRoutes,
|
||||
verificationPrefix,
|
||||
interactionPrefix,
|
||||
} = await import('./index.js');
|
||||
|
||||
describe('session -> interactionRoutes', () => {
|
||||
const sessionRequest = createRequester({
|
||||
anonymousRoutes: interactionRoutes,
|
||||
provider: new Provider(''),
|
||||
provider: createMockProvider(
|
||||
jest.fn().mockResolvedValue({ params: {}, jti: 'jti', client_id: demoAppApplicationId })
|
||||
),
|
||||
middlewares: [
|
||||
async (ctx, next) => {
|
||||
ctx.addLogContext = jest.fn();
|
||||
|
@ -162,14 +167,13 @@ describe('session -> interactionRoutes', () => {
|
|||
|
||||
describe('PATCH /interaction', () => {
|
||||
const path = interactionPrefix;
|
||||
const getInteractionStorageMock = getInteractionStorage as jest.Mock;
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('sign-in event with register event interaction session in record should call methods properly', async () => {
|
||||
getInteractionStorageMock.mockResolvedValueOnce({ event: Event.Register });
|
||||
getInteractionStorage.mockResolvedValueOnce({ event: Event.Register });
|
||||
|
||||
const body = {
|
||||
event: Event.SignIn,
|
||||
|
@ -185,7 +189,7 @@ describe('session -> interactionRoutes', () => {
|
|||
});
|
||||
|
||||
it('sign-in event with forgot password event interaction session in record should reject', async () => {
|
||||
getInteractionStorageMock.mockResolvedValueOnce({ event: Event.ForgotPassword });
|
||||
getInteractionStorage.mockResolvedValueOnce({ event: Event.ForgotPassword });
|
||||
|
||||
const body = {
|
||||
event: Event.SignIn,
|
||||
|
@ -200,7 +204,7 @@ describe('session -> interactionRoutes', () => {
|
|||
});
|
||||
|
||||
it('Forgot event with forgot password event interaction session in record should call methods properly', async () => {
|
||||
getInteractionStorageMock.mockResolvedValueOnce({ event: Event.ForgotPassword });
|
||||
getInteractionStorage.mockResolvedValueOnce({ event: Event.ForgotPassword });
|
||||
|
||||
const body = {
|
||||
event: Event.ForgotPassword,
|
||||
|
@ -216,7 +220,7 @@ describe('session -> interactionRoutes', () => {
|
|||
});
|
||||
|
||||
it('Forgot event with sign-in event interaction session in record should call methods properly', async () => {
|
||||
getInteractionStorageMock.mockResolvedValueOnce({ event: Event.SignIn });
|
||||
getInteractionStorage.mockResolvedValueOnce({ event: Event.SignIn });
|
||||
|
||||
const body = {
|
||||
event: Event.ForgotPassword,
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
import { Event } from '@logto/schemas';
|
||||
import { mockEsmDefault, pickDefault } from '@logto/shared/esm';
|
||||
import type { Context } from 'koa';
|
||||
|
||||
import { interactionMocks } from '#src/__mocks__/interactions.js';
|
||||
import { emptyMiddleware, createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import type { WithGuardedIdentifierPayloadContext } from './koa-interaction-body-guard.js';
|
||||
import koaInteractionBodyGuard from './koa-interaction-body-guard.js';
|
||||
|
||||
jest.mock('koa-body', () => emptyMiddleware);
|
||||
const { jest } = import.meta;
|
||||
|
||||
// User this to bypass the context type assertion
|
||||
const mockIdentifierPayload = Object.freeze({
|
||||
type: 'username_password',
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
});
|
||||
mockEsmDefault('koa-body', () => emptyMiddleware);
|
||||
|
||||
const koaInteractionBodyGuard = await pickDefault(import('./koa-interaction-body-guard.js'));
|
||||
|
||||
describe('koaInteractionBodyGuard', () => {
|
||||
const baseCtx = createContextWithRouteParameters();
|
||||
|
|
|
@ -1,31 +1,27 @@
|
|||
import { Event } from '@logto/schemas';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { mockEsm, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js';
|
||||
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import {
|
||||
signInModeValidation,
|
||||
identifierValidation,
|
||||
profileValidation,
|
||||
} from '../utils/sign-in-experience-validation.js';
|
||||
import koaSessionSignInExperienceGuard from './koa-session-sign-in-experience-guard.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('#src/lib/sign-in-experience/index.js', () => ({
|
||||
mockEsm('#src/lib/sign-in-experience/index.js', () => ({
|
||||
getSignInExperienceForApplication: jest.fn().mockResolvedValue(mockSignInExperience),
|
||||
}));
|
||||
|
||||
jest.mock('../utils/sign-in-experience-validation.js', () => ({
|
||||
const mockUtils = {
|
||||
signInModeValidation: jest.fn(),
|
||||
identifierValidation: jest.fn(),
|
||||
profileValidation: jest.fn(),
|
||||
}));
|
||||
};
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails: jest.fn(async () => ({ params: {} })),
|
||||
})),
|
||||
}));
|
||||
mockEsm('../utils/sign-in-experience-validation.js', () => mockUtils);
|
||||
|
||||
const koaSessionSignInExperienceGuard = await pickDefault(
|
||||
import('./koa-session-sign-in-experience-guard.js')
|
||||
);
|
||||
|
||||
describe('koaSessionSignInExperienceGuard', () => {
|
||||
const baseCtx = createContextWithRouteParameters();
|
||||
|
@ -41,14 +37,15 @@ describe('koaSessionSignInExperienceGuard', () => {
|
|||
}),
|
||||
signInExperience: mockSignInExperience,
|
||||
};
|
||||
const provider = createMockProvider();
|
||||
|
||||
await koaSessionSignInExperienceGuard(new Provider(''))(ctx, next);
|
||||
await koaSessionSignInExperienceGuard(provider)(ctx, next);
|
||||
|
||||
expect(signInModeValidation).toBeCalledWith(Event.SignIn, mockSignInExperience);
|
||||
expect(identifierValidation).toBeCalledWith(
|
||||
expect(mockUtils.signInModeValidation).toBeCalledWith(Event.SignIn, mockSignInExperience);
|
||||
expect(mockUtils.identifierValidation).toBeCalledWith(
|
||||
{ username: 'username', password: 'password' },
|
||||
mockSignInExperience
|
||||
);
|
||||
expect(profileValidation).toBeCalledWith({ email: 'email' }, mockSignInExperience);
|
||||
expect(mockUtils.profileValidation).toBeCalledWith({ email: 'email' }, mockSignInExperience);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,43 +1,41 @@
|
|||
import { getLogtoConnectorById } from '#src/connectors/index.js';
|
||||
import {
|
||||
findUserByEmail,
|
||||
findUserByUsername,
|
||||
findUserByPhone,
|
||||
findUserByIdentity,
|
||||
} from '#src/queries/user.js';
|
||||
import { mockEsm, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import findUserByIdentifier from './find-user-by-identifier.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('#src/queries/user.js', () => ({
|
||||
const queries = {
|
||||
findUserByEmail: jest.fn(),
|
||||
findUserByUsername: jest.fn(),
|
||||
findUserByPhone: jest.fn(),
|
||||
findUserByIdentity: jest.fn(),
|
||||
}));
|
||||
};
|
||||
|
||||
jest.mock('#src/connectors/index.js', () => ({
|
||||
mockEsm('#src/queries/user.js', () => queries);
|
||||
|
||||
const { getLogtoConnectorById } = mockEsm('#src/connectors/index.js', () => ({
|
||||
getLogtoConnectorById: jest.fn().mockResolvedValue({ metadata: { target: 'logto' } }),
|
||||
}));
|
||||
|
||||
const findUserByIdentifier = await pickDefault(import('./find-user-by-identifier.js'));
|
||||
|
||||
describe('findUserByIdentifier', () => {
|
||||
it('username', async () => {
|
||||
await findUserByIdentifier({ username: 'foo' });
|
||||
expect(findUserByUsername).toBeCalledWith('foo');
|
||||
expect(queries.findUserByUsername).toBeCalledWith('foo');
|
||||
});
|
||||
|
||||
it('email', async () => {
|
||||
await findUserByIdentifier({ email: 'foo@logto.io' });
|
||||
expect(findUserByEmail).toBeCalledWith('foo@logto.io');
|
||||
expect(queries.findUserByEmail).toBeCalledWith('foo@logto.io');
|
||||
});
|
||||
|
||||
it('phone', async () => {
|
||||
await findUserByIdentifier({ phone: '123456' });
|
||||
expect(findUserByPhone).toBeCalledWith('123456');
|
||||
expect(queries.findUserByPhone).toBeCalledWith('123456');
|
||||
});
|
||||
|
||||
it('social', async () => {
|
||||
await findUserByIdentifier({ connectorId: 'connector', userInfo: { id: 'foo' } });
|
||||
expect(getLogtoConnectorById).toBeCalledWith('connector');
|
||||
expect(findUserByIdentity).toBeCalledWith('logto', 'foo');
|
||||
expect(queries.findUserByIdentity).toBeCalledWith('logto', 'foo');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import { PasscodeType, Event } from '@logto/schemas';
|
||||
|
||||
import { createPasscode, sendPasscode } from '#src/lib/passcode.js';
|
||||
import { mockEsmWithActual } from '@logto/shared/esm';
|
||||
|
||||
import type { SendPasscodePayload } from '../types/index.js';
|
||||
import { sendPasscodeToIdentifier } from './passcode-validation.js';
|
||||
|
||||
jest.mock('#src/lib/passcode.js', () => ({
|
||||
const { jest } = import.meta;
|
||||
const passcode = {
|
||||
createPasscode: jest.fn(() => ({})),
|
||||
sendPasscode: jest.fn().mockResolvedValue({ dbEntry: { id: 'foo' } }),
|
||||
}));
|
||||
};
|
||||
|
||||
await mockEsmWithActual('#src/lib/passcode.js', () => passcode);
|
||||
|
||||
const { sendPasscodeToIdentifier } = await import('./passcode-validation.js');
|
||||
|
||||
const sendPasscodeTestCase = [
|
||||
{
|
||||
|
@ -38,8 +41,6 @@ const sendPasscodeTestCase = [
|
|||
];
|
||||
|
||||
describe('passcode-validation utils', () => {
|
||||
const createPasscodeMock = createPasscode as jest.Mock;
|
||||
const sendPasscodeMock = sendPasscode as jest.Mock;
|
||||
const log = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -50,8 +51,8 @@ describe('passcode-validation utils', () => {
|
|||
'send passcode successfully',
|
||||
async ({ payload, createPasscodeParams }) => {
|
||||
await sendPasscodeToIdentifier(payload as SendPasscodePayload, 'jti', log);
|
||||
expect(createPasscodeMock).toBeCalledWith('jti', ...createPasscodeParams);
|
||||
expect(sendPasscodeMock).toBeCalled();
|
||||
expect(passcode.createPasscode).toBeCalledWith('jti', ...createPasscodeParams);
|
||||
expect(passcode.sendPasscode).toBeCalled();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { ConnectorType } from '@logto/connector-kit';
|
||||
import { mockEsm } from '@logto/shared/esm';
|
||||
|
||||
import { getUserInfoByAuthCode } from '#src/lib/social.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
import { verifySocialIdentity } from './social-verification.js';
|
||||
|
||||
jest.mock('#src/lib/social.js', () => ({
|
||||
const { getUserInfoByAuthCode } = mockEsm('#src/lib/social.js', () => ({
|
||||
getUserInfoByAuthCode: jest.fn().mockResolvedValue({ id: 'foo' }),
|
||||
}));
|
||||
|
||||
jest.mock('#src/connectors.js', () => ({
|
||||
mockEsm('#src/connectors.js', () => ({
|
||||
getLogtoConnectorById: jest.fn().mockResolvedValue({
|
||||
metadata: {
|
||||
id: 'social',
|
||||
|
@ -18,6 +17,7 @@ jest.mock('#src/connectors.js', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
const { verifySocialIdentity } = await import('./social-verification.js');
|
||||
const log = jest.fn();
|
||||
|
||||
describe('social-verification', () => {
|
||||
|
|
|
@ -1,55 +1,47 @@
|
|||
import { Event } from '@logto/schemas';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { mockEsm, mockEsmDefault, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { verifyUserPassword } from '#src/lib/user.js';
|
||||
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import type { AnonymousInteractionResult, VerifiedPhoneIdentifier } from '../types/index.js';
|
||||
import findUserByIdentifier from '../utils/find-user-by-identifier.js';
|
||||
import { verifyIdentifierByPasscode } from '../utils/passcode-validation.js';
|
||||
import { verifySocialIdentity } from '../utils/social-verification.js';
|
||||
import identifierPayloadVerification from './identifier-payload-verification.js';
|
||||
|
||||
jest.mock('#src/lib/user.js', () => ({
|
||||
const { jest } = import.meta;
|
||||
|
||||
const { verifyUserPassword } = mockEsm('#src/lib/user.js', () => ({
|
||||
verifyUserPassword: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../utils/find-user-by-identifier.js', () => jest.fn());
|
||||
const findUserByIdentifier = mockEsmDefault('../utils/find-user-by-identifier.js', () => jest.fn());
|
||||
|
||||
jest.mock('../utils/interaction.js', () => ({
|
||||
...jest.requireActual('../utils/interaction.js'),
|
||||
await mockEsmWithActual('../utils/interaction.js', () => ({
|
||||
storeInteractionResult: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../utils/passcode-validation.js', () => ({
|
||||
const { verifyIdentifierByPasscode } = mockEsm('../utils/passcode-validation.js', () => ({
|
||||
verifyIdentifierByPasscode: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../utils/social-verification.js', () => ({
|
||||
const { verifySocialIdentity } = mockEsm('../utils/social-verification.js', () => ({
|
||||
verifySocialIdentity: jest.fn().mockResolvedValue({ id: 'foo' }),
|
||||
}));
|
||||
|
||||
const identifierPayloadVerification = await pickDefault(
|
||||
import('./identifier-payload-verification.js')
|
||||
);
|
||||
|
||||
const log = jest.fn();
|
||||
|
||||
describe('identifier verification', () => {
|
||||
const baseCtx = { ...createContextWithRouteParameters(), log };
|
||||
const verifyUserPasswordMock = verifyUserPassword as jest.Mock;
|
||||
const findUserByIdentifierMock = findUserByIdentifier as jest.Mock;
|
||||
const verifyIdentifierByPasscodeMock = verifyIdentifierByPasscode as jest.Mock;
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('username password user not found', async () => {
|
||||
findUserByIdentifierMock.mockResolvedValueOnce(null);
|
||||
findUserByIdentifier.mockResolvedValueOnce(null);
|
||||
|
||||
const identifier = {
|
||||
username: 'username',
|
||||
|
@ -64,14 +56,14 @@ describe('identifier verification', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
await expect(identifierPayloadVerification(ctx, new Provider(''))).rejects.toThrow();
|
||||
await expect(identifierPayloadVerification(ctx, createMockProvider())).rejects.toThrow();
|
||||
expect(findUserByIdentifier).toBeCalledWith({ username: 'username' });
|
||||
expect(verifyUserPassword).toBeCalledWith(null, 'password');
|
||||
});
|
||||
|
||||
it('username password user is suspended', async () => {
|
||||
findUserByIdentifierMock.mockResolvedValueOnce({ id: 'foo' });
|
||||
verifyUserPasswordMock.mockResolvedValueOnce({ id: 'foo', isSuspended: true });
|
||||
findUserByIdentifier.mockResolvedValueOnce({ id: 'foo' });
|
||||
verifyUserPassword.mockResolvedValueOnce({ id: 'foo', isSuspended: true });
|
||||
const identifier = {
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
|
@ -85,7 +77,7 @@ describe('identifier verification', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
await expect(identifierPayloadVerification(ctx, new Provider(''))).rejects.toMatchError(
|
||||
await expect(identifierPayloadVerification(ctx, createMockProvider())).rejects.toMatchError(
|
||||
new RequestError({ code: 'user.suspended', status: 401 })
|
||||
);
|
||||
|
||||
|
@ -94,8 +86,8 @@ describe('identifier verification', () => {
|
|||
});
|
||||
|
||||
it('email password', async () => {
|
||||
findUserByIdentifierMock.mockResolvedValueOnce({ id: 'foo' });
|
||||
verifyUserPasswordMock.mockResolvedValueOnce({ id: 'foo', isSuspended: false });
|
||||
findUserByIdentifier.mockResolvedValueOnce({ id: 'foo' });
|
||||
verifyUserPassword.mockResolvedValueOnce({ id: 'foo', isSuspended: false });
|
||||
|
||||
const identifier = {
|
||||
email: 'email',
|
||||
|
@ -110,7 +102,7 @@ describe('identifier verification', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
const result = await identifierPayloadVerification(ctx, new Provider(''));
|
||||
const result = await identifierPayloadVerification(ctx, createMockProvider());
|
||||
expect(findUserByIdentifier).toBeCalledWith({ email: 'email' });
|
||||
expect(verifyUserPassword).toBeCalledWith({ id: 'foo' }, 'password');
|
||||
expect(result).toEqual({
|
||||
|
@ -120,8 +112,8 @@ describe('identifier verification', () => {
|
|||
});
|
||||
|
||||
it('phone password', async () => {
|
||||
findUserByIdentifierMock.mockResolvedValueOnce({ id: 'foo' });
|
||||
verifyUserPasswordMock.mockResolvedValueOnce({ id: 'foo', isSuspended: false });
|
||||
findUserByIdentifier.mockResolvedValueOnce({ id: 'foo' });
|
||||
verifyUserPassword.mockResolvedValueOnce({ id: 'foo', isSuspended: false });
|
||||
|
||||
const identifier = {
|
||||
phone: 'phone',
|
||||
|
@ -136,7 +128,7 @@ describe('identifier verification', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
const result = await identifierPayloadVerification(ctx, new Provider(''));
|
||||
const result = await identifierPayloadVerification(ctx, createMockProvider());
|
||||
expect(findUserByIdentifier).toBeCalledWith({ phone: 'phone' });
|
||||
expect(verifyUserPassword).toBeCalledWith({ id: 'foo' }, 'password');
|
||||
expect(result).toEqual({
|
||||
|
@ -156,8 +148,8 @@ describe('identifier verification', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
const result = await identifierPayloadVerification(ctx, new Provider(''));
|
||||
expect(verifyIdentifierByPasscodeMock).toBeCalledWith(
|
||||
const result = await identifierPayloadVerification(ctx, createMockProvider());
|
||||
expect(verifyIdentifierByPasscode).toBeCalledWith(
|
||||
{ ...identifier, event: Event.SignIn },
|
||||
'jti',
|
||||
log
|
||||
|
@ -180,8 +172,8 @@ describe('identifier verification', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
const result = await identifierPayloadVerification(ctx, new Provider(''));
|
||||
expect(verifyIdentifierByPasscodeMock).toBeCalledWith(
|
||||
const result = await identifierPayloadVerification(ctx, createMockProvider());
|
||||
expect(verifyIdentifierByPasscode).toBeCalledWith(
|
||||
{ ...identifier, event: Event.SignIn },
|
||||
'jti',
|
||||
log
|
||||
|
@ -204,10 +196,10 @@ describe('identifier verification', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
const result = await identifierPayloadVerification(ctx, new Provider(''));
|
||||
const result = await identifierPayloadVerification(ctx, createMockProvider());
|
||||
|
||||
expect(verifySocialIdentity).toBeCalledWith(identifier, log);
|
||||
expect(findUserByIdentifierMock).not.toBeCalled();
|
||||
expect(findUserByIdentifier).not.toBeCalled();
|
||||
|
||||
expect(result).toEqual({
|
||||
event: Event.SignIn,
|
||||
|
@ -242,7 +234,11 @@ describe('identifier verification', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
const result = await identifierPayloadVerification(ctx, new Provider(''), interactionRecord);
|
||||
const result = await identifierPayloadVerification(
|
||||
ctx,
|
||||
createMockProvider(),
|
||||
interactionRecord
|
||||
);
|
||||
expect(result).toEqual({
|
||||
event: Event.SignIn,
|
||||
identifiers: [
|
||||
|
@ -273,7 +269,7 @@ describe('identifier verification', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
await expect(identifierPayloadVerification(ctx, new Provider(''))).rejects.toMatchError(
|
||||
await expect(identifierPayloadVerification(ctx, createMockProvider())).rejects.toMatchError(
|
||||
new RequestError('session.connector_session_not_found')
|
||||
);
|
||||
});
|
||||
|
@ -303,7 +299,7 @@ describe('identifier verification', () => {
|
|||
};
|
||||
|
||||
await expect(
|
||||
identifierPayloadVerification(ctx, new Provider(''), interactionRecord)
|
||||
identifierPayloadVerification(ctx, createMockProvider(), interactionRecord)
|
||||
).rejects.toMatchError(new RequestError('session.connector_session_not_found'));
|
||||
});
|
||||
|
||||
|
@ -319,12 +315,12 @@ describe('identifier verification', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
const result = await identifierPayloadVerification(ctx, new Provider(''), {
|
||||
const result = await identifierPayloadVerification(ctx, createMockProvider(), {
|
||||
event: Event.Register,
|
||||
identifiers: [oldIdentifier],
|
||||
});
|
||||
|
||||
expect(verifyIdentifierByPasscodeMock).toBeCalledWith(
|
||||
expect(verifyIdentifierByPasscode).toBeCalledWith(
|
||||
{ ...identifier, event: Event.SignIn },
|
||||
'jti',
|
||||
log
|
||||
|
|
|
@ -1,28 +1,26 @@
|
|||
import { Event, MissingProfile, SignInIdentifier } from '@logto/schemas';
|
||||
import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { findUserById } from '#src/queries/user.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import type { IdentifierVerifiedInteractionResult } from '../types/index.js';
|
||||
import { isUserPasswordSet } from '../utils/index.js';
|
||||
import validateMandatoryUserProfile from './mandatory-user-profile-validation.js';
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })),
|
||||
})),
|
||||
}));
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('#src/queries/user.js', () => ({
|
||||
const { findUserById } = await mockEsmWithActual('#src/queries/user.js', () => ({
|
||||
findUserById: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../utils/index.js', () => ({
|
||||
const { isUserPasswordSet } = mockEsm('../utils/index.js', () => ({
|
||||
isUserPasswordSet: jest.fn(),
|
||||
}));
|
||||
|
||||
const validateMandatoryUserProfile = await pickDefault(
|
||||
import('./mandatory-user-profile-validation.js')
|
||||
);
|
||||
|
||||
describe('validateMandatoryUserProfile', () => {
|
||||
const baseCtx = createContextWithRouteParameters();
|
||||
const interaction: IdentifierVerifiedInteractionResult = {
|
||||
|
@ -55,10 +53,10 @@ describe('validateMandatoryUserProfile', () => {
|
|||
});
|
||||
|
||||
it('user account has username and password', async () => {
|
||||
(findUserById as jest.Mock).mockResolvedValueOnce({
|
||||
findUserById.mockResolvedValueOnce({
|
||||
username: 'foo',
|
||||
});
|
||||
(isUserPasswordSet as jest.Mock).mockResolvedValueOnce(true);
|
||||
isUserPasswordSet.mockResolvedValueOnce(true);
|
||||
|
||||
const ctx = {
|
||||
...baseCtx,
|
||||
|
@ -86,7 +84,7 @@ describe('validateMandatoryUserProfile', () => {
|
|||
});
|
||||
|
||||
it('user account has email', async () => {
|
||||
(findUserById as jest.Mock).mockResolvedValueOnce({
|
||||
findUserById.mockResolvedValueOnce({
|
||||
primaryEmail: 'email',
|
||||
});
|
||||
|
||||
|
@ -119,7 +117,7 @@ describe('validateMandatoryUserProfile', () => {
|
|||
});
|
||||
|
||||
it('user account has phone', async () => {
|
||||
(findUserById as jest.Mock).mockResolvedValueOnce({
|
||||
findUserById.mockResolvedValueOnce({
|
||||
primaryPhone: 'phone',
|
||||
});
|
||||
|
||||
|
|
|
@ -1,35 +1,30 @@
|
|||
import { Event } from '@logto/schemas';
|
||||
import { argon2Verify } from 'hash-wasm';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { findUserById } from '#src/queries/user.js';
|
||||
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import type { InteractionContext } from '../types/index.js';
|
||||
import { storeInteractionResult } from '../utils/interaction.js';
|
||||
import verifyProfile from './profile-verification.js';
|
||||
|
||||
jest.mock('../utils/interaction.js', () => ({
|
||||
const { jest } = import.meta;
|
||||
|
||||
const { storeInteractionResult } = mockEsm('../utils/interaction.js', () => ({
|
||||
storeInteractionResult: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/user.js', () => ({
|
||||
const { findUserById } = await mockEsmWithActual('#src/queries/user.js', () => ({
|
||||
findUserById: jest.fn().mockResolvedValue({ id: 'foo', passwordEncrypted: 'passwordHash' }),
|
||||
}));
|
||||
|
||||
jest.mock('hash-wasm', () => ({
|
||||
const { argon2Verify } = mockEsm('hash-wasm', () => ({
|
||||
argon2Verify: jest.fn(),
|
||||
}));
|
||||
|
||||
const verifyProfile = await pickDefault(import('./profile-verification.js'));
|
||||
|
||||
describe('forgot password interaction profile verification', () => {
|
||||
const provider = new Provider('');
|
||||
const provider = createMockProvider();
|
||||
const baseCtx = createContextWithRouteParameters();
|
||||
|
||||
const interaction = {
|
||||
|
@ -55,7 +50,7 @@ describe('forgot password interaction profile verification', () => {
|
|||
});
|
||||
|
||||
it('same password', async () => {
|
||||
(argon2Verify as jest.Mock).mockResolvedValueOnce(true);
|
||||
argon2Verify.mockResolvedValueOnce(true);
|
||||
const ctx: InteractionContext = {
|
||||
...baseCtx,
|
||||
interactionPayload: {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Event } from '@logto/schemas';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { findUserById } from '#src/queries/user.js';
|
||||
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import type {
|
||||
|
@ -10,32 +10,28 @@ import type {
|
|||
IdentifierVerifiedInteractionResult,
|
||||
InteractionContext,
|
||||
} from '../types/index.js';
|
||||
import { storeInteractionResult } from '../utils/interaction.js';
|
||||
import verifyProfile from './profile-verification.js';
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })),
|
||||
})),
|
||||
}));
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('#src/queries/user.js', () => ({
|
||||
const { findUserById } = await mockEsmWithActual('#src/queries/user.js', () => ({
|
||||
findUserById: jest.fn().mockResolvedValue({ id: 'foo' }),
|
||||
hasUserWithEmail: jest.fn().mockResolvedValue(false),
|
||||
hasUserWithPhone: jest.fn().mockResolvedValue(false),
|
||||
hasUserWithIdentity: jest.fn().mockResolvedValue(false),
|
||||
}));
|
||||
|
||||
jest.mock('../utils/interaction.js', () => ({
|
||||
const { storeInteractionResult } = mockEsm('../utils/interaction.js', () => ({
|
||||
storeInteractionResult: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../utils/index.js', () => ({
|
||||
mockEsm('../utils/index.js', () => ({
|
||||
isUserPasswordSet: jest.fn().mockResolvedValueOnce(true),
|
||||
}));
|
||||
|
||||
const verifyProfile = await pickDefault(import('./profile-verification.js'));
|
||||
|
||||
describe('Should throw when providing existing identifiers in profile', () => {
|
||||
const provider = new Provider('');
|
||||
const provider = createMockProvider();
|
||||
const baseCtx = createContextWithRouteParameters();
|
||||
const identifiers: Identifier[] = [
|
||||
{ key: 'accountId', value: 'foo' },
|
||||
|
@ -54,7 +50,7 @@ describe('Should throw when providing existing identifiers in profile', () => {
|
|||
});
|
||||
|
||||
it('username exists', async () => {
|
||||
(findUserById as jest.Mock).mockResolvedValueOnce({ id: 'foo', username: 'foo' });
|
||||
findUserById.mockResolvedValueOnce({ id: 'foo', username: 'foo' });
|
||||
|
||||
const ctx: InteractionContext = {
|
||||
...baseCtx,
|
||||
|
@ -75,7 +71,7 @@ describe('Should throw when providing existing identifiers in profile', () => {
|
|||
});
|
||||
|
||||
it('email exists', async () => {
|
||||
(findUserById as jest.Mock).mockResolvedValueOnce({ id: 'foo', primaryEmail: 'email' });
|
||||
findUserById.mockResolvedValueOnce({ id: 'foo', primaryEmail: 'email' });
|
||||
|
||||
const ctx: InteractionContext = {
|
||||
...baseCtx,
|
||||
|
@ -96,7 +92,7 @@ describe('Should throw when providing existing identifiers in profile', () => {
|
|||
});
|
||||
|
||||
it('phone exists', async () => {
|
||||
(findUserById as jest.Mock).mockResolvedValueOnce({ id: 'foo', primaryPhone: 'phone' });
|
||||
findUserById.mockResolvedValueOnce({ id: 'foo', primaryPhone: 'phone' });
|
||||
|
||||
const ctx: InteractionContext = {
|
||||
...baseCtx,
|
||||
|
@ -117,7 +113,7 @@ describe('Should throw when providing existing identifiers in profile', () => {
|
|||
});
|
||||
|
||||
it('password exists', async () => {
|
||||
(findUserById as jest.Mock).mockResolvedValueOnce({ id: 'foo' });
|
||||
findUserById.mockResolvedValueOnce({ id: 'foo' });
|
||||
|
||||
const ctx: InteractionContext = {
|
||||
...baseCtx,
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
import { Event } from '@logto/schemas';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import {
|
||||
hasUser,
|
||||
hasUserWithEmail,
|
||||
hasUserWithPhone,
|
||||
hasUserWithIdentity,
|
||||
} from '#src/queries/user.js';
|
||||
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import type {
|
||||
|
@ -15,33 +10,30 @@ import type {
|
|||
InteractionContext,
|
||||
IdentifierVerifiedInteractionResult,
|
||||
} from '../types/index.js';
|
||||
import { storeInteractionResult } from '../utils/interaction.js';
|
||||
import verifyProfile from './profile-verification.js';
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })),
|
||||
})),
|
||||
}));
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('../utils/interaction.js', () => ({
|
||||
const { storeInteractionResult } = mockEsm('../utils/interaction.js', () => ({
|
||||
storeInteractionResult: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/user.js', () => ({
|
||||
hasUser: jest.fn().mockResolvedValue(false),
|
||||
findUserById: jest.fn().mockResolvedValue({ id: 'foo' }),
|
||||
hasUserWithEmail: jest.fn().mockResolvedValue(false),
|
||||
hasUserWithPhone: jest.fn().mockResolvedValue(false),
|
||||
hasUserWithIdentity: jest.fn().mockResolvedValue(false),
|
||||
}));
|
||||
const { hasUser, hasUserWithEmail, hasUserWithPhone, hasUserWithIdentity } =
|
||||
await mockEsmWithActual('#src/queries/user.js', () => ({
|
||||
hasUser: jest.fn().mockResolvedValue(false),
|
||||
findUserById: jest.fn().mockResolvedValue({ id: 'foo' }),
|
||||
hasUserWithEmail: jest.fn().mockResolvedValue(false),
|
||||
hasUserWithPhone: jest.fn().mockResolvedValue(false),
|
||||
hasUserWithIdentity: jest.fn().mockResolvedValue(false),
|
||||
}));
|
||||
|
||||
jest.mock('#src/connectors/index.js', () => ({
|
||||
mockEsm('#src/connectors/index.js', () => ({
|
||||
getLogtoConnectorById: jest.fn().mockResolvedValue({
|
||||
metadata: { target: 'logto' },
|
||||
}),
|
||||
}));
|
||||
|
||||
const verifyProfile = await pickDefault(import('./profile-verification.js'));
|
||||
|
||||
const baseCtx = createContextWithRouteParameters();
|
||||
const identifiers: Identifier[] = [
|
||||
{ key: 'accountId', value: 'foo' },
|
||||
|
@ -49,7 +41,7 @@ const identifiers: Identifier[] = [
|
|||
{ key: 'phoneVerified', value: '123456' },
|
||||
{ key: 'social', connectorId: 'connectorId', userInfo: { id: 'foo' } },
|
||||
];
|
||||
const provider = new Provider('');
|
||||
const provider = createMockProvider();
|
||||
|
||||
const interaction: IdentifierVerifiedInteractionResult = {
|
||||
event: Event.Register,
|
||||
|
@ -140,7 +132,7 @@ describe('register payload guard', () => {
|
|||
|
||||
describe('profile registered validation', () => {
|
||||
it('username is registered', async () => {
|
||||
(hasUser as jest.Mock).mockResolvedValueOnce(true);
|
||||
hasUser.mockResolvedValueOnce(true);
|
||||
|
||||
const ctx: InteractionContext = {
|
||||
...baseCtx,
|
||||
|
@ -163,7 +155,7 @@ describe('profile registered validation', () => {
|
|||
});
|
||||
|
||||
it('email is registered', async () => {
|
||||
(hasUserWithEmail as jest.Mock).mockResolvedValueOnce(true);
|
||||
hasUserWithEmail.mockResolvedValueOnce(true);
|
||||
|
||||
const ctx: InteractionContext = {
|
||||
...baseCtx,
|
||||
|
@ -185,7 +177,7 @@ describe('profile registered validation', () => {
|
|||
});
|
||||
|
||||
it('phone is registered', async () => {
|
||||
(hasUserWithPhone as jest.Mock).mockResolvedValueOnce(true);
|
||||
hasUserWithPhone.mockResolvedValueOnce(true);
|
||||
|
||||
const ctx: InteractionContext = {
|
||||
...baseCtx,
|
||||
|
@ -207,7 +199,7 @@ describe('profile registered validation', () => {
|
|||
});
|
||||
|
||||
it('connector identity exist', async () => {
|
||||
(hasUserWithIdentity as jest.Mock).mockResolvedValueOnce(true);
|
||||
hasUserWithIdentity.mockResolvedValueOnce(true);
|
||||
|
||||
const ctx: InteractionContext = {
|
||||
...baseCtx,
|
||||
|
|
|
@ -1,40 +1,37 @@
|
|||
import { Event } from '@logto/schemas';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import type { Identifier, InteractionContext } from '../types/index.js';
|
||||
import { storeInteractionResult } from '../utils/interaction.js';
|
||||
import verifyProfile from './profile-verification.js';
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })),
|
||||
})),
|
||||
}));
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('../utils/interaction.js', () => ({
|
||||
const { storeInteractionResult } = mockEsm('../utils/interaction.js', () => ({
|
||||
storeInteractionResult: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/user.js', () => ({
|
||||
await mockEsmWithActual('#src/queries/user.js', () => ({
|
||||
findUserById: jest.fn().mockResolvedValue({ id: 'foo' }),
|
||||
hasUserWithEmail: jest.fn().mockResolvedValue(false),
|
||||
hasUserWithPhone: jest.fn().mockResolvedValue(false),
|
||||
hasUserWithIdentity: jest.fn().mockResolvedValue(false),
|
||||
}));
|
||||
|
||||
jest.mock('#src/connectors/index.js', () => ({
|
||||
mockEsm('#src/connectors/index.js', () => ({
|
||||
getLogtoConnectorById: jest.fn().mockResolvedValue({
|
||||
metadata: { target: 'logto' },
|
||||
}),
|
||||
}));
|
||||
|
||||
const verifyProfile = await pickDefault(import('./profile-verification.js'));
|
||||
|
||||
describe('profile protected identifier verification', () => {
|
||||
const baseCtx = createContextWithRouteParameters();
|
||||
const interaction = { event: Event.SignIn, accountId: 'foo' };
|
||||
const provider = new Provider('');
|
||||
const provider = createMockProvider();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
|
@ -1,32 +1,28 @@
|
|||
import { Event } from '@logto/schemas';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { mockEsm, mockEsmDefault, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import type { InteractionContext, PayloadVerifiedInteractionResult } from '../types/index.js';
|
||||
import findUserByIdentifier from '../utils/find-user-by-identifier.js';
|
||||
import { storeInteractionResult } from '../utils/interaction.js';
|
||||
import userAccountVerification from './user-identity-verification.js';
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })),
|
||||
})),
|
||||
}));
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('../utils/find-user-by-identifier.js', () => jest.fn());
|
||||
jest.mock('#src/lib/social.js', () => ({
|
||||
const findUserByIdentifier = mockEsmDefault('../utils/find-user-by-identifier.js', () => jest.fn());
|
||||
|
||||
mockEsm('#src/lib/social.js', () => ({
|
||||
findSocialRelatedUser: jest.fn().mockResolvedValue(null),
|
||||
}));
|
||||
|
||||
jest.mock('../utils/interaction.js', () => ({
|
||||
...jest.requireActual('../utils/interaction.js'),
|
||||
const { storeInteractionResult } = await mockEsmWithActual('../utils/interaction.js', () => ({
|
||||
storeInteractionResult: jest.fn(),
|
||||
}));
|
||||
|
||||
const userAccountVerification = await pickDefault(import('./user-identity-verification.js'));
|
||||
|
||||
describe('userAccountVerification', () => {
|
||||
const findUserByIdentifierMock = findUserByIdentifier as jest.Mock;
|
||||
const findUserByIdentifierMock = findUserByIdentifier;
|
||||
|
||||
const ctx: InteractionContext = {
|
||||
...createContextWithRouteParameters(),
|
||||
|
@ -34,7 +30,7 @@ describe('userAccountVerification', () => {
|
|||
event: Event.SignIn,
|
||||
},
|
||||
};
|
||||
const provider = new Provider('');
|
||||
const provider = createMockProvider();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
|
@ -1,25 +1,21 @@
|
|||
import type { LogCondition } from '#src/queries/log.js';
|
||||
import logRoutes from '#src/routes/log.js';
|
||||
import { mockEsm, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const mockBody = { type: 'a', payload: {}, createdAt: 123 };
|
||||
const mockLog = { id: '1', ...mockBody };
|
||||
const mockLogs = [mockLog, { id: '2', ...mockBody }];
|
||||
|
||||
const countLogs = jest.fn(async (condition: LogCondition) => ({
|
||||
count: mockLogs.length,
|
||||
}));
|
||||
const findLogs = jest.fn(
|
||||
async (limit: number, offset: number, condition: LogCondition) => mockLogs
|
||||
);
|
||||
const findLogById = jest.fn(async (id: string) => mockLog);
|
||||
|
||||
jest.mock('#src/queries/log.js', () => ({
|
||||
countLogs: async (condition: LogCondition) => countLogs(condition),
|
||||
findLogs: async (limit: number, offset: number, condition: LogCondition) =>
|
||||
findLogs(limit, offset, condition),
|
||||
findLogById: async (id: string) => findLogById(id),
|
||||
const { countLogs, findLogs, findLogById } = mockEsm('#src/queries/log.js', () => ({
|
||||
countLogs: jest.fn().mockResolvedValue({
|
||||
count: mockLogs.length,
|
||||
}),
|
||||
findLogs: jest.fn().mockResolvedValue(mockLogs),
|
||||
findLogById: jest.fn().mockResolvedValue(mockLog),
|
||||
}));
|
||||
const logRoutes = await pickDefault(import('./log.js'));
|
||||
|
||||
describe('logRoutes', () => {
|
||||
const logRequest = createRequester({ authedRoutes: logRoutes });
|
||||
|
|
|
@ -1,51 +1,42 @@
|
|||
import en from '@logto/phrases-ui/lib/locales/en.js';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { trTrTag, zhCnTag, zhHkTag } from '#src/__mocks__/custom-phrase.js';
|
||||
import { mockSignInExperience } from '#src/__mocks__/index.js';
|
||||
import phraseRoutes from '#src/routes/phrase.js';
|
||||
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
const mockApplicationId = 'mockApplicationIdValue';
|
||||
|
||||
const interactionDetails: jest.MockedFunction<() => Promise<unknown>> = jest.fn(async () => ({
|
||||
params: { client_id: mockApplicationId },
|
||||
}));
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails,
|
||||
})),
|
||||
}));
|
||||
const { jest } = import.meta;
|
||||
|
||||
const fallbackLanguage = trTrTag;
|
||||
const unsupportedLanguageX = 'xx-XX';
|
||||
const unsupportedLanguageY = 'yy-YY';
|
||||
|
||||
const findDefaultSignInExperience = jest.fn(async () => ({
|
||||
...mockSignInExperience,
|
||||
languageInfo: {
|
||||
autoDetect: true,
|
||||
fallbackLanguage,
|
||||
},
|
||||
}));
|
||||
const { findDefaultSignInExperience } = await mockEsmWithActual(
|
||||
'#src/queries/sign-in-experience.js',
|
||||
() => ({
|
||||
findDefaultSignInExperience: jest.fn(async () => ({
|
||||
...mockSignInExperience,
|
||||
languageInfo: {
|
||||
autoDetect: true,
|
||||
fallbackLanguage,
|
||||
},
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock('#src/queries/sign-in-experience.js', () => ({
|
||||
findDefaultSignInExperience: async () => findDefaultSignInExperience(),
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/custom-phrase.js', () => ({
|
||||
await mockEsmWithActual('#src/queries/custom-phrase.js', () => ({
|
||||
findAllCustomLanguageTags: async () => [trTrTag, zhCnTag],
|
||||
}));
|
||||
|
||||
jest.mock('#src/lib/phrase.js', () => ({
|
||||
...jest.requireActual('#src/lib/phrase.js'),
|
||||
await mockEsmWithActual('#src/lib/phrase.js', () => ({
|
||||
getPhrase: jest.fn().mockResolvedValue(en),
|
||||
}));
|
||||
const phraseRoutes = await pickDefault(import('./phrase.js'));
|
||||
|
||||
const phraseRequest = createRequester({
|
||||
anonymousRoutes: phraseRoutes,
|
||||
provider: new Provider(''),
|
||||
provider: createMockProvider(),
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
@ -4,67 +4,48 @@ import {
|
|||
adminConsoleApplicationId,
|
||||
adminConsoleSignInExperience,
|
||||
} from '@logto/schemas/lib/seeds/index.js';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { zhCnTag } from '#src/__mocks__/custom-phrase.js';
|
||||
import { mockSignInExperience } from '#src/__mocks__/index.js';
|
||||
import * as detectLanguage from '#src/i18n/detect-language.js';
|
||||
import phraseRoutes from '#src/routes/phrase.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||
|
||||
const mockApplicationId = 'mockApplicationIdValue';
|
||||
|
||||
const interactionDetails: jest.MockedFunction<() => Promise<unknown>> = jest.fn(async () => ({
|
||||
params: { client_id: mockApplicationId },
|
||||
}));
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails,
|
||||
})),
|
||||
}));
|
||||
const { jest } = import.meta;
|
||||
|
||||
const customizedLanguage = zhCnTag;
|
||||
|
||||
const findDefaultSignInExperience = jest.fn(
|
||||
async (): Promise<SignInExperience> => ({
|
||||
...mockSignInExperience,
|
||||
languageInfo: {
|
||||
autoDetect: true,
|
||||
fallbackLanguage: customizedLanguage,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock('#src/queries/sign-in-experience.js', () => ({
|
||||
findDefaultSignInExperience: async () => findDefaultSignInExperience(),
|
||||
const { findDefaultSignInExperience } = mockEsm('#src/queries/sign-in-experience.js', () => ({
|
||||
findDefaultSignInExperience: jest.fn(
|
||||
async (): Promise<SignInExperience> => ({
|
||||
...mockSignInExperience,
|
||||
languageInfo: {
|
||||
autoDetect: true,
|
||||
fallbackLanguage: customizedLanguage,
|
||||
},
|
||||
})
|
||||
),
|
||||
}));
|
||||
|
||||
const detectLanguageSpy = jest.spyOn(detectLanguage, 'default');
|
||||
|
||||
const findAllCustomLanguageTags = jest.fn(async () => [customizedLanguage]);
|
||||
const findCustomPhraseByLanguageTag = jest.fn(async (tag: string) => ({}));
|
||||
|
||||
jest.mock('#src/queries/custom-phrase.js', () => ({
|
||||
findAllCustomLanguageTags: async () => findAllCustomLanguageTags(),
|
||||
findCustomPhraseByLanguageTag: async (tag: string) => findCustomPhraseByLanguageTag(tag),
|
||||
const { default: detectLanguageSpy } = mockEsm('#src/i18n/detect-language.js', () => ({
|
||||
default: jest.fn().mockReturnValue([]),
|
||||
}));
|
||||
|
||||
const getPhrase = jest.fn(async (language: string, customLanguages: string[]) => zhCN);
|
||||
|
||||
jest.mock('#src/lib/phrase.js', () => ({
|
||||
...jest.requireActual('#src/lib/phrase.js'),
|
||||
getPhrase: async (language: string, customLanguages: string[]) =>
|
||||
getPhrase(language, customLanguages),
|
||||
const { findAllCustomLanguageTags } = mockEsm('#src/queries/custom-phrase.js', () => ({
|
||||
findAllCustomLanguageTags: jest.fn(async () => [customizedLanguage]),
|
||||
findCustomPhraseByLanguageTag: jest.fn(async (tag: string) => ({})),
|
||||
}));
|
||||
|
||||
const { getPhrase } = await mockEsmWithActual('#src/lib/phrase.js', () => ({
|
||||
getPhrase: jest.fn(async () => zhCN),
|
||||
}));
|
||||
|
||||
const interactionDetails = jest.fn();
|
||||
const phraseRoutes = await pickDefault(import('./phrase.js'));
|
||||
|
||||
const { createRequester } = await import('#src/utils/test-utils.js');
|
||||
const phraseRequest = createRequester({
|
||||
anonymousRoutes: phraseRoutes,
|
||||
provider: new Provider(''),
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
provider: createMockProvider(interactionDetails),
|
||||
});
|
||||
|
||||
describe('when the application is admin-console', () => {
|
||||
|
@ -74,6 +55,10 @@ describe('when the application is admin-console', () => {
|
|||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call interactionDetails', async () => {
|
||||
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
|
||||
expect(interactionDetails).toBeCalledTimes(1);
|
||||
|
@ -104,6 +89,18 @@ describe('when the application is admin-console', () => {
|
|||
});
|
||||
|
||||
describe('when the application is not admin-console', () => {
|
||||
beforeEach(() => {
|
||||
interactionDetails.mockResolvedValue({
|
||||
params: {},
|
||||
jti: 'jti',
|
||||
client_id: 'mockApplicationId',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call interactionDetails', async () => {
|
||||
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
|
||||
expect(interactionDetails).toBeCalledTimes(1);
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
/* eslint-disable max-lines */
|
||||
import type { CreateUser, User } from '@logto/schemas';
|
||||
import { ConnectorType } from '@logto/schemas';
|
||||
import { mockEsm, mockEsmWithActual } from '@logto/shared/esm';
|
||||
import { getUnixTime } from 'date-fns';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import {
|
||||
mockLogtoConnectorList,
|
||||
|
@ -11,101 +10,79 @@ import {
|
|||
mockUserResponse,
|
||||
} from '#src/__mocks__/index.js';
|
||||
import type { SocialUserInfo } from '#src/connectors/types.js';
|
||||
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
import profileRoutes, { profileRoute } from './profile.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
const mockFindUserById = jest.fn(async (): Promise<User> => mockUser);
|
||||
const mockHasUser = jest.fn(async () => false);
|
||||
const mockHasUserWithEmail = jest.fn(async () => false);
|
||||
const mockHasUserWithPhone = jest.fn(async () => false);
|
||||
const mockUpdateUserById = jest.fn(
|
||||
async (_, data: Partial<CreateUser>): Promise<User> => ({
|
||||
...mockUser,
|
||||
...data,
|
||||
})
|
||||
);
|
||||
const mockDeleteUserIdentity = jest.fn();
|
||||
const encryptUserPassword = jest.fn(async (password: string) => ({
|
||||
passwordEncrypted: password + '_user1',
|
||||
passwordEncryptionMethod: 'Argon2i',
|
||||
}));
|
||||
const mockArgon2Verify = jest.fn(async (password: string) => password === mockPasswordEncrypted);
|
||||
const mockGetSession = jest.fn();
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
Session: {
|
||||
get: async () => mockGetSession(),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('#src/lib/user.js', () => ({
|
||||
...jest.requireActual('#src/lib/user.js'),
|
||||
encryptUserPassword: async (password: string) => encryptUserPassword(password),
|
||||
}));
|
||||
|
||||
const mockGetLogtoConnectorById = jest.fn(async () => ({
|
||||
const getLogtoConnectorById = jest.fn(async () => ({
|
||||
dbEntry: { enabled: true },
|
||||
metadata: { id: 'connectorId', target: 'mock_social' },
|
||||
type: ConnectorType.Social,
|
||||
getAuthorizationUri: jest.fn(async () => ''),
|
||||
}));
|
||||
|
||||
jest.mock('#src/connectors/index.js', () => ({
|
||||
getLogtoConnectors: jest.fn(async () => mockLogtoConnectorList),
|
||||
getLogtoConnectorById: jest.fn(async () => mockGetLogtoConnectorById()),
|
||||
mockEsm('#src/connectors/index.js', () => ({
|
||||
getLogtoConnectors: mockLogtoConnectorList,
|
||||
getLogtoConnectorById,
|
||||
}));
|
||||
|
||||
const mockFindSocialRelatedUser = jest.fn(async () => [
|
||||
{ id: 'user1', identities: {}, isSuspended: false },
|
||||
]);
|
||||
const mockGetUserInfoByAuthCode = jest.fn();
|
||||
jest.mock('#src/lib/social.js', () => ({
|
||||
...jest.requireActual('#src/lib/social.js'),
|
||||
findSocialRelatedUser: async () => mockFindSocialRelatedUser(),
|
||||
getUserInfoByAuthCode: async () => mockGetUserInfoByAuthCode(),
|
||||
const { getUserInfoByAuthCode } = await mockEsmWithActual('#src/lib/social.js', () => ({
|
||||
findSocialRelatedUser: jest.fn(async () => [{ id: 'user1', identities: {}, isSuspended: false }]),
|
||||
getUserInfoByAuthCode: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/user.js', () => ({
|
||||
...jest.requireActual('#src/queries/user.js'),
|
||||
findUserById: async () => mockFindUserById(),
|
||||
hasUser: async () => mockHasUser(),
|
||||
hasUserWithEmail: async () => mockHasUserWithEmail(),
|
||||
hasUserWithPhone: async () => mockHasUserWithPhone(),
|
||||
updateUserById: async (id: string, data: Partial<CreateUser>) => mockUpdateUserById(id, data),
|
||||
deleteUserIdentity: async (...args: unknown[]) => mockDeleteUserIdentity(...args),
|
||||
const {
|
||||
findUserById,
|
||||
hasUser,
|
||||
hasUserWithEmail,
|
||||
hasUserWithPhone,
|
||||
updateUserById,
|
||||
deleteUserIdentity,
|
||||
} = await mockEsmWithActual('#src/queries/user.js', () => ({
|
||||
findUserById: jest.fn(async (): Promise<User> => mockUser),
|
||||
hasUser: jest.fn(async () => false),
|
||||
hasUserWithEmail: jest.fn(async () => false),
|
||||
hasUserWithPhone: jest.fn(async () => false),
|
||||
updateUserById: jest.fn(
|
||||
async (_, data: Partial<CreateUser>): Promise<User> => ({
|
||||
...mockUser,
|
||||
...data,
|
||||
})
|
||||
),
|
||||
deleteUserIdentity: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockFindDefaultSignInExperience = jest.fn(async () => ({
|
||||
signUp: {
|
||||
identifier: [],
|
||||
password: false,
|
||||
verify: false,
|
||||
},
|
||||
const { encryptUserPassword } = await mockEsmWithActual('#src/lib/user.js', () => ({
|
||||
encryptUserPassword: jest.fn(async (password: string) => ({
|
||||
passwordEncrypted: password + '_user1',
|
||||
passwordEncryptionMethod: 'Argon2i',
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/sign-in-experience.js', () => ({
|
||||
findDefaultSignInExperience: jest.fn(async () => mockFindDefaultSignInExperience()),
|
||||
mockEsm('#src/queries/sign-in-experience.js', () => ({
|
||||
findDefaultSignInExperience: async () => ({
|
||||
signUp: {
|
||||
identifier: [],
|
||||
password: false,
|
||||
verify: false,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hash-wasm', () => ({
|
||||
argon2Verify: async (password: string) => mockArgon2Verify(password),
|
||||
const { argon2Verify } = mockEsm('hash-wasm', () => ({
|
||||
argon2Verify: jest.fn(async (password: string) => password === mockPasswordEncrypted),
|
||||
}));
|
||||
|
||||
const { default: profileRoutes, profileRoute } = await import('./profile.js');
|
||||
|
||||
describe('session -> profileRoutes', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockGetSession.mockImplementation(async () => ({
|
||||
accountId: 'id',
|
||||
loginTs: getUnixTime(new Date()) - 60,
|
||||
}));
|
||||
});
|
||||
|
||||
const provider = createMockProvider();
|
||||
// @ts-expect-error for testing
|
||||
const mockGetSession: jest.Mock = jest.spyOn(provider.Session, 'get');
|
||||
const sessionRequest = createRequester({
|
||||
anonymousRoutes: profileRoutes,
|
||||
provider: new Provider(''),
|
||||
provider,
|
||||
middlewares: [
|
||||
async (ctx, next) => {
|
||||
ctx.addLogContext = jest.fn();
|
||||
|
@ -116,6 +93,14 @@ describe('session -> profileRoutes', () => {
|
|||
],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockGetSession.mockImplementation(async () => ({
|
||||
accountId: 'id',
|
||||
loginTs: getUnixTime(new Date()) - 60,
|
||||
}));
|
||||
});
|
||||
|
||||
describe('GET /session/profile', () => {
|
||||
it('should return current user data', async () => {
|
||||
const response = await sessionRequest.get(profileRoute);
|
||||
|
@ -146,7 +131,7 @@ describe('session -> profileRoutes', () => {
|
|||
|
||||
const response = await sessionRequest.patch(profileRoute).send(updatedUserInfo);
|
||||
|
||||
expect(mockUpdateUserById).toBeCalledWith('id', expect.objectContaining(updatedUserInfo));
|
||||
expect(updateUserById).toBeCalledWith('id', expect.objectContaining(updatedUserInfo));
|
||||
expect(response.statusCode).toEqual(204);
|
||||
});
|
||||
|
||||
|
@ -175,7 +160,7 @@ describe('session -> profileRoutes', () => {
|
|||
.send({ username: 'test' });
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
expect(updateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should update username with the new value', async () => {
|
||||
|
@ -190,7 +175,7 @@ describe('session -> profileRoutes', () => {
|
|||
});
|
||||
|
||||
it('should throw when username is already in use', async () => {
|
||||
mockHasUser.mockImplementationOnce(async () => true);
|
||||
hasUser.mockImplementationOnce(async () => true);
|
||||
|
||||
const response = await sessionRequest
|
||||
.patch(`${profileRoute}/username`)
|
||||
|
@ -212,7 +197,7 @@ describe('session -> profileRoutes', () => {
|
|||
.send({ password: mockPasswordEncrypted });
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
expect(updateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should update password with the new value', async () => {
|
||||
|
@ -220,7 +205,7 @@ describe('session -> profileRoutes', () => {
|
|||
.patch(`${profileRoute}/password`)
|
||||
.send({ password: mockPasswordEncrypted });
|
||||
|
||||
expect(mockUpdateUserById).toBeCalledWith(
|
||||
expect(updateUserById).toBeCalledWith(
|
||||
'id',
|
||||
expect.objectContaining({
|
||||
passwordEncrypted: 'a1b2c3_user1',
|
||||
|
@ -235,14 +220,14 @@ describe('session -> profileRoutes', () => {
|
|||
passwordEncrypted: password,
|
||||
passwordEncryptionMethod: 'Argon2i',
|
||||
}));
|
||||
mockArgon2Verify.mockResolvedValueOnce(true);
|
||||
argon2Verify.mockResolvedValueOnce(true);
|
||||
|
||||
const response = await sessionRequest
|
||||
.patch(`${profileRoute}/password`)
|
||||
.send({ password: 'password' });
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
expect(updateUserById).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -258,7 +243,7 @@ describe('session -> profileRoutes', () => {
|
|||
.send({ primaryEmail: 'test@logto.io' });
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
expect(updateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should link email address to the user profile', async () => {
|
||||
|
@ -267,7 +252,7 @@ describe('session -> profileRoutes', () => {
|
|||
.patch(`${profileRoute}/email`)
|
||||
.send({ primaryEmail: mockEmailAddress });
|
||||
|
||||
expect(mockUpdateUserById).toBeCalledWith(
|
||||
expect(updateUserById).toBeCalledWith(
|
||||
'id',
|
||||
expect.objectContaining({
|
||||
primaryEmail: mockEmailAddress,
|
||||
|
@ -277,25 +262,25 @@ describe('session -> profileRoutes', () => {
|
|||
});
|
||||
|
||||
it('should throw when email address already exists', async () => {
|
||||
mockHasUserWithEmail.mockImplementationOnce(async () => true);
|
||||
hasUserWithEmail.mockImplementationOnce(async () => true);
|
||||
|
||||
const response = await sessionRequest
|
||||
.patch(`${profileRoute}/email`)
|
||||
.send({ primaryEmail: mockUser.primaryEmail });
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
expect(updateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should throw when email address is invalid', async () => {
|
||||
mockHasUserWithEmail.mockImplementationOnce(async () => true);
|
||||
hasUserWithEmail.mockImplementationOnce(async () => true);
|
||||
|
||||
const response = await sessionRequest
|
||||
.patch(`${profileRoute}/email`)
|
||||
.send({ primaryEmail: 'test' });
|
||||
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
expect(updateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should throw if last authentication time is over 10 mins ago on unlinking email', async () => {
|
||||
|
@ -307,13 +292,13 @@ describe('session -> profileRoutes', () => {
|
|||
const response = await sessionRequest.delete(`${profileRoute}/email`);
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
expect(updateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should unlink email address from user', async () => {
|
||||
const response = await sessionRequest.delete(`${profileRoute}/email`);
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(mockUpdateUserById).toBeCalledWith(
|
||||
expect(updateUserById).toBeCalledWith(
|
||||
'id',
|
||||
expect.objectContaining({
|
||||
primaryEmail: null,
|
||||
|
@ -322,11 +307,11 @@ describe('session -> profileRoutes', () => {
|
|||
});
|
||||
|
||||
it('should throw when no email address found in user on unlinking email', async () => {
|
||||
mockFindUserById.mockImplementationOnce(async () => ({ ...mockUser, primaryEmail: null }));
|
||||
findUserById.mockImplementationOnce(async () => ({ ...mockUser, primaryEmail: null }));
|
||||
const response = await sessionRequest.delete(`${profileRoute}/email`);
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
expect(updateUserById).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -342,7 +327,7 @@ describe('session -> profileRoutes', () => {
|
|||
.send({ primaryPhone: '6533333333' });
|
||||
|
||||
expect(updateResponse.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
expect(updateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should link phone number to the user profile', async () => {
|
||||
|
@ -351,7 +336,7 @@ describe('session -> profileRoutes', () => {
|
|||
.patch(`${profileRoute}/phone`)
|
||||
.send({ primaryPhone: mockPhoneNumber });
|
||||
|
||||
expect(mockUpdateUserById).toBeCalledWith(
|
||||
expect(updateUserById).toBeCalledWith(
|
||||
'id',
|
||||
expect.objectContaining({
|
||||
primaryPhone: mockPhoneNumber,
|
||||
|
@ -361,25 +346,25 @@ describe('session -> profileRoutes', () => {
|
|||
});
|
||||
|
||||
it('should throw when phone number already exists on linking phone number', async () => {
|
||||
mockHasUserWithPhone.mockImplementationOnce(async () => true);
|
||||
hasUserWithPhone.mockImplementationOnce(async () => true);
|
||||
|
||||
const response = await sessionRequest
|
||||
.patch(`${profileRoute}/phone`)
|
||||
.send({ primaryPhone: mockUser.primaryPhone });
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
expect(updateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should throw when phone number is invalid', async () => {
|
||||
mockHasUserWithPhone.mockImplementationOnce(async () => true);
|
||||
hasUserWithPhone.mockImplementationOnce(async () => true);
|
||||
|
||||
const response = await sessionRequest
|
||||
.patch(`${profileRoute}/phone`)
|
||||
.send({ primaryPhone: 'invalid' });
|
||||
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
expect(updateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should throw if last authentication time is over 10 mins ago on unlinking phone number', async () => {
|
||||
|
@ -391,13 +376,13 @@ describe('session -> profileRoutes', () => {
|
|||
const response = await sessionRequest.delete(`${profileRoute}/phone`);
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
expect(updateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should unlink phone number from user', async () => {
|
||||
const response = await sessionRequest.delete(`${profileRoute}/phone`);
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(mockUpdateUserById).toBeCalledWith(
|
||||
expect(updateUserById).toBeCalledWith(
|
||||
'id',
|
||||
expect.objectContaining({
|
||||
primaryPhone: null,
|
||||
|
@ -406,11 +391,11 @@ describe('session -> profileRoutes', () => {
|
|||
});
|
||||
|
||||
it('should throw when no phone number found in user on unlinking phone number', async () => {
|
||||
mockFindUserById.mockImplementationOnce(async () => ({ ...mockUser, primaryPhone: null }));
|
||||
findUserById.mockImplementationOnce(async () => ({ ...mockUser, primaryPhone: null }));
|
||||
const response = await sessionRequest.delete(`${profileRoute}/phone`);
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
expect(updateUserById).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -423,7 +408,7 @@ describe('session -> profileRoutes', () => {
|
|||
email: 'johndoe@social.com',
|
||||
phone: '123456789',
|
||||
};
|
||||
mockGetUserInfoByAuthCode.mockReturnValueOnce(mockSocialUserInfo);
|
||||
getUserInfoByAuthCode.mockReturnValueOnce(mockSocialUserInfo);
|
||||
|
||||
const response = await sessionRequest.patch(`${profileRoute}/identities`).send({
|
||||
connectorId: 'connectorId',
|
||||
|
@ -431,7 +416,7 @@ describe('session -> profileRoutes', () => {
|
|||
});
|
||||
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(mockUpdateUserById).toBeCalledWith(
|
||||
expect(updateUserById).toBeCalledWith(
|
||||
'id',
|
||||
expect.objectContaining({
|
||||
identities: {
|
||||
|
@ -456,7 +441,7 @@ describe('session -> profileRoutes', () => {
|
|||
});
|
||||
|
||||
expect(response.statusCode).toEqual(401);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
expect(updateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should throw if last authentication time is over 10 mins ago on linking email', async () => {
|
||||
|
@ -470,11 +455,11 @@ describe('session -> profileRoutes', () => {
|
|||
.send({ connectorId: 'connectorId', data: { code: '123456' } });
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
expect(updateUserById).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should unlink social identities from user', async () => {
|
||||
mockFindUserById.mockImplementationOnce(async () => ({
|
||||
findUserById.mockImplementationOnce(async () => ({
|
||||
...mockUser,
|
||||
identities: {
|
||||
mock_social: {
|
||||
|
@ -490,8 +475,7 @@ describe('session -> profileRoutes', () => {
|
|||
const response = await sessionRequest.delete(`${profileRoute}/identities/mock_social`);
|
||||
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(mockDeleteUserIdentity).toBeCalledWith('id', 'mock_social');
|
||||
expect(deleteUserIdentity).toBeCalledWith('id', 'mock_social');
|
||||
});
|
||||
});
|
||||
});
|
||||
/* eslint-enable max-lines */
|
||||
|
|
|
@ -65,15 +65,12 @@ export default function profileRoutes<T extends AnonymousRouter>(router: T, prov
|
|||
}),
|
||||
async (ctx, next) => {
|
||||
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
||||
|
||||
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
||||
|
||||
const { username } = ctx.guard.body;
|
||||
|
||||
await checkIdentifierCollision({ username }, userId);
|
||||
|
||||
const user = await updateUserById(userId, { username }, 'replace');
|
||||
|
||||
ctx.body = pick(user, ...userInfoSelectFields);
|
||||
|
||||
return next();
|
||||
|
|
|
@ -1,34 +1,33 @@
|
|||
import type { Resource, CreateResource } from '@logto/schemas';
|
||||
import { mockEsm, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockResource } from '#src/__mocks__/index.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
import resourceRoutes from './resource.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('#src/queries/resource.js', () => ({
|
||||
findTotalNumberOfResources: jest.fn(async () => ({ count: 10 })),
|
||||
findAllResources: jest.fn(async (): Promise<Resource[]> => [mockResource]),
|
||||
findResourceById: jest.fn(async (): Promise<Resource> => mockResource),
|
||||
insertResource: jest.fn(
|
||||
async (body: CreateResource): Promise<Resource> => ({
|
||||
...mockResource,
|
||||
...body,
|
||||
})
|
||||
),
|
||||
updateResourceById: jest.fn(
|
||||
async (_, data: Partial<CreateResource>): Promise<Resource> => ({
|
||||
...mockResource,
|
||||
...data,
|
||||
})
|
||||
),
|
||||
mockEsm('#src/queries/resource.js', () => ({
|
||||
findTotalNumberOfResources: async () => ({ count: 10 }),
|
||||
findAllResources: async (): Promise<Resource[]> => [mockResource],
|
||||
findResourceById: async (): Promise<Resource> => mockResource,
|
||||
insertResource: async (body: CreateResource): Promise<Resource> => ({
|
||||
...mockResource,
|
||||
...body,
|
||||
}),
|
||||
updateResourceById: async (_: unknown, data: Partial<CreateResource>): Promise<Resource> => ({
|
||||
...mockResource,
|
||||
...data,
|
||||
}),
|
||||
deleteResourceById: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@logto/shared', () => ({
|
||||
mockEsm('@logto/shared', () => ({
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
buildIdGenerator: jest.fn(() => () => 'randomId'),
|
||||
buildIdGenerator: () => () => 'randomId',
|
||||
}));
|
||||
|
||||
const resourceRoutes = await pickDefault(import('./resource.js'));
|
||||
|
||||
describe('resource routes', () => {
|
||||
const resourceRequest = createRequester({ authedRoutes: resourceRoutes });
|
||||
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import type { Role } from '@logto/schemas';
|
||||
import { mockEsm, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockRole } from '#src/__mocks__/index.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
import roleRoutes from './role.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('#src/queries/roles.js', () => ({
|
||||
mockEsm('#src/queries/roles.js', () => ({
|
||||
findAllRoles: jest.fn(async (): Promise<Role[]> => [mockRole]),
|
||||
}));
|
||||
const roleRoutes = await pickDefault(import('./role.js'));
|
||||
|
||||
describe('role routes', () => {
|
||||
const roleRequester = createRequester({ authedRoutes: roleRoutes });
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import type { User } from '@logto/schemas';
|
||||
import { UserRole, SignInIdentifier } from '@logto/schemas';
|
||||
import { createMockContext } from '@shopify/jest-koa-mocks';
|
||||
import type { Nullable } from '@silverhand/essentials';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import { mockSignInExperience, mockSignInMethod, mockUser } from '#src/__mocks__/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js';
|
||||
|
||||
import { checkRequiredProfile, signInWithPassword } from './utils.js';
|
||||
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
import type { Setting, CreateSetting } from '@logto/schemas';
|
||||
import { mockEsm, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockSetting } from '#src/__mocks__/index.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
import settingRoutes from './setting.js';
|
||||
|
||||
jest.mock('#src/queries/setting.js', () => ({
|
||||
getSetting: jest.fn(async (): Promise<Setting> => mockSetting),
|
||||
updateSetting: jest.fn(
|
||||
async (data: Partial<CreateSetting>): Promise<Setting> => ({
|
||||
...mockSetting,
|
||||
...data,
|
||||
})
|
||||
),
|
||||
mockEsm('#src/queries/setting.js', () => ({
|
||||
getSetting: async (): Promise<Setting> => mockSetting,
|
||||
updateSetting: async (data: Partial<CreateSetting>): Promise<Setting> => ({
|
||||
...mockSetting,
|
||||
...data,
|
||||
}),
|
||||
}));
|
||||
|
||||
const settingRoutes = await pickDefault(import('./setting.js'));
|
||||
|
||||
describe('settings routes', () => {
|
||||
const roleRequester = createRequester({ authedRoutes: settingRoutes });
|
||||
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import type { CreateSignInExperience, SignInExperience } from '@logto/schemas';
|
||||
import { BrandingStyle } from '@logto/schemas';
|
||||
import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockBranding, mockSignInExperience } from '#src/__mocks__/index.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
import signInExperiencesRoutes from './sign-in-experience.js';
|
||||
|
||||
jest.mock('#src/queries/sign-in-experience.js', () => ({
|
||||
updateDefaultSignInExperience: jest.fn(
|
||||
async (data: Partial<CreateSignInExperience>): Promise<SignInExperience> => ({
|
||||
...mockSignInExperience,
|
||||
...data,
|
||||
})
|
||||
),
|
||||
await mockEsmWithActual('#src/queries/sign-in-experience.js', () => ({
|
||||
updateDefaultSignInExperience: async (
|
||||
data: Partial<CreateSignInExperience>
|
||||
): Promise<SignInExperience> => ({
|
||||
...mockSignInExperience,
|
||||
...data,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('#src/connectors.js', () => ({
|
||||
getLogtoConnectors: jest.fn(async () => []),
|
||||
mockEsm('#src/connectors.js', () => ({
|
||||
getLogtoConnectors: async () => [],
|
||||
}));
|
||||
|
||||
const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js'));
|
||||
const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes });
|
||||
|
||||
const expectPatchResponseStatus = async (
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import type { CreateSignInExperience, SignInExperience } from '@logto/schemas';
|
||||
import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockColor, mockSignInExperience } from '#src/__mocks__/index.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
import signInExperiencesRoutes from './sign-in-experience.js';
|
||||
|
||||
jest.mock('#src/queries/sign-in-experience.js', () => ({
|
||||
updateDefaultSignInExperience: jest.fn(
|
||||
async (data: Partial<CreateSignInExperience>): Promise<SignInExperience> => ({
|
||||
...mockSignInExperience,
|
||||
...data,
|
||||
})
|
||||
),
|
||||
await mockEsmWithActual('#src/queries/sign-in-experience.js', () => ({
|
||||
updateDefaultSignInExperience: async (
|
||||
data: Partial<CreateSignInExperience>
|
||||
): Promise<SignInExperience> => ({
|
||||
...mockSignInExperience,
|
||||
...data,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('#src/connectors.js', () => ({
|
||||
getLogtoConnectors: jest.fn(async () => []),
|
||||
mockEsm('#src/connectors.js', () => ({
|
||||
getLogtoConnectors: async () => [],
|
||||
}));
|
||||
|
||||
const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js'));
|
||||
const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes });
|
||||
|
||||
const expectPatchResponseStatus = async (
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { CreateSignInExperience, LanguageInfo, SignInExperience } from '@logto/schemas';
|
||||
import type { CreateSignInExperience, SignInExperience } from '@logto/schemas';
|
||||
import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import {
|
||||
mockAliyunDmConnector,
|
||||
|
@ -10,11 +11,10 @@ import {
|
|||
mockSignInExperience,
|
||||
mockTermsOfUse,
|
||||
} from '#src/__mocks__/index.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
import signInExperiencesRoutes from './sign-in-experience.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('#src/connectors.js', () => ({
|
||||
mockEsm('#src/connectors.js', () => ({
|
||||
getLogtoConnectors: jest.fn(async () => [
|
||||
mockAliyunDmConnector,
|
||||
mockAliyunSmsConnector,
|
||||
|
@ -24,23 +24,21 @@ jest.mock('#src/connectors.js', () => ({
|
|||
]),
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const validateLanguageInfo = jest.fn(async (languageInfo: LanguageInfo): Promise<void> => {});
|
||||
|
||||
jest.mock('#src/lib/sign-in-experience.js', () => ({
|
||||
...jest.requireActual('#src/lib/sign-in-experience.js'),
|
||||
validateLanguageInfo: async (languageInfo: LanguageInfo) => validateLanguageInfo(languageInfo),
|
||||
const { validateLanguageInfo } = await mockEsmWithActual('#src/lib/sign-in-experience.js', () => ({
|
||||
validateLanguageInfo: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/sign-in-experience.js', () => ({
|
||||
updateDefaultSignInExperience: jest.fn(
|
||||
async (data: Partial<CreateSignInExperience>): Promise<SignInExperience> => ({
|
||||
...mockSignInExperience,
|
||||
...data,
|
||||
})
|
||||
),
|
||||
await mockEsmWithActual('#src/queries/sign-in-experience.js', () => ({
|
||||
updateDefaultSignInExperience: async (
|
||||
data: Partial<CreateSignInExperience>
|
||||
): Promise<SignInExperience> => ({
|
||||
...mockSignInExperience,
|
||||
...data,
|
||||
}),
|
||||
}));
|
||||
|
||||
const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js'));
|
||||
const { createRequester } = await import('#src/utils/test-utils.js');
|
||||
const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes });
|
||||
|
||||
const expectPatchResponseStatus = async (
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { SignInExperience, CreateSignInExperience, TermsOfUse } from '@logto/schemas';
|
||||
import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import {
|
||||
mockFacebookConnector,
|
||||
|
@ -13,12 +14,23 @@ import {
|
|||
mockLanguageInfo,
|
||||
mockAliyunSmsConnector,
|
||||
} from '#src/__mocks__/index.js';
|
||||
import * as signInExpLib from '#src/lib/sign-in-experience/index.js';
|
||||
import * as signInLib from '#src/lib/sign-in-experience/sign-in.js';
|
||||
import * as signUpLib from '#src/lib/sign-in-experience/sign-up.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
import signInExperiencesRoutes from './sign-in-experience.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
const {
|
||||
validateBranding,
|
||||
validateLanguageInfo,
|
||||
validateTermsOfUse,
|
||||
validateSignIn,
|
||||
validateSignUp,
|
||||
} = await mockEsmWithActual('#src/lib/sign-in-experience/index.js', () => ({
|
||||
validateBranding: jest.fn(),
|
||||
validateLanguageInfo: jest.fn(),
|
||||
validateTermsOfUse: jest.fn(),
|
||||
validateSignIn: jest.fn(),
|
||||
validateSignUp: jest.fn(),
|
||||
}));
|
||||
|
||||
const logtoConnectors = [
|
||||
mockFacebookConnector,
|
||||
|
@ -28,33 +40,27 @@ const logtoConnectors = [
|
|||
mockAliyunSmsConnector,
|
||||
];
|
||||
|
||||
const getLogtoConnectors = jest.fn(async () => logtoConnectors);
|
||||
|
||||
jest.mock('#src/connectors.js', () => {
|
||||
return {
|
||||
...jest.requireActual('#src/connectors.js'),
|
||||
getLogtoConnectors: jest.fn(async () => getLogtoConnectors()),
|
||||
};
|
||||
});
|
||||
|
||||
const findDefaultSignInExperience = jest.fn(async () => mockSignInExperience);
|
||||
|
||||
jest.mock('#src/queries/sign-in-experience.js', () => ({
|
||||
findDefaultSignInExperience: jest.fn(async () => findDefaultSignInExperience()),
|
||||
updateDefaultSignInExperience: jest.fn(
|
||||
async (data: Partial<CreateSignInExperience>): Promise<SignInExperience> => ({
|
||||
...mockSignInExperience,
|
||||
...data,
|
||||
})
|
||||
),
|
||||
await mockEsmWithActual('#src/connectors.js', () => ({
|
||||
getLogtoConnectors: async () => logtoConnectors,
|
||||
}));
|
||||
|
||||
const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes });
|
||||
const { findDefaultSignInExperience } = mockEsm('#src/queries/sign-in-experience.js', () => ({
|
||||
findDefaultSignInExperience: jest.fn(async () => mockSignInExperience),
|
||||
updateDefaultSignInExperience: async (
|
||||
data: Partial<CreateSignInExperience>
|
||||
): Promise<SignInExperience> => ({
|
||||
...mockSignInExperience,
|
||||
...data,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/custom-phrase.js', () => ({
|
||||
mockEsm('#src/queries/custom-phrase.js', () => ({
|
||||
findAllCustomLanguageTags: async () => [],
|
||||
}));
|
||||
|
||||
const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js'));
|
||||
const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes });
|
||||
|
||||
describe('GET /sign-in-exp', () => {
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -103,12 +109,6 @@ describe('PATCH /sign-in-exp', () => {
|
|||
const termsOfUse: TermsOfUse = { enabled: false };
|
||||
const socialSignInConnectorTargets = ['github', 'facebook', 'wechat'];
|
||||
|
||||
const validateBranding = jest.spyOn(signInExpLib, 'validateBranding');
|
||||
const validateLanguageInfo = jest.spyOn(signInExpLib, 'validateLanguageInfo');
|
||||
const validateTermsOfUse = jest.spyOn(signInExpLib, 'validateTermsOfUse');
|
||||
const validateSignIn = jest.spyOn(signInLib, 'validateSignIn');
|
||||
const validateSignUp = jest.spyOn(signUpLib, 'validateSignUp');
|
||||
|
||||
const response = await signInExperienceRequester.patch('/sign-in-exp').send({
|
||||
color: mockColor,
|
||||
branding: mockBranding,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { load } from 'js-yaml';
|
||||
import { mockEsm } from '@logto/shared/esm';
|
||||
import Koa from 'koa';
|
||||
import Router from 'koa-router';
|
||||
import request from 'supertest';
|
||||
|
@ -8,12 +8,18 @@ import koaGuard from '#src/middleware/koa-guard.js';
|
|||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
import type { AnonymousRouter } from '#src/routes/types.js';
|
||||
|
||||
import swaggerRoutes, { defaultResponses, paginationParameters } from './swagger.js';
|
||||
const { jest } = import.meta;
|
||||
|
||||
jest.mock('js-yaml', () => ({
|
||||
const { load } = mockEsm('js-yaml', () => ({
|
||||
load: jest.fn().mockReturnValue({ paths: {} }),
|
||||
}));
|
||||
|
||||
const {
|
||||
default: swaggerRoutes,
|
||||
defaultResponses,
|
||||
paginationParameters,
|
||||
} = await import('./swagger.js');
|
||||
|
||||
export const createSwaggerRequest = (
|
||||
allRouters: Router[],
|
||||
swaggerRouter: AnonymousRouter = new Router()
|
||||
|
@ -221,7 +227,7 @@ describe('GET /swagger.json', () => {
|
|||
|
||||
describe('should use correct responses', () => {
|
||||
it('should use "defaultResponses" if there is no custom "responses" from the additional swagger', async () => {
|
||||
(load as jest.Mock).mockReturnValueOnce({
|
||||
load.mockReturnValueOnce({
|
||||
paths: { '/api/mock': { delete: {} } },
|
||||
});
|
||||
|
||||
|
@ -235,7 +241,7 @@ describe('GET /swagger.json', () => {
|
|||
});
|
||||
|
||||
it('should use custom "responses" from the additional swagger if it exists', async () => {
|
||||
(load as jest.Mock).mockReturnValueOnce({
|
||||
load.mockReturnValueOnce({
|
||||
paths: {
|
||||
'/api/mock': {
|
||||
get: {
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
adminConsoleApplicationId,
|
||||
adminConsoleSignInExperience,
|
||||
} from '@logto/schemas/lib/seeds/index.js';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import {
|
||||
mockAliyunDmConnector,
|
||||
|
@ -15,50 +15,48 @@ import {
|
|||
mockWechatConnector,
|
||||
mockWechatNativeConnector,
|
||||
} from '#src/__mocks__/index.js';
|
||||
import * as signInExperienceQueries from '#src/queries/sign-in-experience.js';
|
||||
import wellKnownRoutes from '#src/routes/well-known.js';
|
||||
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
const getLogtoConnectors = jest.fn(async () => [
|
||||
mockAliyunDmConnector,
|
||||
mockAliyunSmsConnector,
|
||||
mockFacebookConnector,
|
||||
mockGithubConnector,
|
||||
mockGoogleConnector,
|
||||
mockWechatConnector,
|
||||
mockWechatNativeConnector,
|
||||
]);
|
||||
|
||||
jest.mock('#src/connectors.js', () => ({
|
||||
getLogtoConnectors: async () => getLogtoConnectors(),
|
||||
const { jest } = import.meta;
|
||||
await mockEsmWithActual('i18next', () => ({
|
||||
default: {
|
||||
t: (key: string) => key,
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('#src/queries/user.js', () => ({
|
||||
const { findDefaultSignInExperience } = mockEsm('#src/queries/sign-in-experience.js', () => ({
|
||||
updateDefaultSignInExperience: jest.fn(),
|
||||
findDefaultSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience),
|
||||
}));
|
||||
|
||||
await mockEsmWithActual('#src/queries/user.js', () => ({
|
||||
hasActiveUsers: jest.fn().mockResolvedValue(true),
|
||||
}));
|
||||
|
||||
const interactionDetails: jest.MockedFunction<() => Promise<unknown>> = jest.fn(async () => ({
|
||||
params: {},
|
||||
mockEsm('#src/connectors.js', () => ({
|
||||
getLogtoConnectors: jest.fn(async () => [
|
||||
mockAliyunDmConnector,
|
||||
mockAliyunSmsConnector,
|
||||
mockFacebookConnector,
|
||||
mockGithubConnector,
|
||||
mockGoogleConnector,
|
||||
mockWechatConnector,
|
||||
mockWechatNativeConnector,
|
||||
]),
|
||||
}));
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('i18next', () => ({
|
||||
t: (key: string) => key,
|
||||
}));
|
||||
const wellKnownRoutes = await pickDefault(import('#src/routes/well-known.js'));
|
||||
|
||||
describe('GET /.well-known/sign-in-exp', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const provider = createMockProvider();
|
||||
const sessionRequest = createRequester({
|
||||
anonymousRoutes: wellKnownRoutes,
|
||||
provider: new Provider(''),
|
||||
provider,
|
||||
middlewares: [
|
||||
async (ctx, next) => {
|
||||
ctx.addLogContext = jest.fn();
|
||||
|
@ -69,13 +67,9 @@ describe('GET /.well-known/sign-in-exp', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const signInExperienceQuerySpyOn = jest
|
||||
.spyOn(signInExperienceQueries, 'findDefaultSignInExperience')
|
||||
.mockResolvedValue(mockSignInExperience);
|
||||
|
||||
it('should return github and facebook connector instances', async () => {
|
||||
const response = await sessionRequest.get('/.well-known/sign-in-exp');
|
||||
expect(signInExperienceQuerySpyOn).toHaveBeenCalledTimes(1);
|
||||
expect(findDefaultSignInExperience).toHaveBeenCalledTimes(1);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toMatchObject({
|
||||
...mockSignInExperience,
|
||||
|
@ -101,7 +95,10 @@ describe('GET /.well-known/sign-in-exp', () => {
|
|||
});
|
||||
|
||||
it('should return admin console settings', async () => {
|
||||
interactionDetails.mockResolvedValue({ params: { client_id: adminConsoleApplicationId } });
|
||||
jest
|
||||
.spyOn(provider, 'interactionDetails')
|
||||
// @ts-expect-error for testing
|
||||
.mockResolvedValue({ params: { client_id: adminConsoleApplicationId } });
|
||||
const response = await sessionRequest.get('/.well-known/sign-in-exp');
|
||||
expect(response.status).toEqual(200);
|
||||
|
||||
|
|
9
packages/core/src/test-utils/jest-koa-mocks/LICENSE
Normal file
9
packages/core/src/test-utils/jest-koa-mocks/LICENSE
Normal file
|
@ -0,0 +1,9 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018-present Shopify
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
3
packages/core/src/test-utils/jest-koa-mocks/README.md
Normal file
3
packages/core/src/test-utils/jest-koa-mocks/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# jest-koa-mocks
|
||||
|
||||
Updated from https://github.com/Shopify/quilt/tree/main/packages/jest-koa-mocks since ESM tests need explicit `const { jest } = import.meta;` to fetch the global jest object.
|
|
@ -0,0 +1,119 @@
|
|||
/* eslint-disable unicorn/no-abusive-eslint-disable */
|
||||
/* eslint-disable */
|
||||
import stream from 'stream';
|
||||
import { URL } from 'url';
|
||||
|
||||
import type { Context } from 'koa';
|
||||
import Koa from 'koa';
|
||||
import type { RequestMethod } from 'node-mocks-http';
|
||||
import httpMocks from 'node-mocks-http';
|
||||
|
||||
import type { MockCookies } from './create-mock-cookies.js';
|
||||
import createMockCookies from './create-mock-cookies.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
export type Dictionary<T> = Record<string, T>;
|
||||
|
||||
export type MockContext = {
|
||||
cookies: MockCookies;
|
||||
request: Context['request'] & {
|
||||
body?: any;
|
||||
rawBody?: string;
|
||||
session?: any;
|
||||
};
|
||||
} & Context;
|
||||
|
||||
export type Options<CustomProperties extends Record<string, unknown>, RequestBody = undefined> = {
|
||||
url?: string;
|
||||
method?: RequestMethod;
|
||||
statusCode?: number;
|
||||
session?: Dictionary<any>;
|
||||
headers?: Dictionary<string>;
|
||||
cookies?: Dictionary<string>;
|
||||
state?: Dictionary<any>;
|
||||
encrypted?: boolean;
|
||||
host?: string;
|
||||
requestBody?: RequestBody;
|
||||
rawBody?: string;
|
||||
throw?: Function;
|
||||
redirect?: Function;
|
||||
customProperties?: CustomProperties;
|
||||
};
|
||||
|
||||
export default function createContext<
|
||||
CustomProperties extends Record<string, unknown>,
|
||||
RequestBody = undefined
|
||||
>(options: Options<CustomProperties, RequestBody> = {}) {
|
||||
const app = new Koa();
|
||||
|
||||
const {
|
||||
cookies,
|
||||
method,
|
||||
statusCode,
|
||||
session,
|
||||
requestBody,
|
||||
rawBody = '',
|
||||
url = '',
|
||||
host = 'test.com',
|
||||
encrypted = false,
|
||||
throw: throwFunction = jest.fn(),
|
||||
redirect = jest.fn(),
|
||||
headers = {},
|
||||
state = {},
|
||||
customProperties = {},
|
||||
} = options;
|
||||
|
||||
const extensions = {
|
||||
...customProperties,
|
||||
throw: throwFunction,
|
||||
session,
|
||||
redirect,
|
||||
state,
|
||||
};
|
||||
|
||||
const protocolFallback = encrypted ? 'https' : 'http';
|
||||
const urlObject = new URL(url, `${protocolFallback}://${host}`);
|
||||
|
||||
const request = httpMocks.createRequest({
|
||||
url: urlObject.toString(),
|
||||
method,
|
||||
statusCode,
|
||||
session,
|
||||
headers: {
|
||||
// Koa determines protocol based on the `Host` header.
|
||||
Host: urlObject.host,
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
|
||||
// Some functions we call in the implementations will perform checks for `req.encrypted`, which delegates to the socket.
|
||||
// MockRequest doesn't set a fake socket itself, so we create one here.
|
||||
request.socket = new stream.Duplex() as any;
|
||||
Object.defineProperty(request.socket, 'encrypted', {
|
||||
writable: false,
|
||||
value: urlObject.protocol === 'https:',
|
||||
});
|
||||
|
||||
const res = httpMocks.createResponse();
|
||||
|
||||
// Koa sets a default status code of 404, not the node default of 200
|
||||
// https://github.com/koajs/koa/blob/master/docs/api/response.md#responsestatus
|
||||
res.statusCode = 404;
|
||||
|
||||
// This is to get around an odd behavior in the `cookies` library, where if `res.set` is defined, it will use an internal
|
||||
// node function to set headers, which results in them being set in the wrong place.
|
||||
|
||||
res.set = undefined as any;
|
||||
|
||||
const context = app.createContext(request, res) as MockContext & CustomProperties;
|
||||
Object.assign(context, extensions);
|
||||
context.cookies = createMockCookies(cookies);
|
||||
|
||||
// Ctx.request.body is a common enough custom property for middleware to add that it's handy to just support it by default
|
||||
context.request.body = requestBody;
|
||||
context.request.rawBody = rawBody;
|
||||
|
||||
return context as Context;
|
||||
}
|
||||
/* eslint-enable */
|
|
@ -0,0 +1,37 @@
|
|||
/* eslint-disable unicorn/no-abusive-eslint-disable */
|
||||
/* eslint-disable */
|
||||
import type { Context } from 'koa';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
export type Cookies = Context['cookies'];
|
||||
|
||||
export type Dictionary<T> = Record<string, T>;
|
||||
|
||||
export type MockCookies = {
|
||||
requestStore: Map<string, string>;
|
||||
responseStore: Map<string, string>;
|
||||
} & Cookies;
|
||||
|
||||
export default function createMockCookies(
|
||||
cookies: Record<string, unknown> = {},
|
||||
secure = true
|
||||
): MockCookies {
|
||||
const cookieEntries = Object.keys(cookies).map((key) => [key, cookies[key]] as [string, string]);
|
||||
|
||||
const requestStore = new Map<string, string>(cookieEntries);
|
||||
const responseStore = new Map<string, string>(cookieEntries);
|
||||
|
||||
return {
|
||||
set: jest.fn((key, value) => {
|
||||
return responseStore.set(key, value);
|
||||
}),
|
||||
get: jest.fn((key) => {
|
||||
return requestStore.get(key);
|
||||
}),
|
||||
requestStore,
|
||||
responseStore,
|
||||
secure,
|
||||
} as any;
|
||||
}
|
||||
/* eslint-enable */
|
24
packages/core/src/test-utils/oidc-provider.ts
Normal file
24
packages/core/src/test-utils/oidc-provider.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { Provider } from 'oidc-provider';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
export const createMockProvider = (interactionDetails?: jest.Mock): Provider => {
|
||||
const originalWarn = console.warn;
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation((...args) => {
|
||||
// Disable while creating. Too many warnings.
|
||||
if (typeof args[0] === 'string' && args[0].includes('oidc-provider')) {
|
||||
return;
|
||||
}
|
||||
|
||||
originalWarn(...args);
|
||||
});
|
||||
const provider = new Provider('https://logto.test');
|
||||
|
||||
warn.mockRestore();
|
||||
jest.spyOn(provider, 'interactionDetails').mockImplementation(
|
||||
// @ts-expect-error for testing
|
||||
interactionDetails ?? (async () => ({ params: {}, jti: 'jti', client_id: 'mockApplicationId' }))
|
||||
);
|
||||
|
||||
return provider;
|
||||
};
|
|
@ -9,36 +9,26 @@ import {
|
|||
} from '#src/utils/oidc-provider-event-listener.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const userId = 'userIdValue';
|
||||
const sessionId = 'sessionIdValue';
|
||||
const applicationId = 'applicationIdValue';
|
||||
|
||||
const addLogContext = jest.fn();
|
||||
const log = jest.fn();
|
||||
const addListener = jest.fn();
|
||||
|
||||
jest.mock('oidc-provider', () => ({ Provider: jest.fn(() => ({ addListener })) }));
|
||||
|
||||
describe('addOidcEventListeners', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should add grantSuccessListener', () => {
|
||||
const provider = new Provider('');
|
||||
it('should add proper listeners', () => {
|
||||
const provider = new Provider('https://logto.test');
|
||||
const addListener = jest.spyOn(provider, 'addListener');
|
||||
addOidcEventListeners(provider);
|
||||
expect(addListener).toHaveBeenCalledWith('grant.success', grantSuccessListener);
|
||||
});
|
||||
|
||||
it('should add grantErrorListener', () => {
|
||||
const provider = new Provider('');
|
||||
addOidcEventListeners(provider);
|
||||
expect(addListener).toHaveBeenCalledWith('grant.error', grantErrorListener);
|
||||
});
|
||||
|
||||
it('should add grantRevokedListener', () => {
|
||||
const provider = new Provider('');
|
||||
addOidcEventListeners(provider);
|
||||
expect(addListener).toHaveBeenCalledWith('grant.revoked', grantRevokedListener);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import type { Options } from '@shopify/jest-koa-mocks';
|
||||
import { createMockContext } from '@shopify/jest-koa-mocks';
|
||||
import type { MiddlewareType, Context, Middleware } from 'koa';
|
||||
import Koa from 'koa';
|
||||
import type { IRouterParamContext } from 'koa-router';
|
||||
|
@ -11,6 +9,8 @@ import type { PrimitiveValueExpression } from 'slonik/dist/src/types.js';
|
|||
import request from 'supertest';
|
||||
|
||||
import type { AuthedRouter, AnonymousRouter } from '#src/routes/types.js';
|
||||
import type { Options } from '#src/test-utils/jest-koa-mocks/create-mock-context.js';
|
||||
import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js';
|
||||
|
||||
/**
|
||||
* Slonik Query Mock Utils
|
||||
|
|
|
@ -8,6 +8,14 @@
|
|||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./lib/index.js"
|
||||
},
|
||||
"./esm": {
|
||||
"import": "./lib/esm/index.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
|
2
packages/shared/src/esm/index.ts
Normal file
2
packages/shared/src/esm/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default as moduleProxy } from './module-proxy.js';
|
||||
export * from './mock-esm.js';
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue