mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +08:00
test: add backend unit tests (#4000)
* test: add backend unit tests * test: cas * test: oidc & saml * test: sql collection * fix: test files * test: data-visualization * test: localization * fix: test
This commit is contained in:
parent
769de9a69e
commit
8b88b29b5e
@ -1,8 +1,6 @@
|
||||
import { vi } from 'vitest';
|
||||
import { Context } from '@nocobase/actions';
|
||||
import { Auth, AuthManager } from '@nocobase/auth';
|
||||
import Database, { Model } from '@nocobase/database';
|
||||
import { MockServer, mockServer } from '@nocobase/test';
|
||||
import { Model } from '@nocobase/database';
|
||||
|
||||
class MockStorer {
|
||||
elements: Map<string, any> = new Map();
|
||||
@ -29,12 +27,13 @@ class BasicAuth extends Auth {
|
||||
|
||||
describe('auth-manager', () => {
|
||||
let authManager: AuthManager;
|
||||
let storer: MockStorer;
|
||||
beforeEach(() => {
|
||||
authManager = new AuthManager({
|
||||
authKey: 'X-Authenticator',
|
||||
});
|
||||
|
||||
const storer = new MockStorer();
|
||||
storer = new MockStorer();
|
||||
const authenticator = {
|
||||
name: 'basic-test',
|
||||
authType: 'basic',
|
||||
@ -48,70 +47,25 @@ describe('auth-manager', () => {
|
||||
authManager.registerTypes('basic', { auth: BasicAuth });
|
||||
const authenticator = await authManager.get('basic-test', {} as Context);
|
||||
expect(authenticator).toBeInstanceOf(BasicAuth);
|
||||
|
||||
storer.set('basic2-test', { name: 'basic2-test', authType: 'basic2', options: {} });
|
||||
expect(authManager.get('basic2-test', {} as Context)).rejects.toThrowError('AuthType [basic2] is not found.');
|
||||
|
||||
await expect(authManager.get('not-exists', {} as Context)).rejects.toThrowError(
|
||||
'Authenticator [not-exists] is not found.',
|
||||
);
|
||||
|
||||
authManager.setStorer(null);
|
||||
await expect(authManager.get('any', {} as Context)).rejects.toThrowError('AuthManager.storer is not set.');
|
||||
});
|
||||
|
||||
describe('middleware', () => {
|
||||
let app: MockServer;
|
||||
let db: Database;
|
||||
let agent;
|
||||
it('should list types', () => {
|
||||
authManager.registerTypes('basic', { auth: BasicAuth, title: 'Basic' });
|
||||
expect(authManager.listTypes()).toEqual([{ name: 'basic', title: 'Basic' }]);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer({
|
||||
registerActions: true,
|
||||
acl: true,
|
||||
plugins: ['users', 'auth', 'acl', 'data-source-manager'],
|
||||
});
|
||||
|
||||
// app.plugin(ApiKeysPlugin);
|
||||
await app.loadAndInstall({ clean: true });
|
||||
db = app.db;
|
||||
agent = app.agent();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
describe('blacklist', () => {
|
||||
const hasFn = vi.fn();
|
||||
const addFn = vi.fn();
|
||||
beforeEach(async () => {
|
||||
await agent.login(1);
|
||||
app.authManager.setTokenBlacklistService({
|
||||
has: hasFn,
|
||||
add: addFn,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
hasFn.mockReset();
|
||||
addFn.mockReset();
|
||||
});
|
||||
|
||||
it('basic', async () => {
|
||||
const res = await agent.resource('auth').check();
|
||||
const token = res.request.header['Authorization'].replace('Bearer ', '');
|
||||
expect(res.status).toBe(200);
|
||||
expect(hasFn).toHaveBeenCalledWith(token);
|
||||
});
|
||||
|
||||
it('signOut should add token to blacklist', async () => {
|
||||
// signOut will add token
|
||||
const res = await agent.resource('auth').signOut();
|
||||
const token = res.request.header['Authorization'].replace('Bearer ', '');
|
||||
expect(addFn).toHaveBeenCalledWith({
|
||||
token,
|
||||
// Date or String is ok
|
||||
expiration: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw 401 when token in blacklist', async () => {
|
||||
hasFn.mockImplementation(() => true);
|
||||
const res = await agent.resource('auth').check();
|
||||
expect(res.status).toBe(401);
|
||||
expect(res.text).toContain('token is not available');
|
||||
});
|
||||
});
|
||||
it('should get auth config', () => {
|
||||
authManager.registerTypes('basic', { auth: BasicAuth, title: 'Basic' });
|
||||
expect(authManager.getAuthConfig('basic')).toEqual({ auth: BasicAuth, title: 'Basic' });
|
||||
});
|
||||
});
|
||||
|
135
packages/core/auth/src/__tests__/base-auth.test.ts
Normal file
135
packages/core/auth/src/__tests__/base-auth.test.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { BaseAuth } from '../base/auth';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
describe('base-auth', () => {
|
||||
it('should validate username', () => {
|
||||
const auth = new BaseAuth({
|
||||
userCollection: {},
|
||||
} as any);
|
||||
|
||||
expect(auth.validateUsername('')).toBe(false);
|
||||
expect(auth.validateUsername('a')).toBe(false);
|
||||
expect(auth.validateUsername('a@')).toBe(false);
|
||||
expect(auth.validateUsername('a.')).toBe(false);
|
||||
expect(auth.validateUsername('a<')).toBe(false);
|
||||
expect(auth.validateUsername('a>')).toBe(false);
|
||||
expect(auth.validateUsername('a"')).toBe(false);
|
||||
expect(auth.validateUsername('a/')).toBe(false);
|
||||
expect(auth.validateUsername("a'")).toBe(false);
|
||||
expect(auth.validateUsername('ab')).toBe(true);
|
||||
// 16 characters
|
||||
expect(auth.validateUsername('12345678910111213')).toBe(false);
|
||||
});
|
||||
|
||||
it('check: should return null when no token', async () => {
|
||||
const auth = new BaseAuth({
|
||||
userCollection: {},
|
||||
ctx: {
|
||||
getBearerToken: () => null,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(await auth.check()).toBe(null);
|
||||
});
|
||||
|
||||
it('check: should set roleName to headers', async () => {
|
||||
const ctx = {
|
||||
getBearerToken: () => 'token',
|
||||
headers: {},
|
||||
logger: {
|
||||
error: (...args) => console.log(args),
|
||||
},
|
||||
app: {
|
||||
authManager: {
|
||||
jwt: {
|
||||
decode: () => ({ userId: 1, roleName: 'admin' }),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const auth = new BaseAuth({
|
||||
ctx,
|
||||
userCollection: {
|
||||
repository: {
|
||||
findOne: () => ({ id: 1 }),
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
await auth.check();
|
||||
expect(ctx.headers['x-role']).toBe('admin');
|
||||
});
|
||||
|
||||
it('check: should return user', async () => {
|
||||
const ctx = {
|
||||
getBearerToken: () => 'token',
|
||||
headers: {},
|
||||
logger: {
|
||||
error: (...args) => console.log(args),
|
||||
},
|
||||
app: {
|
||||
authManager: {
|
||||
jwt: {
|
||||
decode: () => ({ userId: 1, roleName: 'admin' }),
|
||||
},
|
||||
},
|
||||
},
|
||||
cache: {
|
||||
wrap: async (key, fn) => fn(),
|
||||
},
|
||||
};
|
||||
const auth = new BaseAuth({
|
||||
ctx,
|
||||
userCollection: {
|
||||
repository: {
|
||||
findOne: () => ({ id: 1 }),
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
expect(await auth.check()).toEqual({ id: 1 });
|
||||
});
|
||||
|
||||
it('signIn: should throw 401', async () => {
|
||||
const ctx = {
|
||||
throw: vi.fn().mockImplementation((status, message) => {
|
||||
throw new Error(message);
|
||||
}),
|
||||
};
|
||||
|
||||
const auth = new BaseAuth({
|
||||
userCollection: {},
|
||||
ctx,
|
||||
} as any);
|
||||
await expect(auth.signIn()).rejects.toThrowError('Unauthorized');
|
||||
});
|
||||
|
||||
it('signIn: should return user and token', async () => {
|
||||
class TestAuth extends BaseAuth {
|
||||
async validate() {
|
||||
return { id: 1 } as any;
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = {
|
||||
throw: vi.fn().mockImplementation((status, message) => {
|
||||
throw new Error(message);
|
||||
}),
|
||||
app: {
|
||||
authManager: {
|
||||
jwt: {
|
||||
sign: () => 'token',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const auth = new TestAuth({
|
||||
userCollection: {},
|
||||
ctx,
|
||||
} as any);
|
||||
|
||||
const res = await auth.signIn();
|
||||
expect(res.token).toBe('token');
|
||||
expect(res.user).toEqual({ id: 1 });
|
||||
});
|
||||
});
|
68
packages/core/auth/src/__tests__/middleware.test.ts
Normal file
68
packages/core/auth/src/__tests__/middleware.test.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { Database } from '@nocobase/database';
|
||||
import { MockServer, mockServer } from '@nocobase/test';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
describe('middleware', () => {
|
||||
let app: MockServer;
|
||||
let db: Database;
|
||||
let agent;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer({
|
||||
registerActions: true,
|
||||
acl: true,
|
||||
plugins: ['users', 'auth', 'acl', 'data-source-manager'],
|
||||
});
|
||||
|
||||
// app.plugin(ApiKeysPlugin);
|
||||
await app.loadAndInstall({ clean: true });
|
||||
db = app.db;
|
||||
agent = app.agent();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
describe('blacklist', () => {
|
||||
const hasFn = vi.fn();
|
||||
const addFn = vi.fn();
|
||||
beforeEach(async () => {
|
||||
await agent.login(1);
|
||||
app.authManager.setTokenBlacklistService({
|
||||
has: hasFn,
|
||||
add: addFn,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
hasFn.mockReset();
|
||||
addFn.mockReset();
|
||||
});
|
||||
|
||||
it('basic', async () => {
|
||||
const res = await agent.resource('auth').check();
|
||||
const token = res.request.header['Authorization'].replace('Bearer ', '');
|
||||
expect(res.status).toBe(200);
|
||||
expect(hasFn).toHaveBeenCalledWith(token);
|
||||
});
|
||||
|
||||
it('signOut should add token to blacklist', async () => {
|
||||
// signOut will add token
|
||||
const res = await agent.resource('auth').signOut();
|
||||
const token = res.request.header['Authorization'].replace('Bearer ', '');
|
||||
expect(addFn).toHaveBeenCalledWith({
|
||||
token,
|
||||
// Date or String is ok
|
||||
expiration: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw 401 when token in blacklist', async () => {
|
||||
hasFn.mockImplementation(() => true);
|
||||
const res = await agent.resource('auth').check();
|
||||
expect(res.status).toBe(401);
|
||||
expect(res.text).toContain('token is not available');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,3 +1,4 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
import { Handlers } from '@nocobase/resourcer';
|
||||
|
||||
export const actions = {
|
||||
|
@ -85,9 +85,9 @@ export class AuthManager {
|
||||
if (!authenticator) {
|
||||
throw new Error(`Authenticator [${name}] is not found.`);
|
||||
}
|
||||
const { auth } = this.authTypes.get(authenticator.authType);
|
||||
const { auth } = this.authTypes.get(authenticator.authType) || {};
|
||||
if (!auth) {
|
||||
throw new Error(`AuthType [${name}] is not found.`);
|
||||
throw new Error(`AuthType [${authenticator.authType}] is not found.`);
|
||||
}
|
||||
return new auth({ authenticator, options: authenticator.options, ctx });
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ export class BaseAuth extends Auth {
|
||||
try {
|
||||
user = await this.validate();
|
||||
} catch (err) {
|
||||
this.ctx.throw(401, err.message);
|
||||
this.ctx.throw(err.status || 401, err.message);
|
||||
}
|
||||
if (!user) {
|
||||
this.ctx.throw(401, 'Unauthorized');
|
||||
|
@ -31,6 +31,7 @@ export class JwtService {
|
||||
return this.options.secret;
|
||||
}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
sign(payload: SignPayload, options?: SignOptions) {
|
||||
const opt = { expiresIn: this.expiresIn(), ...options };
|
||||
if (opt.expiresIn === 'never') {
|
||||
@ -39,6 +40,7 @@ export class JwtService {
|
||||
return jwt.sign(payload, this.secret(), opt);
|
||||
}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
decode(token: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, this.secret(), (err: any, decoded: any) => {
|
||||
|
@ -21,4 +21,14 @@ describe('bloomFilter', () => {
|
||||
expect(await bloomFilter.exists('bloom-test', 'hello')).toBeTruthy();
|
||||
expect(await bloomFilter.exists('bloom-test', 'world')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should mAdd and check', async () => {
|
||||
await bloomFilter.mAdd('bloom-test', ['hello', 'world']);
|
||||
expect(await bloomFilter.exists('bloom-test', 'hello')).toBeTruthy();
|
||||
expect(await bloomFilter.exists('bloom-test', 'world')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false if not reserved', async () => {
|
||||
expect(await bloomFilter.exists('not-reserved', 'hello')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
49
packages/core/cache/src/__tests__/cache.test.ts
vendored
49
packages/core/cache/src/__tests__/cache.test.ts
vendored
@ -21,7 +21,44 @@ describe('cache', () => {
|
||||
expect(value).toBe('value');
|
||||
});
|
||||
|
||||
it('set and get value in object', async () => {
|
||||
it('should del value', async () => {
|
||||
await cache.set('key', 'value');
|
||||
await cache.del('key');
|
||||
const value = await cache.get('key');
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should mset and mget values', async () => {
|
||||
await cache.mset([
|
||||
['key1', 'value1'],
|
||||
['key2', 'value2'],
|
||||
]);
|
||||
const values = await cache.mget('key1', 'key2');
|
||||
expect(values).toMatchObject(['value1', 'value2']);
|
||||
});
|
||||
|
||||
it('should mdel values', async () => {
|
||||
await cache.mset([
|
||||
['key1', 'value1'],
|
||||
['key2', 'value2'],
|
||||
]);
|
||||
await cache.mdel('key1', 'key2');
|
||||
const values = await cache.mget('key1', 'key2');
|
||||
expect(values).toMatchObject([undefined, undefined]);
|
||||
});
|
||||
|
||||
it('should get all keys', async () => {
|
||||
await cache.mset([
|
||||
[`key1`, 'value1'],
|
||||
[`key2`, 'value2'],
|
||||
]);
|
||||
const keys = await cache.keys();
|
||||
expect(keys.length).toBe(2);
|
||||
expect(keys).toContain('key1');
|
||||
expect(keys).toContain('key2');
|
||||
});
|
||||
|
||||
it('should set and get value in object', async () => {
|
||||
const value = { a: 1 };
|
||||
await cache.set('key', value);
|
||||
const cacheA = await cache.getValueInObject('key', 'a');
|
||||
@ -32,6 +69,16 @@ describe('cache', () => {
|
||||
expect(cacheVal2).toEqual(2);
|
||||
});
|
||||
|
||||
it('should del value in object', async () => {
|
||||
const value = { a: 1, b: 2 };
|
||||
await cache.set('key', value);
|
||||
await cache.delValueInObject('key', 'a');
|
||||
const cacheA = await cache.getValueInObject('key', 'a');
|
||||
expect(cacheA).toBeUndefined();
|
||||
const cacheB = await cache.getValueInObject('key', 'b');
|
||||
expect(cacheB).toEqual(2);
|
||||
});
|
||||
|
||||
it('wrap with condition, useCache', async () => {
|
||||
const obj = {};
|
||||
const get = () => obj;
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
import { RedisStore } from 'cache-manager-redis-yet';
|
||||
import { BloomFilter } from '.';
|
||||
import { Cache } from '../cache';
|
||||
|
@ -42,6 +42,21 @@ describe('infer fields', () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('should infer for select *', async () => {
|
||||
const model = class extends SQLModel {};
|
||||
model.init(null, {
|
||||
modelName: 'users',
|
||||
tableName: 'users',
|
||||
sequelize: db.sequelize,
|
||||
});
|
||||
model.database = db;
|
||||
model.sql = `select * from users`;
|
||||
expect(model.inferFields()).toMatchObject({
|
||||
id: { type: 'bigInt', source: 'users.id' },
|
||||
nickname: { type: 'string', source: 'users.nickname' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should infer fields', async () => {
|
||||
const model = class extends SQLModel {};
|
||||
model.init(null, {
|
||||
@ -60,4 +75,23 @@ left join roles r on ru.role_name=r.name`;
|
||||
name: { type: 'string', source: 'roles.name' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should infer fields for with statement', async () => {
|
||||
const model = class extends SQLModel {};
|
||||
model.init(null, {
|
||||
modelName: 'test',
|
||||
tableName: 'test',
|
||||
sequelize: db.sequelize,
|
||||
});
|
||||
model.database = db;
|
||||
model.sql = `with u as (select id, nickname from users),
|
||||
r as (select id, title, name from roles)
|
||||
select u.id as uid, u.nickname, r.title, r.name`;
|
||||
expect(model.inferFields()).toMatchObject({
|
||||
uid: { type: 'bigInt', source: 'users.id' },
|
||||
nickname: { type: 'string', source: 'users.nickname' },
|
||||
title: { type: 'string', source: 'roles.title' },
|
||||
name: { type: 'string', source: 'roles.name' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -37,8 +37,10 @@ describe('select query', () => {
|
||||
});
|
||||
|
||||
test('group', () => {
|
||||
const query = queryGenerator.selectQuery('users', { group: 'id' }, model);
|
||||
expect(query).toBe('SELECT * FROM (SELECT * FROM "users") AS "users" GROUP BY "id";');
|
||||
const query1 = queryGenerator.selectQuery('users', { group: 'id' }, model);
|
||||
expect(query1).toBe('SELECT * FROM (SELECT * FROM "users") AS "users" GROUP BY "id";');
|
||||
const query2 = queryGenerator.selectQuery('users', { group: ['id', 'name'] }, model);
|
||||
expect(query2).toBe('SELECT * FROM (SELECT * FROM "users") AS "users" GROUP BY "id", "name";');
|
||||
});
|
||||
|
||||
test('order', () => {
|
||||
|
@ -0,0 +1,20 @@
|
||||
import { mockDatabase } from '../../mock-database';
|
||||
import { SqlCollection } from '../sql-collection';
|
||||
|
||||
test('sql-collection', async () => {
|
||||
const db = mockDatabase({ tablePrefix: '' });
|
||||
await db.clean({ drop: true });
|
||||
const collection = db.collectionFactory.createCollection<SqlCollection>({
|
||||
name: 'test',
|
||||
sql: true,
|
||||
});
|
||||
expect(collection.isSql()).toBe(true);
|
||||
expect(collection.collectionSchema()).toBeUndefined();
|
||||
expect(collection.options.autoGenId).toBe(false);
|
||||
expect(collection.options.timestamps).toBe(false);
|
||||
expect(collection.options.underscored).toBe(false);
|
||||
|
||||
collection.modelInit();
|
||||
// @ts-ignore
|
||||
expect(collection.model._schema).toBeUndefined();
|
||||
});
|
@ -18,6 +18,7 @@ export class SqlCollection extends Collection {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
get filterTargetKey() {
|
||||
const targetKey = this.options?.filterTargetKey || 'id';
|
||||
if (targetKey && this.model.getAttributes()[targetKey]) {
|
||||
|
@ -36,26 +36,38 @@ export class SQLModel extends Model {
|
||||
if (Array.isArray(ast)) {
|
||||
ast = ast[0];
|
||||
}
|
||||
ast.from = ast.from || [];
|
||||
ast.columns = ast.columns || [];
|
||||
if (ast.with) {
|
||||
ast.with.forEach((withItem: any) => {
|
||||
const as = withItem.name;
|
||||
const withAst = withItem.stmt.ast;
|
||||
ast.from.push(...withAst.from.map((f: any) => ({ ...f, as })));
|
||||
ast.columns.push(
|
||||
...withAst.columns.map((c: any) => ({
|
||||
...c,
|
||||
expr: {
|
||||
...c.expr,
|
||||
table: as,
|
||||
},
|
||||
})),
|
||||
);
|
||||
});
|
||||
}
|
||||
if (ast.columns === '*') {
|
||||
const tables = new Set<string>();
|
||||
// parse sql includes with clause is not accurate
|
||||
// the parsed columns seems to be always '*'
|
||||
// it is supposed to be improved in the future
|
||||
ast.with.forEach((withItem: { tableList: string[] }) => {
|
||||
const tableList = withItem.tableList;
|
||||
tableList.forEach((table) => {
|
||||
const name = table.split('::')[2]; // "select::null::users"
|
||||
tables.add(name);
|
||||
});
|
||||
ast.from.forEach((fromItem: { table: string; as: string }) => {
|
||||
tables.add(fromItem.table);
|
||||
});
|
||||
return Array.from(tables).map((table) => ({ table, columns: '*' }));
|
||||
}
|
||||
if (ast.columns === '*') {
|
||||
return ast.from.map((fromItem: { table: string; as: string }) => ({
|
||||
table: fromItem.table,
|
||||
columns: '*',
|
||||
}));
|
||||
}
|
||||
const tableAliases = {};
|
||||
ast.from.forEach((fromItem: { table: string; as: string }) => {
|
||||
if (!fromItem.as) {
|
||||
return;
|
||||
}
|
||||
tableAliases[fromItem.as] = fromItem.table;
|
||||
});
|
||||
const columns: string[] = ast.columns.reduce(
|
||||
(
|
||||
tableMp: { [table: string]: { name: string; as: string }[] },
|
||||
@ -72,22 +84,17 @@ export class SQLModel extends Model {
|
||||
return tableMp;
|
||||
}
|
||||
const table = column.expr.table;
|
||||
const name = tableAliases[table] || table;
|
||||
const columnAttr = { name: column.expr.column, as: column.as };
|
||||
if (!tableMp[table]) {
|
||||
tableMp[table] = [columnAttr];
|
||||
if (!tableMp[name]) {
|
||||
tableMp[name] = [columnAttr];
|
||||
} else {
|
||||
tableMp[table].push(columnAttr);
|
||||
tableMp[name].push(columnAttr);
|
||||
}
|
||||
return tableMp;
|
||||
},
|
||||
{},
|
||||
);
|
||||
ast.from.forEach((fromItem: { table: string; as: string }) => {
|
||||
if (columns[fromItem.as]) {
|
||||
columns[fromItem.table] = columns[fromItem.as];
|
||||
columns[fromItem.as] = undefined;
|
||||
}
|
||||
});
|
||||
return Object.entries(columns)
|
||||
.filter(([_, columns]) => columns)
|
||||
.map(([table, columns]) => ({ table, columns }));
|
||||
|
@ -16,6 +16,7 @@
|
||||
"Please enter a valid username": "Please enter a valid username",
|
||||
"Please enter a valid email": "Please enter a valid email",
|
||||
"Please enter your username or email": "Please enter your username or email",
|
||||
"Please enter a password": "Please enter a password",
|
||||
"SMS": "SMS",
|
||||
"Username/Email": "Username/Email",
|
||||
"Auth UID": "Auth UID",
|
||||
|
@ -16,6 +16,7 @@
|
||||
"Please enter a valid username": "请输入有效的用户名",
|
||||
"Please enter a valid email": "请输入有效的邮箱",
|
||||
"Please enter your username or email": "请输入用户名或邮箱",
|
||||
"Please enter a password": "请输入密码",
|
||||
"SMS": "短信",
|
||||
"Username/Email": "用户名/邮箱",
|
||||
"Auth UID": "认证标识",
|
||||
|
@ -97,6 +97,29 @@ describe('actions', () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should check parameters when signing in', async () => {
|
||||
const res = await agent.post('/auth:signIn').set({ 'X-Authenticator': 'basic' }).send({});
|
||||
expect(res.statusCode).toEqual(400);
|
||||
expect(res.error.text).toBe('Please enter your username or email');
|
||||
});
|
||||
|
||||
it('should check user when signing in', async () => {
|
||||
const res = await agent.post('/auth:signIn').set({ 'X-Authenticator': 'basic' }).send({
|
||||
email: 'no-exists@nocobase.com',
|
||||
});
|
||||
expect(res.statusCode).toEqual(401);
|
||||
expect(res.error.text).toBe('The username or email is incorrect, please re-enter');
|
||||
});
|
||||
|
||||
it('should check password when signing in', async () => {
|
||||
const res = await agent.post('/auth:signIn').set({ 'X-Authenticator': 'basic' }).send({
|
||||
email: process.env.INIT_ROOT_EMAIL,
|
||||
password: 'incorrect',
|
||||
});
|
||||
expect(res.statusCode).toEqual(401);
|
||||
expect(res.error.text).toBe('The password is incorrect, please re-enter');
|
||||
});
|
||||
|
||||
it('should sign in with password', async () => {
|
||||
let res = await agent.resource('auth').check();
|
||||
expect(res.body.data.id).toBeUndefined();
|
||||
@ -175,35 +198,72 @@ describe('actions', () => {
|
||||
password: '12345',
|
||||
},
|
||||
});
|
||||
const res = await agent.login(user).post('/auth:changePassword').set({ 'X-Authenticator': 'basic' }).send({
|
||||
const userAgent = await agent.login(user);
|
||||
|
||||
// Should check password consistency
|
||||
const res = await userAgent.post('/auth:changePassword').set({ 'X-Authenticator': 'basic' }).send({
|
||||
oldPassword: '12345',
|
||||
newPassword: '123456',
|
||||
confirmPassword: '1234567',
|
||||
});
|
||||
expect(res.statusCode).toEqual(400);
|
||||
expect(res.error.text).toBe('The password is inconsistent, please re-enter');
|
||||
|
||||
// Should check old password
|
||||
const res1 = await userAgent.post('/auth:changePassword').set({ 'X-Authenticator': 'basic' }).send({
|
||||
oldPassword: '1',
|
||||
newPassword: '123456',
|
||||
confirmPassword: '123456',
|
||||
});
|
||||
expect(res.statusCode).toEqual(200);
|
||||
expect(res1.statusCode).toEqual(401);
|
||||
expect(res1.error.text).toBe('The password is incorrect, please re-enter');
|
||||
|
||||
// Create a user without username
|
||||
const user1 = await userRepo.create({
|
||||
values: {
|
||||
username: 'test2',
|
||||
password: '12345',
|
||||
},
|
||||
});
|
||||
const res2 = await agent.login(user1).post('/auth:changePassword').set({ 'X-Authenticator': 'basic' }).send({
|
||||
const res2 = await userAgent.post('/auth:changePassword').set({ 'X-Authenticator': 'basic' }).send({
|
||||
oldPassword: '12345',
|
||||
newPassword: '123456',
|
||||
confirmPassword: '123456',
|
||||
});
|
||||
expect(res2.statusCode).toEqual(200);
|
||||
|
||||
// Create a user without username
|
||||
const user1 = await userRepo.create({
|
||||
values: {
|
||||
email: 'test3@nocobase.com',
|
||||
password: '12345',
|
||||
},
|
||||
});
|
||||
const res3 = await agent.login(user1).post('/auth:changePassword').set({ 'X-Authenticator': 'basic' }).send({
|
||||
oldPassword: '12345',
|
||||
newPassword: '123456',
|
||||
confirmPassword: '123456',
|
||||
});
|
||||
expect(res3.statusCode).toEqual(200);
|
||||
});
|
||||
|
||||
it('should check confirm password', async () => {
|
||||
it('should check confirm password when signing up', async () => {
|
||||
const res = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({
|
||||
username: 'new',
|
||||
password: 'new',
|
||||
confirm_password: 'new1',
|
||||
});
|
||||
expect(res.statusCode).toEqual(400);
|
||||
expect(res.error.text).toBe('The password is inconsistent, please re-enter');
|
||||
});
|
||||
|
||||
it('should check username when signing up', async () => {
|
||||
const res = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({
|
||||
username: '@@',
|
||||
});
|
||||
expect(res.statusCode).toEqual(400);
|
||||
expect(res.error.text).toBe('Please enter a valid username');
|
||||
});
|
||||
|
||||
it('should check password when signing up', async () => {
|
||||
const res = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({
|
||||
username: 'new',
|
||||
});
|
||||
expect(res.statusCode).toEqual(400);
|
||||
expect(res.error.text).toBe('Please enter a password');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,105 @@
|
||||
import { MockServer, createMockServer } from '@nocobase/test';
|
||||
import { AuthModel } from '../model/authenticator';
|
||||
import { Database, Repository } from '@nocobase/database';
|
||||
|
||||
describe('AuthModel', () => {
|
||||
let app: MockServer;
|
||||
let db: Database;
|
||||
let repo: Repository;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createMockServer({
|
||||
plugins: ['auth', 'users'],
|
||||
});
|
||||
db = app.db;
|
||||
repo = db.getRepository('authenticators');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.db.clean({ drop: true });
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should new user', async () => {
|
||||
const emitSpy = vi.spyOn(db, 'emitAsync');
|
||||
const authenticator = (await repo.create({
|
||||
values: {
|
||||
name: 'test',
|
||||
authType: 'testType',
|
||||
},
|
||||
})) as AuthModel;
|
||||
const user = await authenticator.newUser('uuid1', {
|
||||
username: 'test',
|
||||
});
|
||||
expect(emitSpy).toHaveBeenCalledWith('users.afterCreateWithAssociations', user, expect.any(Object));
|
||||
const res = await repo.findOne({
|
||||
filterByTk: authenticator.id,
|
||||
appends: ['users'],
|
||||
});
|
||||
expect(res.users.length).toBe(1);
|
||||
expect(res.users[0].username).toBe('test');
|
||||
});
|
||||
|
||||
it('should new user without userValues', async () => {
|
||||
const authenticator = (await repo.create({
|
||||
values: {
|
||||
name: 'test',
|
||||
authType: 'testType',
|
||||
},
|
||||
})) as AuthModel;
|
||||
await authenticator.newUser('uuid1');
|
||||
const res = await repo.findOne({
|
||||
filterByTk: authenticator.id,
|
||||
appends: ['users'],
|
||||
});
|
||||
expect(res.users.length).toBe(1);
|
||||
expect(res.users[0].nickname).toBe('uuid1');
|
||||
});
|
||||
|
||||
it('should find user', async () => {
|
||||
const authenticator = (await repo.create({
|
||||
values: {
|
||||
name: 'test',
|
||||
authType: 'testType',
|
||||
},
|
||||
})) as AuthModel;
|
||||
const user = await authenticator.newUser('uuid1', {
|
||||
username: 'test',
|
||||
});
|
||||
const res = await authenticator.findUser('uuid1');
|
||||
expect(res).toBeDefined();
|
||||
expect(res.id).toBe(user.id);
|
||||
});
|
||||
|
||||
it('should find or create user', async () => {
|
||||
const authenticator = (await repo.create({
|
||||
values: {
|
||||
name: 'test',
|
||||
authType: 'testType',
|
||||
},
|
||||
})) as AuthModel;
|
||||
// find user
|
||||
let user1 = await authenticator.findUser('uuid1');
|
||||
expect(user1).toBeUndefined();
|
||||
user1 = await authenticator.newUser('uuid1', {
|
||||
username: 'test',
|
||||
});
|
||||
const res1 = await authenticator.findOrCreateUser('uuid1', {
|
||||
username: 'test1',
|
||||
});
|
||||
expect(res1).toBeDefined();
|
||||
expect(res1.username).toBe('test');
|
||||
expect(res1.id).toBe(user1.id);
|
||||
|
||||
// create user
|
||||
let user2 = await authenticator.findUser('uuid2');
|
||||
expect(user2).toBeUndefined();
|
||||
user2 = await authenticator.findOrCreateUser('uuid2', {
|
||||
username: 'test2',
|
||||
});
|
||||
const res2 = await authenticator.findUser('uuid2');
|
||||
expect(res2).toBeDefined();
|
||||
expect(res2.username).toBe('test2');
|
||||
expect(res2.id).toBe(user2.id);
|
||||
});
|
||||
});
|
@ -1,3 +1,4 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
import { Context, Next } from '@nocobase/actions';
|
||||
|
||||
export default {
|
||||
|
@ -12,12 +12,10 @@ export class BasicAuth extends BaseAuth {
|
||||
async validate() {
|
||||
const ctx = this.ctx;
|
||||
const {
|
||||
values: {
|
||||
account, // Username or email
|
||||
email, // Old parameter, compatible with old api
|
||||
password,
|
||||
},
|
||||
} = ctx.action.params;
|
||||
account, // Username or email
|
||||
email, // Old parameter, compatible with old api
|
||||
password,
|
||||
} = ctx.action.params.values || {};
|
||||
|
||||
if (!account && !email) {
|
||||
ctx.throw(400, ctx.t('Please enter your username or email', { ns: namespace }));
|
||||
@ -55,6 +53,9 @@ export class BasicAuth extends BaseAuth {
|
||||
if (!/^[^@.<>"'/]{2,16}$/.test(username)) {
|
||||
ctx.throw(400, ctx.t('Please enter a valid username', { ns: namespace }));
|
||||
}
|
||||
if (!password) {
|
||||
ctx.throw(400, ctx.t('Please enter a password', { ns: namespace }));
|
||||
}
|
||||
if (password !== confirm_password) {
|
||||
ctx.throw(400, ctx.t('The password is inconsistent, please re-enter', { ns: namespace }));
|
||||
}
|
||||
@ -62,6 +63,7 @@ export class BasicAuth extends BaseAuth {
|
||||
return user;
|
||||
}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
async lostPassword() {
|
||||
const ctx = this.ctx;
|
||||
const {
|
||||
@ -83,6 +85,7 @@ export class BasicAuth extends BaseAuth {
|
||||
return user;
|
||||
}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
async resetPassword() {
|
||||
const ctx = this.ctx;
|
||||
const {
|
||||
@ -104,6 +107,7 @@ export class BasicAuth extends BaseAuth {
|
||||
return user;
|
||||
}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
async getUserByResetToken() {
|
||||
const ctx = this.ctx;
|
||||
const { token } = ctx.action.params;
|
||||
@ -121,8 +125,11 @@ export class BasicAuth extends BaseAuth {
|
||||
async changePassword() {
|
||||
const ctx = this.ctx;
|
||||
const {
|
||||
values: { oldPassword, newPassword },
|
||||
values: { oldPassword, newPassword, confirmPassword },
|
||||
} = ctx.action.params;
|
||||
if (newPassword !== confirmPassword) {
|
||||
ctx.throw(400, ctx.t('The password is inconsistent, please re-enter', { ns: namespace }));
|
||||
}
|
||||
const currentUser = ctx.auth.user;
|
||||
if (!currentUser) {
|
||||
ctx.throw(401);
|
||||
|
@ -0,0 +1,160 @@
|
||||
import { Database } from '@nocobase/database';
|
||||
import { MockServer, createMockServer } from '@nocobase/test';
|
||||
import { vi } from 'vitest';
|
||||
import { authType } from '../../constants';
|
||||
import axios from 'axios';
|
||||
import { CASAuth } from '../auth';
|
||||
import { AppSupervisor } from '@nocobase/server';
|
||||
|
||||
describe('cas', () => {
|
||||
let app: MockServer;
|
||||
let db: Database;
|
||||
let agent;
|
||||
let authenticator;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createMockServer({
|
||||
plugins: ['users', 'auth', 'cas'],
|
||||
});
|
||||
db = app.db;
|
||||
agent = app.agent();
|
||||
|
||||
const authenticatorRepo = db.getRepository('authenticators');
|
||||
authenticator = await authenticatorRepo.create({
|
||||
values: {
|
||||
name: 'cas-auth',
|
||||
authType: authType,
|
||||
enabled: 1,
|
||||
options: {
|
||||
casUrl: 'http://localhost:3000/cas',
|
||||
serviceDomain: 'http://localhost:13000',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await db.getRepository('users').destroy({
|
||||
truncate: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not sign in without auto signup', async () => {
|
||||
await authenticator.update({
|
||||
options: {
|
||||
...authenticator.options,
|
||||
autoSignup: false,
|
||||
},
|
||||
});
|
||||
vi.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: `
|
||||
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
|
||||
<cas:authenticationSuccess>
|
||||
<cas:user>test-user</cas:user>
|
||||
<cas:proxyGrantingTicket>PGTIOU-84678-8a9d...
|
||||
</cas:proxyGrantingTicket>
|
||||
</cas:authenticationSuccess>
|
||||
</cas:serviceResponse>
|
||||
`,
|
||||
});
|
||||
|
||||
const res = await agent.set('X-Authenticator', 'cas-auth').get('/auth:signIn?ticket=test-ticket');
|
||||
|
||||
expect(res.statusCode).toBe(401);
|
||||
expect(res.text).toBe('User not found');
|
||||
});
|
||||
|
||||
it('should sign in with auto signup', async () => {
|
||||
await authenticator.update({
|
||||
options: {
|
||||
...authenticator.options,
|
||||
autoSignup: true,
|
||||
},
|
||||
});
|
||||
vi.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: `
|
||||
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
|
||||
<cas:authenticationSuccess>
|
||||
<cas:user>test-user</cas:user>
|
||||
<cas:proxyGrantingTicket>PGTIOU-84678-8a9d...
|
||||
</cas:proxyGrantingTicket>
|
||||
</cas:authenticationSuccess>
|
||||
</cas:serviceResponse>
|
||||
`,
|
||||
});
|
||||
|
||||
const res = await agent.set('X-Authenticator', 'cas-auth').get('/auth:signIn?ticket=test-ticket');
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body.data.user).toBeDefined();
|
||||
expect(res.body.data.user.nickname).toBe('test-user');
|
||||
});
|
||||
|
||||
it('missing ticket', async () => {
|
||||
const res = await agent.set('X-Authenticator', 'cas-auth').get('/auth:signIn');
|
||||
expect(res.statusCode).toBe(401);
|
||||
expect(res.text).toBe('Missing ticket');
|
||||
});
|
||||
|
||||
it('invalid ticket', async () => {
|
||||
vi.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: `
|
||||
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
|
||||
<cas:authenticationFailure code="INVALID_TICKET">
|
||||
Ticket ST-1856339-aA5Yuvrxzpv8Tau1cYQ7 not recognized
|
||||
</cas:authenticationFailure>
|
||||
</cas:serviceResponse>
|
||||
`,
|
||||
});
|
||||
|
||||
const res = await agent.set('X-Authenticator', 'cas-auth').get('/auth:signIn?ticket=test-ticket');
|
||||
expect(res.statusCode).toBe(401);
|
||||
expect(res.text).toBe('Invalid ticket');
|
||||
});
|
||||
|
||||
it('cas:login', async () => {
|
||||
const res = await agent.get('/cas:login?authenticator=cas-auth');
|
||||
expect(res.statusCode).toBe(302);
|
||||
const service = encodeURIComponent(
|
||||
`http://localhost:13000${process.env.API_BASE_URL || '/api/'}cas:service?authenticator=cas-auth&__appName=${
|
||||
app.name
|
||||
}&redirect=/admin`,
|
||||
);
|
||||
expect(res.headers.location).toBe(`http://localhost:3000/cas/login?service=${service}`);
|
||||
});
|
||||
|
||||
it('cas:service', async () => {
|
||||
vi.spyOn(CASAuth.prototype, 'signIn').mockResolvedValue({
|
||||
user: {} as any,
|
||||
token: 'test-token',
|
||||
});
|
||||
const res = await agent.get('/cas:service?authenticator=cas-auth&__appName=main');
|
||||
expect(res.statusCode).toBe(302);
|
||||
expect(res.headers.location).toBe(`/admin?authenticator=cas-auth&token=test-token`);
|
||||
});
|
||||
|
||||
it('cas:service, sub app', async () => {
|
||||
vi.spyOn(CASAuth.prototype, 'signIn').mockResolvedValue({
|
||||
user: {} as any,
|
||||
token: 'test-token',
|
||||
});
|
||||
vi.spyOn(AppSupervisor, 'getInstance').mockReturnValue({
|
||||
runningMode: 'multiple',
|
||||
} as any);
|
||||
const res = await agent.get('/cas:service?authenticator=cas-auth&__appName=sub');
|
||||
expect(res.statusCode).toBe(302);
|
||||
expect(res.headers.location).toBe(`/apps/sub/admin?authenticator=cas-auth&token=test-token`);
|
||||
});
|
||||
|
||||
it('cas:service, error', async () => {
|
||||
vi.spyOn(CASAuth.prototype, 'signIn').mockRejectedValue(new Error('test error'));
|
||||
const res = await agent.get('/cas:service?authenticator=cas-auth&__appName=main');
|
||||
expect(res.statusCode).toBe(302);
|
||||
expect(res.headers.location).toBe(`/signin?authenticator=cas-auth&error=test%20error&redirect=/admin`);
|
||||
});
|
||||
});
|
@ -23,7 +23,9 @@ export const service = async (ctx: Context, next: Next) => {
|
||||
const { token } = await auth.signIn();
|
||||
ctx.redirect(`${prefix}${redirect || '/admin'}?authenticator=${authenticator}&token=${token}`);
|
||||
} catch (error) {
|
||||
ctx.redirect(`${prefix}/signin?authenticator=${authenticator}&error=${error.message}&redirect=${redirect}`);
|
||||
ctx.redirect(
|
||||
`${prefix}/signin?authenticator=${authenticator}&error=${error.message}&redirect=${redirect || '/admin'}`,
|
||||
);
|
||||
}
|
||||
return next();
|
||||
};
|
||||
|
@ -1,50 +1,47 @@
|
||||
import { vi } from 'vitest';
|
||||
import { dateFormatFn } from '../actions/formatter';
|
||||
import { formatter } from '../actions/formatter';
|
||||
|
||||
describe('formatter', () => {
|
||||
const field = 'field';
|
||||
const format = 'YYYY-MM-DD hh:mm:ss';
|
||||
describe('dateFormatFn', () => {
|
||||
it('should return correct format for sqlite', () => {
|
||||
const sequelize = {
|
||||
fn: vi.fn().mockImplementation((fn: string, format: string, field: string) => ({
|
||||
fn,
|
||||
format,
|
||||
field,
|
||||
})),
|
||||
col: vi.fn().mockImplementation((field: string) => field),
|
||||
};
|
||||
const dialect = 'sqlite';
|
||||
const result = dateFormatFn(sequelize, dialect, field, format);
|
||||
expect(result.format).toEqual('%Y-%m-%d %H:%M:%S');
|
||||
});
|
||||
it('should return correct format for sqlite', () => {
|
||||
const sequelize = {
|
||||
fn: (fn: string, format: string, field: string) => ({
|
||||
fn,
|
||||
format,
|
||||
field,
|
||||
}),
|
||||
col: (field: string) => field,
|
||||
getDialect: () => 'sqlite',
|
||||
};
|
||||
const result = formatter(sequelize, 'datetime', field, format);
|
||||
expect(result.format).toEqual('%Y-%m-%d %H:%M:%S');
|
||||
});
|
||||
|
||||
it('should return correct format for mysql', () => {
|
||||
const sequelize = {
|
||||
fn: vi.fn().mockImplementation((fn: string, field: string, format: string) => ({
|
||||
fn,
|
||||
format,
|
||||
field,
|
||||
})),
|
||||
col: vi.fn().mockImplementation((field: string) => field),
|
||||
};
|
||||
const dialect = 'mysql';
|
||||
const result = dateFormatFn(sequelize, dialect, field, format);
|
||||
expect(result.format).toEqual('%Y-%m-%d %H:%i:%S');
|
||||
});
|
||||
it('should return correct format for mysql', () => {
|
||||
const sequelize = {
|
||||
fn: (fn: string, field: string, format: string) => ({
|
||||
fn,
|
||||
format,
|
||||
field,
|
||||
}),
|
||||
col: (field: string) => field,
|
||||
getDialect: () => 'mysql',
|
||||
};
|
||||
const result = formatter(sequelize, 'datetime', field, format);
|
||||
expect(result.format).toEqual('%Y-%m-%d %H:%i:%S');
|
||||
});
|
||||
|
||||
it('should return correct format for postgres', () => {
|
||||
const sequelize = {
|
||||
fn: vi.fn().mockImplementation((fn: string, field: string, format: string) => ({
|
||||
fn,
|
||||
format,
|
||||
field,
|
||||
})),
|
||||
col: vi.fn().mockImplementation((field: string) => field),
|
||||
};
|
||||
const dialect = 'postgres';
|
||||
const result = dateFormatFn(sequelize, dialect, field, format);
|
||||
expect(result.format).toEqual('YYYY-MM-DD HH24:MI:SS');
|
||||
});
|
||||
it('should return correct format for postgres', () => {
|
||||
const sequelize = {
|
||||
fn: (fn: string, field: string, format: string) => ({
|
||||
fn,
|
||||
format,
|
||||
field,
|
||||
}),
|
||||
col: (field: string) => field,
|
||||
getDialect: () => 'postgres',
|
||||
};
|
||||
const result = formatter(sequelize, 'datetime', field, format);
|
||||
expect(result.format).toEqual('YYYY-MM-DD HH24:MI:SS');
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,14 @@
|
||||
import { MockServer, createMockServer } from '@nocobase/test';
|
||||
import compose from 'koa-compose';
|
||||
import { vi } from 'vitest';
|
||||
import { cacheMiddleware, parseBuilder, parseFieldAndAssociations } from '../actions/query';
|
||||
import {
|
||||
cacheMiddleware,
|
||||
parseBuilder,
|
||||
parseFieldAndAssociations,
|
||||
checkPermission,
|
||||
postProcess,
|
||||
parseVariables,
|
||||
} from '../actions/query';
|
||||
const formatter = await import('../actions/formatter');
|
||||
|
||||
describe('query', () => {
|
||||
@ -14,7 +21,7 @@ describe('query', () => {
|
||||
let app: MockServer;
|
||||
beforeAll(async () => {
|
||||
app = await createMockServer({
|
||||
plugins: ['data-source-manager'],
|
||||
plugins: ['data-source-manager', 'users', 'acl'],
|
||||
});
|
||||
app.db.options.underscored = true;
|
||||
app.db.collection({
|
||||
@ -41,32 +48,32 @@ describe('query', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
app.db.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'bigInt',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx = {
|
||||
app,
|
||||
db: {
|
||||
sequelize,
|
||||
getRepository: (name: string) => app.db.getRepository(name),
|
||||
getModel: (name: string) => app.db.getModel(name),
|
||||
getCollection: (name: string) => app.db.getCollection(name),
|
||||
options: {
|
||||
underscored: true,
|
||||
db: app.db,
|
||||
};
|
||||
ctx.db.sequelize = sequelize;
|
||||
});
|
||||
|
||||
it('should check permissions', async () => {
|
||||
const context = {
|
||||
...ctx,
|
||||
state: {
|
||||
currentRole: '',
|
||||
},
|
||||
action: {
|
||||
params: {
|
||||
values: {
|
||||
collection: 'users',
|
||||
},
|
||||
},
|
||||
},
|
||||
throw: vi.fn(),
|
||||
};
|
||||
await checkPermission(context, async () => {});
|
||||
expect(context.throw).toBeCalledWith(403, 'No permissions');
|
||||
});
|
||||
|
||||
it('should parse field and associations', async () => {
|
||||
const context = {
|
||||
...ctx,
|
||||
@ -225,7 +232,65 @@ describe('query', () => {
|
||||
await compose([parseFieldAndAssociations, parseBuilder])(context, async () => {});
|
||||
expect(context.action.params.values.queryParams.where.createdAt).toBeDefined();
|
||||
});
|
||||
|
||||
it('post process', async () => {
|
||||
const context = {
|
||||
...ctx,
|
||||
action: {
|
||||
params: {
|
||||
values: {
|
||||
data: [{ key: '123' }],
|
||||
fieldMap: {
|
||||
key: { type: 'bigInt' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await postProcess(context, async () => {});
|
||||
expect(context.body).toEqual([{ key: 123 }]);
|
||||
});
|
||||
|
||||
it('parse variables', async () => {
|
||||
const context = {
|
||||
...ctx,
|
||||
state: {
|
||||
currentUser: {
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
get: (key: string) => {
|
||||
return {
|
||||
'x-timezone': '',
|
||||
}[key];
|
||||
},
|
||||
action: {
|
||||
params: {
|
||||
values: {
|
||||
filter: {
|
||||
$and: [
|
||||
{
|
||||
createdAt: { $dateOn: '{{$nDate.now}}' },
|
||||
},
|
||||
{
|
||||
userId: { $eq: '{{$user.id}}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await parseVariables(context, async () => {});
|
||||
const { filter } = context.action.params.values;
|
||||
const dateOn = filter.$and[0].createdAt.$dateOn;
|
||||
console.log(dateOn);
|
||||
expect(new Date(dateOn).getTime()).toBeLessThanOrEqual(new Date().getTime());
|
||||
const userId = filter.$and[1].userId.$eq;
|
||||
expect(userId).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cacheMiddleware', () => {
|
||||
const key = 'test-key';
|
||||
const value = 'test-val';
|
||||
|
@ -27,6 +27,7 @@ export const dateFormatFn = (sequelize: any, dialect: string, field: string, for
|
||||
}
|
||||
};
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
export const formatFn = (sequelize: any, dialect: string, field: string, format: string) => {
|
||||
switch (dialect) {
|
||||
case 'sqlite':
|
||||
|
@ -316,7 +316,7 @@ export const cacheMiddleware = async (ctx: Context, next: Next) => {
|
||||
}
|
||||
};
|
||||
|
||||
const checkPermission = (ctx: Context, next: Next) => {
|
||||
export const checkPermission = (ctx: Context, next: Next) => {
|
||||
const { collection } = ctx.action.params.values as QueryParams;
|
||||
const roleName = ctx.state.currentRole || 'anonymous';
|
||||
const can = ctx.app.acl.can({ role: roleName, resource: collection, action: 'list' });
|
||||
|
@ -151,7 +151,7 @@ const Sync = () => {
|
||||
setLoading(true);
|
||||
await api.resource('localization').sync({
|
||||
values: {
|
||||
type: checkedList,
|
||||
types: checkedList,
|
||||
},
|
||||
});
|
||||
setLoading(false);
|
||||
|
@ -0,0 +1,34 @@
|
||||
import { MockServer, createMockServer } from '@nocobase/test';
|
||||
import PluginLocalizationServer from '../plugin';
|
||||
|
||||
describe('middleware', () => {
|
||||
let app: MockServer;
|
||||
let agent: any;
|
||||
let plugin: PluginLocalizationServer;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createMockServer({
|
||||
plugins: ['localization-management', 'client', 'ui-schema-storage', 'system-settings'],
|
||||
});
|
||||
await app.localeManager.load();
|
||||
agent = app.agent();
|
||||
plugin = app.pm.get('localization-management') as PluginLocalizationServer;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should merge resources', async () => {
|
||||
vi.spyOn(plugin.resources, 'getResources').mockResolvedValue({
|
||||
'test-resource': {
|
||||
'test-text': 'text Translation',
|
||||
},
|
||||
});
|
||||
const res = await agent.resource('app').getLang();
|
||||
const data = JSON.parse(res.text);
|
||||
const resources = data.data.resources;
|
||||
expect(resources['test-resource']).toBeDefined();
|
||||
expect(resources['test-resource']['test-text']).toBe('text Translation');
|
||||
});
|
||||
});
|
@ -0,0 +1,157 @@
|
||||
import { Model, Repository } from '@nocobase/database';
|
||||
import { MockServer, createMockServer } from '@nocobase/test';
|
||||
import { getSchemaUid, getTextsFromDB, getTextsFromMenu } from '../actions/localization';
|
||||
import { getMenuSchema, getMobileMenuSchema } from './utils';
|
||||
import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage';
|
||||
|
||||
describe('sync', () => {
|
||||
let app: MockServer;
|
||||
let agent: any;
|
||||
let repo: Repository;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createMockServer({
|
||||
plugins: [
|
||||
// 'data-source-manager',
|
||||
'collection-manager',
|
||||
'localization-management',
|
||||
'ui-schema-storage',
|
||||
'client',
|
||||
'mobile-client',
|
||||
'error-handler',
|
||||
],
|
||||
});
|
||||
await app.localeManager.load();
|
||||
agent = app.agent();
|
||||
repo = app.db.getRepository('localizationTexts');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.db.clean({ drop: true });
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should check sync types', async () => {
|
||||
const res = await agent.resource('localization').sync({
|
||||
types: [],
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.errors[0].message).toBe('Please provide synchronization source.');
|
||||
});
|
||||
|
||||
it('should sync local resources', async () => {
|
||||
vi.spyOn(app.localeManager, 'getResources').mockResolvedValue({
|
||||
test: {
|
||||
'sync.local.test1': 'Test1',
|
||||
'sync.local.test2': 'Test2',
|
||||
},
|
||||
});
|
||||
const res = await agent.resource('localization').sync({
|
||||
values: {
|
||||
types: ['local'],
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const texts = await repo.find({
|
||||
filter: {
|
||||
text: {
|
||||
$in: ['sync.local.test1', 'sync.local.test2'],
|
||||
},
|
||||
module: 'resources.test',
|
||||
'translations.locale': 'en-US',
|
||||
},
|
||||
appends: ['translations'],
|
||||
});
|
||||
expect(texts.length).toBe(2);
|
||||
expect(texts[0].translations[0].translation).toBe('Test1');
|
||||
expect(texts[1].translations[0].translation).toBe('Test2');
|
||||
});
|
||||
|
||||
it('should get texts from menu', async () => {
|
||||
const { adminSchemaUid, mobileSchemaUid } = await getSchemaUid(app.db);
|
||||
const repo = app.db.getRepository('uiSchemas') as UiSchemaRepository;
|
||||
await repo.insertAdjacent('beforeEnd', adminSchemaUid, getMenuSchema('test'));
|
||||
await repo.insertAdjacent('beforeEnd', mobileSchemaUid, getMobileMenuSchema('test-mobile'));
|
||||
const result = await getTextsFromMenu(app.db);
|
||||
expect(Object.keys(result)).toEqual(['test', 'test-mobile']);
|
||||
});
|
||||
|
||||
it('should get texts from db', async () => {
|
||||
await app.db.getRepository('collections').create({
|
||||
values: {
|
||||
key: 'test-collection',
|
||||
name: 'sync.db.collection',
|
||||
title: 'sync.db.collection',
|
||||
},
|
||||
hooks: false,
|
||||
});
|
||||
await app.db.getRepository('fields').create({
|
||||
values: {
|
||||
key: 'test-field',
|
||||
name: 'sync.db.field',
|
||||
title: 'sync.db.field',
|
||||
collectionNname: 'sync.db.collection',
|
||||
options: {
|
||||
uiSchema: {
|
||||
title: '{{t("sync.db.field")}}',
|
||||
enum: [{ label: 'sync.db.enum', value: '1' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
hooks: false,
|
||||
});
|
||||
const result = await getTextsFromDB(app.db);
|
||||
expect(Object.keys(result)).toMatchObject(['sync.db.collection', 'sync.db.field', 'sync.db.enum']);
|
||||
});
|
||||
|
||||
it('should add text when adding menu item', async () => {
|
||||
vi.spyOn(repo, 'create');
|
||||
const title = 'sync.menu.hook';
|
||||
const text = await repo.findOne({
|
||||
filter: {
|
||||
text: title,
|
||||
},
|
||||
});
|
||||
expect(text).toBeNull();
|
||||
const { adminSchemaUid } = await getSchemaUid(app.db);
|
||||
const schemaRepo = app.db.getRepository('uiSchemas') as UiSchemaRepository;
|
||||
await schemaRepo.insertAdjacent('beforeEnd', adminSchemaUid, getMenuSchema(title, true));
|
||||
expect(repo.create).toBeCalledWith({
|
||||
values: {
|
||||
module: 'resources.lm-menus',
|
||||
text: title,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should add text when creating fields', async () => {
|
||||
const model = app.db.getModel('localizationTexts');
|
||||
vi.spyOn(model, 'bulkCreate');
|
||||
await app.db.getRepository('fields').create({
|
||||
values: {
|
||||
name: 'sync.db.field',
|
||||
title: 'sync.db.field',
|
||||
collectionNname: 'sync.db.collection',
|
||||
options: {
|
||||
uiSchema: {
|
||||
title: '{{t("sync.db.field")}}',
|
||||
enum: [{ label: 'sync.db.enum', value: '1' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(model.bulkCreate).toBeCalledWith(
|
||||
[
|
||||
{
|
||||
module: 'resources.lm-collections',
|
||||
text: 'sync.db.field',
|
||||
},
|
||||
{
|
||||
module: 'resources.lm-collections',
|
||||
text: 'sync.db.enum',
|
||||
},
|
||||
],
|
||||
{ transaction: expect.anything() },
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,84 @@
|
||||
export const getMenuSchema = (title: string, hook = false) => ({
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title,
|
||||
'x-component': 'Menu.Item',
|
||||
'x-decorator': 'ACLMenuItemProvider',
|
||||
'x-component-props': {},
|
||||
...(hook
|
||||
? {
|
||||
'x-server-hooks': [
|
||||
{
|
||||
type: 'onSelfSave',
|
||||
method: 'extractTextToLocale',
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
properties: {
|
||||
page: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Page',
|
||||
'x-async': true,
|
||||
properties: {
|
||||
'8x6xrx59vpd': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'page:addBlock',
|
||||
'x-uid': 'th3q97bzylq',
|
||||
name: '8x6xrx59vpd',
|
||||
'x-app-version': '0.21.0-alpha.7',
|
||||
},
|
||||
},
|
||||
'x-uid': 'gqgfjv38all',
|
||||
name: 'page',
|
||||
'x-app-version': '0.21.0-alpha.7',
|
||||
},
|
||||
},
|
||||
name: '6ler5jumdz0',
|
||||
'x-uid': 'ri757idkdw0',
|
||||
'x-app-version': '0.21.0-alpha.7',
|
||||
});
|
||||
|
||||
export const getMobileMenuSchema = (title: string, hook = false) => ({
|
||||
name: 'tabBar',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'MTabBar',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
'9b6f369x1ef': {
|
||||
'x-uid': '0lafotn74pu',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'MTabBar.Item',
|
||||
'x-designer': 'MTabBar.Item.Designer',
|
||||
'x-component-props': {
|
||||
icon: 'HomeOutlined',
|
||||
title,
|
||||
},
|
||||
...(hook
|
||||
? {
|
||||
'x-server-hooks': [
|
||||
{
|
||||
type: 'onSelfSave',
|
||||
method: 'extractTextToLocale',
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'uq0qwy7rkyg',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
});
|
@ -3,7 +3,7 @@ import { Database, Model, Op } from '@nocobase/database';
|
||||
import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage';
|
||||
import { NAMESPACE_COLLECTIONS, NAMESPACE_MENUS } from '../constans';
|
||||
import LocalizationManagementPlugin from '../plugin';
|
||||
import { getTextsFromDBRecord, getTextsFromUISchema } from '../utils';
|
||||
import { getTextsFromDBRecord } from '../utils';
|
||||
|
||||
const getResourcesInstance = async (ctx: Context) => {
|
||||
const plugin = ctx.app.getPlugin('localization-management') as LocalizationManagementPlugin;
|
||||
@ -58,16 +58,6 @@ export const getUISchemas = async (db: Database) => {
|
||||
return uiSchemas;
|
||||
};
|
||||
|
||||
const getTextsFromUISchemas = async (db: Database) => {
|
||||
const result = {};
|
||||
const schemas = await getUISchemas(db);
|
||||
schemas.forEach((schema: Model) => {
|
||||
const texts = getTextsFromUISchema(schema.schema);
|
||||
texts.forEach((text) => (result[text] = ''));
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getTextsFromDB = async (db: Database) => {
|
||||
const result = {};
|
||||
const collections = Array.from(db.collections.values());
|
||||
@ -88,7 +78,7 @@ export const getTextsFromDB = async (db: Database) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const getSchemaUid = async (db: Database, migrate = false) => {
|
||||
export const getSchemaUid = async (db: Database, migrate = false) => {
|
||||
if (migrate) {
|
||||
const systemSettings = await db.getRepository('systemSettings').findOne();
|
||||
const options = systemSettings?.options || {};
|
||||
@ -135,22 +125,22 @@ const sync = async (ctx: Context, next: Next) => {
|
||||
ctx.logger.info('Start sync localization resources');
|
||||
const resourcesInstance = await getResourcesInstance(ctx);
|
||||
const locale = ctx.get('X-Locale') || 'en-US';
|
||||
const { type = [] } = ctx.action.params.values || {};
|
||||
if (!type.length) {
|
||||
const { types = [] } = ctx.action.params.values || {};
|
||||
if (!types.length) {
|
||||
ctx.throw(400, ctx.t('Please provide synchronization source.'));
|
||||
}
|
||||
|
||||
let resources: { [module: string]: any } = { client: {} };
|
||||
if (type.includes('local')) {
|
||||
if (types.includes('local')) {
|
||||
resources = await getResources(ctx);
|
||||
}
|
||||
if (type.includes('menu')) {
|
||||
if (types.includes('menu')) {
|
||||
const menuTexts = await getTextsFromMenu(ctx.db);
|
||||
resources[NAMESPACE_MENUS] = {
|
||||
...menuTexts,
|
||||
};
|
||||
}
|
||||
if (type.includes('db')) {
|
||||
if (types.includes('db')) {
|
||||
const dbTexts = await getTextsFromDB(ctx.db);
|
||||
resources[NAMESPACE_COLLECTIONS] = {
|
||||
...dbTexts,
|
||||
|
@ -1,5 +1,6 @@
|
||||
export const compile = (title: string) => (title || '').replace(/{{\s*t\(["|'|`](.*)["|'|`]\)\s*}}/g, '$1');
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
export const getTextsFromUISchema = (schema: any) => {
|
||||
const texts = [];
|
||||
const title = compile(schema.title);
|
||||
|
@ -3,6 +3,7 @@ import { MockServer, createMockServer } from '@nocobase/test';
|
||||
import { vi } from 'vitest';
|
||||
import { authType } from '../../constants';
|
||||
import { OIDCAuth } from '../oidc-auth';
|
||||
import { AppSupervisor } from '@nocobase/server';
|
||||
|
||||
describe('oidc', () => {
|
||||
let app: MockServer;
|
||||
@ -187,9 +188,39 @@ describe('oidc', () => {
|
||||
expect(res.body.data.user).toBeDefined();
|
||||
expect(res.body.data.user.id).toBe(user.id);
|
||||
});
|
||||
|
||||
it('oidc:redirect', async () => {
|
||||
vi.spyOn(OIDCAuth.prototype, 'signIn').mockResolvedValue({
|
||||
user: {} as any,
|
||||
token: 'test-token',
|
||||
});
|
||||
const res = await agent.get(`/oidc:redirect?state=${encodeURIComponent('name=oidc-auth&app=main')}`);
|
||||
expect(res.statusCode).toBe(302);
|
||||
expect(res.headers.location).toBe(`/admin?authenticator=oidc-auth&token=test-token`);
|
||||
});
|
||||
|
||||
it('oidc:redirect, sub app', async () => {
|
||||
vi.spyOn(OIDCAuth.prototype, 'signIn').mockResolvedValue({
|
||||
user: {} as any,
|
||||
token: 'test-token',
|
||||
});
|
||||
vi.spyOn(AppSupervisor, 'getInstance').mockReturnValue({
|
||||
runningMode: 'multiple',
|
||||
} as any);
|
||||
const res = await agent.get(`/oidc:redirect?state=${encodeURIComponent('name=oidc-auth&app=sub')}`);
|
||||
expect(res.statusCode).toBe(302);
|
||||
expect(res.headers.location).toBe(`/apps/sub/admin?authenticator=oidc-auth&token=test-token`);
|
||||
});
|
||||
|
||||
it('oidc:redirect, error', async () => {
|
||||
vi.spyOn(OIDCAuth.prototype, 'signIn').mockRejectedValue(new Error('test error'));
|
||||
const res = await agent.get(`/oidc:redirect?state=${encodeURIComponent('name=oidc-auth&app=main')}`);
|
||||
expect(res.statusCode).toBe(302);
|
||||
expect(res.headers.location).toBe(`/signin?redirect=/admin&authenticator=oidc-auth&error=test%20error`);
|
||||
});
|
||||
});
|
||||
|
||||
it('field mapping', () => {
|
||||
test('field mapping', () => {
|
||||
const auth = new OIDCAuth({
|
||||
authenticator: null,
|
||||
ctx: {
|
||||
@ -218,3 +249,35 @@ it('field mapping', () => {
|
||||
nickname: 'user1',
|
||||
});
|
||||
});
|
||||
|
||||
test('getExchangeBody', () => {
|
||||
const auth = new OIDCAuth({
|
||||
ctx: {
|
||||
db: {
|
||||
getCollection: () => ({}),
|
||||
},
|
||||
},
|
||||
options: {
|
||||
oidc: {
|
||||
clientId: 'test_client_id',
|
||||
clientSecret: 'test_client_secret',
|
||||
exchangeBodyKeys: [
|
||||
{
|
||||
paramName: 'client_id',
|
||||
optionsKey: 'clientId',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
paramName: 'client_secret',
|
||||
optionsKey: 'clientSecret',
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
const body = auth.getExchangeBody();
|
||||
expect(body).toMatchObject({
|
||||
client_id: 'test_client_id',
|
||||
});
|
||||
});
|
||||
|
@ -34,6 +34,7 @@ export class PluginOIDCServer extends Plugin {
|
||||
|
||||
this.app.acl.allow('oidc', '*', 'public');
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
Gateway.getInstance().addAppSelectorMiddleware(async (ctx, next) => {
|
||||
const { req } = ctx;
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
|
@ -3,6 +3,8 @@ import { MockServer, createMockServer } from '@nocobase/test';
|
||||
import { SAML } from '@node-saml/node-saml';
|
||||
import { vi } from 'vitest';
|
||||
import { authType } from '../../constants';
|
||||
import { SAMLAuth } from '../saml-auth';
|
||||
import { AppSupervisor } from '@nocobase/server';
|
||||
|
||||
describe('saml', () => {
|
||||
let app: MockServer;
|
||||
@ -204,4 +206,34 @@ describe('saml', () => {
|
||||
expect(res.body.data.user).toBeDefined();
|
||||
expect(res.body.data.user.id).toBe(user.id);
|
||||
});
|
||||
|
||||
it('saml:redirect', async () => {
|
||||
vi.spyOn(SAMLAuth.prototype, 'signIn').mockResolvedValue({
|
||||
user: {} as any,
|
||||
token: 'test-token',
|
||||
});
|
||||
const res = await agent.get('/saml:redirect?authenticator=saml-auth&__appName=main');
|
||||
expect(res.statusCode).toBe(302);
|
||||
expect(res.headers.location).toBe(`/admin?authenticator=saml-auth&token=test-token`);
|
||||
});
|
||||
|
||||
it('saml:redirect, sub app', async () => {
|
||||
vi.spyOn(SAMLAuth.prototype, 'signIn').mockResolvedValue({
|
||||
user: {} as any,
|
||||
token: 'test-token',
|
||||
});
|
||||
vi.spyOn(AppSupervisor, 'getInstance').mockReturnValue({
|
||||
runningMode: 'multiple',
|
||||
} as any);
|
||||
const res = await agent.get('/saml:redirect?authenticator=saml-auth&__appName=sub');
|
||||
expect(res.statusCode).toBe(302);
|
||||
expect(res.headers.location).toBe(`/apps/sub/admin?authenticator=saml-auth&token=test-token`);
|
||||
});
|
||||
|
||||
it('saml:redirect, error', async () => {
|
||||
vi.spyOn(SAMLAuth.prototype, 'signIn').mockRejectedValue(new Error('test error'));
|
||||
const res = await agent.get('/saml:redirect?authenticator=saml-auth&__appName=main');
|
||||
expect(res.statusCode).toBe(302);
|
||||
expect(res.headers.location).toBe(`/signin?authenticator=saml-auth&error=test%20error&redirect=/admin`);
|
||||
});
|
||||
});
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { Context, Next } from '@nocobase/actions';
|
||||
import { SAMLAuth } from '../saml-auth';
|
||||
import { SAML } from '@node-saml/node-saml';
|
||||
|
||||
export const metadata = async (ctx: Context, next: Next) => {
|
||||
const auth = ctx.auth as SAMLAuth;
|
||||
const options = auth.getOptions();
|
||||
const saml = new SAML(options);
|
||||
|
||||
ctx.type = 'text/xml';
|
||||
ctx.body = saml.generateServiceProviderMetadata(options.cert as string);
|
||||
ctx.withoutDataWrapping = true;
|
||||
|
||||
return next();
|
||||
};
|
@ -9,7 +9,7 @@ export const redirect = async (ctx: Context, next: Next) => {
|
||||
if (appName && appName !== 'main') {
|
||||
const appSupervisor = AppSupervisor.getInstance();
|
||||
if (appSupervisor?.runningMode !== 'single') {
|
||||
prefix += `/apps/${appName}`;
|
||||
prefix += `apps/${appName}`;
|
||||
}
|
||||
}
|
||||
const auth = (await ctx.app.authManager.get(authenticator, ctx)) as SAMLAuth;
|
||||
@ -20,7 +20,9 @@ export const redirect = async (ctx: Context, next: Next) => {
|
||||
const { token } = await auth.signIn();
|
||||
ctx.redirect(`${prefix}${redirect || '/admin'}?authenticator=${authenticator}&token=${token}`);
|
||||
} catch (error) {
|
||||
ctx.redirect(`${prefix}/signin?authenticator=${authenticator}&error=${error.message}&redirect=${redirect}`);
|
||||
ctx.redirect(
|
||||
`${prefix}/signin?authenticator=${authenticator}&error=${error.message}&redirect=${redirect || '/admin'}`,
|
||||
);
|
||||
}
|
||||
await next();
|
||||
};
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { InstallOptions, Plugin } from '@nocobase/server';
|
||||
import { getAuthUrl } from './actions/getAuthUrl';
|
||||
import { metadata } from './actions/metadata';
|
||||
import { redirect } from './actions/redirect';
|
||||
import { SAMLAuth } from './saml-auth';
|
||||
import { authType } from '../constants';
|
||||
@ -29,7 +28,6 @@ export class PluginSAMLServer extends Plugin {
|
||||
name: 'saml',
|
||||
actions: {
|
||||
redirect,
|
||||
metadata,
|
||||
getAuthUrl,
|
||||
},
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user