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 { Context } from '@nocobase/actions';
|
||||||
import { Auth, AuthManager } from '@nocobase/auth';
|
import { Auth, AuthManager } from '@nocobase/auth';
|
||||||
import Database, { Model } from '@nocobase/database';
|
import { Model } from '@nocobase/database';
|
||||||
import { MockServer, mockServer } from '@nocobase/test';
|
|
||||||
|
|
||||||
class MockStorer {
|
class MockStorer {
|
||||||
elements: Map<string, any> = new Map();
|
elements: Map<string, any> = new Map();
|
||||||
@ -29,12 +27,13 @@ class BasicAuth extends Auth {
|
|||||||
|
|
||||||
describe('auth-manager', () => {
|
describe('auth-manager', () => {
|
||||||
let authManager: AuthManager;
|
let authManager: AuthManager;
|
||||||
|
let storer: MockStorer;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
authManager = new AuthManager({
|
authManager = new AuthManager({
|
||||||
authKey: 'X-Authenticator',
|
authKey: 'X-Authenticator',
|
||||||
});
|
});
|
||||||
|
|
||||||
const storer = new MockStorer();
|
storer = new MockStorer();
|
||||||
const authenticator = {
|
const authenticator = {
|
||||||
name: 'basic-test',
|
name: 'basic-test',
|
||||||
authType: 'basic',
|
authType: 'basic',
|
||||||
@ -48,70 +47,25 @@ describe('auth-manager', () => {
|
|||||||
authManager.registerTypes('basic', { auth: BasicAuth });
|
authManager.registerTypes('basic', { auth: BasicAuth });
|
||||||
const authenticator = await authManager.get('basic-test', {} as Context);
|
const authenticator = await authManager.get('basic-test', {} as Context);
|
||||||
expect(authenticator).toBeInstanceOf(BasicAuth);
|
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', () => {
|
it('should list types', () => {
|
||||||
let app: MockServer;
|
authManager.registerTypes('basic', { auth: BasicAuth, title: 'Basic' });
|
||||||
let db: Database;
|
expect(authManager.listTypes()).toEqual([{ name: 'basic', title: 'Basic' }]);
|
||||||
let agent;
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
it('should get auth config', () => {
|
||||||
app = mockServer({
|
authManager.registerTypes('basic', { auth: BasicAuth, title: 'Basic' });
|
||||||
registerActions: true,
|
expect(authManager.getAuthConfig('basic')).toEqual({ auth: BasicAuth, title: 'Basic' });
|
||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
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';
|
import { Handlers } from '@nocobase/resourcer';
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
|
@ -85,9 +85,9 @@ export class AuthManager {
|
|||||||
if (!authenticator) {
|
if (!authenticator) {
|
||||||
throw new Error(`Authenticator [${name}] is not found.`);
|
throw new Error(`Authenticator [${name}] is not found.`);
|
||||||
}
|
}
|
||||||
const { auth } = this.authTypes.get(authenticator.authType);
|
const { auth } = this.authTypes.get(authenticator.authType) || {};
|
||||||
if (!auth) {
|
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 });
|
return new auth({ authenticator, options: authenticator.options, ctx });
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@ export class BaseAuth extends Auth {
|
|||||||
try {
|
try {
|
||||||
user = await this.validate();
|
user = await this.validate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.ctx.throw(401, err.message);
|
this.ctx.throw(err.status || 401, err.message);
|
||||||
}
|
}
|
||||||
if (!user) {
|
if (!user) {
|
||||||
this.ctx.throw(401, 'Unauthorized');
|
this.ctx.throw(401, 'Unauthorized');
|
||||||
|
@ -31,6 +31,7 @@ export class JwtService {
|
|||||||
return this.options.secret;
|
return this.options.secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
sign(payload: SignPayload, options?: SignOptions) {
|
sign(payload: SignPayload, options?: SignOptions) {
|
||||||
const opt = { expiresIn: this.expiresIn(), ...options };
|
const opt = { expiresIn: this.expiresIn(), ...options };
|
||||||
if (opt.expiresIn === 'never') {
|
if (opt.expiresIn === 'never') {
|
||||||
@ -39,6 +40,7 @@ export class JwtService {
|
|||||||
return jwt.sign(payload, this.secret(), opt);
|
return jwt.sign(payload, this.secret(), opt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
decode(token: string): Promise<any> {
|
decode(token: string): Promise<any> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
jwt.verify(token, this.secret(), (err: any, decoded: any) => {
|
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', 'hello')).toBeTruthy();
|
||||||
expect(await bloomFilter.exists('bloom-test', 'world')).toBeFalsy();
|
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');
|
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 };
|
const value = { a: 1 };
|
||||||
await cache.set('key', value);
|
await cache.set('key', value);
|
||||||
const cacheA = await cache.getValueInObject('key', 'a');
|
const cacheA = await cache.getValueInObject('key', 'a');
|
||||||
@ -32,6 +69,16 @@ describe('cache', () => {
|
|||||||
expect(cacheVal2).toEqual(2);
|
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 () => {
|
it('wrap with condition, useCache', async () => {
|
||||||
const obj = {};
|
const obj = {};
|
||||||
const get = () => obj;
|
const get = () => obj;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* istanbul ignore file -- @preserve */
|
||||||
import { RedisStore } from 'cache-manager-redis-yet';
|
import { RedisStore } from 'cache-manager-redis-yet';
|
||||||
import { BloomFilter } from '.';
|
import { BloomFilter } from '.';
|
||||||
import { Cache } from '../cache';
|
import { Cache } from '../cache';
|
||||||
|
@ -42,6 +42,21 @@ describe('infer fields', () => {
|
|||||||
await db.close();
|
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 () => {
|
it('should infer fields', async () => {
|
||||||
const model = class extends SQLModel {};
|
const model = class extends SQLModel {};
|
||||||
model.init(null, {
|
model.init(null, {
|
||||||
@ -60,4 +75,23 @@ left join roles r on ru.role_name=r.name`;
|
|||||||
name: { type: 'string', source: 'roles.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', () => {
|
test('group', () => {
|
||||||
const query = queryGenerator.selectQuery('users', { group: 'id' }, model);
|
const query1 = queryGenerator.selectQuery('users', { group: 'id' }, model);
|
||||||
expect(query).toBe('SELECT * FROM (SELECT * FROM "users") AS "users" GROUP BY "id";');
|
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', () => {
|
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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
get filterTargetKey() {
|
get filterTargetKey() {
|
||||||
const targetKey = this.options?.filterTargetKey || 'id';
|
const targetKey = this.options?.filterTargetKey || 'id';
|
||||||
if (targetKey && this.model.getAttributes()[targetKey]) {
|
if (targetKey && this.model.getAttributes()[targetKey]) {
|
||||||
|
@ -36,26 +36,38 @@ export class SQLModel extends Model {
|
|||||||
if (Array.isArray(ast)) {
|
if (Array.isArray(ast)) {
|
||||||
ast = ast[0];
|
ast = ast[0];
|
||||||
}
|
}
|
||||||
|
ast.from = ast.from || [];
|
||||||
|
ast.columns = ast.columns || [];
|
||||||
if (ast.with) {
|
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>();
|
const tables = new Set<string>();
|
||||||
// parse sql includes with clause is not accurate
|
ast.from.forEach((fromItem: { table: string; as: string }) => {
|
||||||
// the parsed columns seems to be always '*'
|
tables.add(fromItem.table);
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
return Array.from(tables).map((table) => ({ table, columns: '*' }));
|
return Array.from(tables).map((table) => ({ table, columns: '*' }));
|
||||||
}
|
}
|
||||||
if (ast.columns === '*') {
|
const tableAliases = {};
|
||||||
return ast.from.map((fromItem: { table: string; as: string }) => ({
|
ast.from.forEach((fromItem: { table: string; as: string }) => {
|
||||||
table: fromItem.table,
|
if (!fromItem.as) {
|
||||||
columns: '*',
|
return;
|
||||||
}));
|
}
|
||||||
}
|
tableAliases[fromItem.as] = fromItem.table;
|
||||||
|
});
|
||||||
const columns: string[] = ast.columns.reduce(
|
const columns: string[] = ast.columns.reduce(
|
||||||
(
|
(
|
||||||
tableMp: { [table: string]: { name: string; as: string }[] },
|
tableMp: { [table: string]: { name: string; as: string }[] },
|
||||||
@ -72,22 +84,17 @@ export class SQLModel extends Model {
|
|||||||
return tableMp;
|
return tableMp;
|
||||||
}
|
}
|
||||||
const table = column.expr.table;
|
const table = column.expr.table;
|
||||||
|
const name = tableAliases[table] || table;
|
||||||
const columnAttr = { name: column.expr.column, as: column.as };
|
const columnAttr = { name: column.expr.column, as: column.as };
|
||||||
if (!tableMp[table]) {
|
if (!tableMp[name]) {
|
||||||
tableMp[table] = [columnAttr];
|
tableMp[name] = [columnAttr];
|
||||||
} else {
|
} else {
|
||||||
tableMp[table].push(columnAttr);
|
tableMp[name].push(columnAttr);
|
||||||
}
|
}
|
||||||
return tableMp;
|
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)
|
return Object.entries(columns)
|
||||||
.filter(([_, columns]) => columns)
|
.filter(([_, columns]) => columns)
|
||||||
.map(([table, columns]) => ({ table, columns }));
|
.map(([table, columns]) => ({ table, columns }));
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
"Please enter a valid username": "Please enter a valid username",
|
"Please enter a valid username": "Please enter a valid username",
|
||||||
"Please enter a valid email": "Please enter a valid email",
|
"Please enter a valid email": "Please enter a valid email",
|
||||||
"Please enter your username or email": "Please enter your username or email",
|
"Please enter your username or email": "Please enter your username or email",
|
||||||
|
"Please enter a password": "Please enter a password",
|
||||||
"SMS": "SMS",
|
"SMS": "SMS",
|
||||||
"Username/Email": "Username/Email",
|
"Username/Email": "Username/Email",
|
||||||
"Auth UID": "Auth UID",
|
"Auth UID": "Auth UID",
|
||||||
|
@ -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": "请输入密码",
|
||||||
"SMS": "短信",
|
"SMS": "短信",
|
||||||
"Username/Email": "用户名/邮箱",
|
"Username/Email": "用户名/邮箱",
|
||||||
"Auth UID": "认证标识",
|
"Auth UID": "认证标识",
|
||||||
|
@ -97,6 +97,29 @@ describe('actions', () => {
|
|||||||
await app.destroy();
|
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 () => {
|
it('should sign in with password', async () => {
|
||||||
let res = await agent.resource('auth').check();
|
let res = await agent.resource('auth').check();
|
||||||
expect(res.body.data.id).toBeUndefined();
|
expect(res.body.data.id).toBeUndefined();
|
||||||
@ -175,35 +198,72 @@ describe('actions', () => {
|
|||||||
password: '12345',
|
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',
|
oldPassword: '12345',
|
||||||
newPassword: '123456',
|
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',
|
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 res2 = await userAgent.post('/auth:changePassword').set({ 'X-Authenticator': 'basic' }).send({
|
||||||
const user1 = await userRepo.create({
|
|
||||||
values: {
|
|
||||||
username: 'test2',
|
|
||||||
password: '12345',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const res2 = await agent.login(user1).post('/auth:changePassword').set({ 'X-Authenticator': 'basic' }).send({
|
|
||||||
oldPassword: '12345',
|
oldPassword: '12345',
|
||||||
newPassword: '123456',
|
newPassword: '123456',
|
||||||
confirmPassword: '123456',
|
confirmPassword: '123456',
|
||||||
});
|
});
|
||||||
expect(res2.statusCode).toEqual(200);
|
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({
|
const res = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({
|
||||||
username: 'new',
|
username: 'new',
|
||||||
password: 'new',
|
password: 'new',
|
||||||
confirm_password: 'new1',
|
confirm_password: 'new1',
|
||||||
});
|
});
|
||||||
expect(res.statusCode).toEqual(400);
|
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';
|
import { Context, Next } from '@nocobase/actions';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -12,12 +12,10 @@ export class BasicAuth extends BaseAuth {
|
|||||||
async validate() {
|
async validate() {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
const {
|
const {
|
||||||
values: {
|
account, // Username or email
|
||||||
account, // Username or email
|
email, // Old parameter, compatible with old api
|
||||||
email, // Old parameter, compatible with old api
|
password,
|
||||||
password,
|
} = ctx.action.params.values || {};
|
||||||
},
|
|
||||||
} = ctx.action.params;
|
|
||||||
|
|
||||||
if (!account && !email) {
|
if (!account && !email) {
|
||||||
ctx.throw(400, ctx.t('Please enter your username or email', { ns: namespace }));
|
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)) {
|
if (!/^[^@.<>"'/]{2,16}$/.test(username)) {
|
||||||
ctx.throw(400, ctx.t('Please enter a valid username', { ns: namespace }));
|
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) {
|
if (password !== confirm_password) {
|
||||||
ctx.throw(400, ctx.t('The password is inconsistent, please re-enter', { ns: namespace }));
|
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;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
async lostPassword() {
|
async lostPassword() {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
const {
|
const {
|
||||||
@ -83,6 +85,7 @@ export class BasicAuth extends BaseAuth {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
async resetPassword() {
|
async resetPassword() {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
const {
|
const {
|
||||||
@ -104,6 +107,7 @@ export class BasicAuth extends BaseAuth {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
async getUserByResetToken() {
|
async getUserByResetToken() {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
const { token } = ctx.action.params;
|
const { token } = ctx.action.params;
|
||||||
@ -121,8 +125,11 @@ export class BasicAuth extends BaseAuth {
|
|||||||
async changePassword() {
|
async changePassword() {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
const {
|
const {
|
||||||
values: { oldPassword, newPassword },
|
values: { oldPassword, newPassword, confirmPassword },
|
||||||
} = ctx.action.params;
|
} = 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;
|
const currentUser = ctx.auth.user;
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
ctx.throw(401);
|
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();
|
const { token } = await auth.signIn();
|
||||||
ctx.redirect(`${prefix}${redirect || '/admin'}?authenticator=${authenticator}&token=${token}`);
|
ctx.redirect(`${prefix}${redirect || '/admin'}?authenticator=${authenticator}&token=${token}`);
|
||||||
} catch (error) {
|
} 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();
|
return next();
|
||||||
};
|
};
|
||||||
|
@ -1,50 +1,47 @@
|
|||||||
import { vi } from 'vitest';
|
import { formatter } from '../actions/formatter';
|
||||||
import { dateFormatFn } from '../actions/formatter';
|
|
||||||
|
|
||||||
describe('formatter', () => {
|
describe('formatter', () => {
|
||||||
const field = 'field';
|
const field = 'field';
|
||||||
const format = 'YYYY-MM-DD hh:mm:ss';
|
const format = 'YYYY-MM-DD hh:mm:ss';
|
||||||
describe('dateFormatFn', () => {
|
it('should return correct format for sqlite', () => {
|
||||||
it('should return correct format for sqlite', () => {
|
const sequelize = {
|
||||||
const sequelize = {
|
fn: (fn: string, format: string, field: string) => ({
|
||||||
fn: vi.fn().mockImplementation((fn: string, format: string, field: string) => ({
|
fn,
|
||||||
fn,
|
format,
|
||||||
format,
|
field,
|
||||||
field,
|
}),
|
||||||
})),
|
col: (field: string) => field,
|
||||||
col: vi.fn().mockImplementation((field: string) => field),
|
getDialect: () => 'sqlite',
|
||||||
};
|
};
|
||||||
const dialect = 'sqlite';
|
const result = formatter(sequelize, 'datetime', field, format);
|
||||||
const result = dateFormatFn(sequelize, dialect, field, format);
|
expect(result.format).toEqual('%Y-%m-%d %H:%M:%S');
|
||||||
expect(result.format).toEqual('%Y-%m-%d %H:%M:%S');
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('should return correct format for mysql', () => {
|
it('should return correct format for mysql', () => {
|
||||||
const sequelize = {
|
const sequelize = {
|
||||||
fn: vi.fn().mockImplementation((fn: string, field: string, format: string) => ({
|
fn: (fn: string, field: string, format: string) => ({
|
||||||
fn,
|
fn,
|
||||||
format,
|
format,
|
||||||
field,
|
field,
|
||||||
})),
|
}),
|
||||||
col: vi.fn().mockImplementation((field: string) => field),
|
col: (field: string) => field,
|
||||||
};
|
getDialect: () => 'mysql',
|
||||||
const dialect = 'mysql';
|
};
|
||||||
const result = dateFormatFn(sequelize, dialect, field, format);
|
const result = formatter(sequelize, 'datetime', field, format);
|
||||||
expect(result.format).toEqual('%Y-%m-%d %H:%i:%S');
|
expect(result.format).toEqual('%Y-%m-%d %H:%i:%S');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return correct format for postgres', () => {
|
it('should return correct format for postgres', () => {
|
||||||
const sequelize = {
|
const sequelize = {
|
||||||
fn: vi.fn().mockImplementation((fn: string, field: string, format: string) => ({
|
fn: (fn: string, field: string, format: string) => ({
|
||||||
fn,
|
fn,
|
||||||
format,
|
format,
|
||||||
field,
|
field,
|
||||||
})),
|
}),
|
||||||
col: vi.fn().mockImplementation((field: string) => field),
|
col: (field: string) => field,
|
||||||
};
|
getDialect: () => 'postgres',
|
||||||
const dialect = 'postgres';
|
};
|
||||||
const result = dateFormatFn(sequelize, dialect, field, format);
|
const result = formatter(sequelize, 'datetime', field, format);
|
||||||
expect(result.format).toEqual('YYYY-MM-DD HH24:MI:SS');
|
expect(result.format).toEqual('YYYY-MM-DD HH24:MI:SS');
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
import { MockServer, createMockServer } from '@nocobase/test';
|
import { MockServer, createMockServer } from '@nocobase/test';
|
||||||
import compose from 'koa-compose';
|
import compose from 'koa-compose';
|
||||||
import { vi } from 'vitest';
|
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');
|
const formatter = await import('../actions/formatter');
|
||||||
|
|
||||||
describe('query', () => {
|
describe('query', () => {
|
||||||
@ -14,7 +21,7 @@ describe('query', () => {
|
|||||||
let app: MockServer;
|
let app: MockServer;
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await createMockServer({
|
app = await createMockServer({
|
||||||
plugins: ['data-source-manager'],
|
plugins: ['data-source-manager', 'users', 'acl'],
|
||||||
});
|
});
|
||||||
app.db.options.underscored = true;
|
app.db.options.underscored = true;
|
||||||
app.db.collection({
|
app.db.collection({
|
||||||
@ -41,32 +48,32 @@ describe('query', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
app.db.collection({
|
|
||||||
name: 'users',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'id',
|
|
||||||
type: 'bigInt',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
ctx = {
|
ctx = {
|
||||||
app,
|
app,
|
||||||
db: {
|
db: app.db,
|
||||||
sequelize,
|
};
|
||||||
getRepository: (name: string) => app.db.getRepository(name),
|
ctx.db.sequelize = sequelize;
|
||||||
getModel: (name: string) => app.db.getModel(name),
|
});
|
||||||
getCollection: (name: string) => app.db.getCollection(name),
|
|
||||||
options: {
|
it('should check permissions', async () => {
|
||||||
underscored: true,
|
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 () => {
|
it('should parse field and associations', async () => {
|
||||||
const context = {
|
const context = {
|
||||||
...ctx,
|
...ctx,
|
||||||
@ -225,7 +232,65 @@ describe('query', () => {
|
|||||||
await compose([parseFieldAndAssociations, parseBuilder])(context, async () => {});
|
await compose([parseFieldAndAssociations, parseBuilder])(context, async () => {});
|
||||||
expect(context.action.params.values.queryParams.where.createdAt).toBeDefined();
|
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', () => {
|
describe('cacheMiddleware', () => {
|
||||||
const key = 'test-key';
|
const key = 'test-key';
|
||||||
const value = 'test-val';
|
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) => {
|
export const formatFn = (sequelize: any, dialect: string, field: string, format: string) => {
|
||||||
switch (dialect) {
|
switch (dialect) {
|
||||||
case 'sqlite':
|
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 { collection } = ctx.action.params.values as QueryParams;
|
||||||
const roleName = ctx.state.currentRole || 'anonymous';
|
const roleName = ctx.state.currentRole || 'anonymous';
|
||||||
const can = ctx.app.acl.can({ role: roleName, resource: collection, action: 'list' });
|
const can = ctx.app.acl.can({ role: roleName, resource: collection, action: 'list' });
|
||||||
|
@ -151,7 +151,7 @@ const Sync = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
await api.resource('localization').sync({
|
await api.resource('localization').sync({
|
||||||
values: {
|
values: {
|
||||||
type: checkedList,
|
types: checkedList,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setLoading(false);
|
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 { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage';
|
||||||
import { NAMESPACE_COLLECTIONS, NAMESPACE_MENUS } from '../constans';
|
import { NAMESPACE_COLLECTIONS, NAMESPACE_MENUS } from '../constans';
|
||||||
import LocalizationManagementPlugin from '../plugin';
|
import LocalizationManagementPlugin from '../plugin';
|
||||||
import { getTextsFromDBRecord, getTextsFromUISchema } from '../utils';
|
import { getTextsFromDBRecord } from '../utils';
|
||||||
|
|
||||||
const getResourcesInstance = async (ctx: Context) => {
|
const getResourcesInstance = async (ctx: Context) => {
|
||||||
const plugin = ctx.app.getPlugin('localization-management') as LocalizationManagementPlugin;
|
const plugin = ctx.app.getPlugin('localization-management') as LocalizationManagementPlugin;
|
||||||
@ -58,16 +58,6 @@ export const getUISchemas = async (db: Database) => {
|
|||||||
return uiSchemas;
|
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) => {
|
export const getTextsFromDB = async (db: Database) => {
|
||||||
const result = {};
|
const result = {};
|
||||||
const collections = Array.from(db.collections.values());
|
const collections = Array.from(db.collections.values());
|
||||||
@ -88,7 +78,7 @@ export const getTextsFromDB = async (db: Database) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSchemaUid = async (db: Database, migrate = false) => {
|
export const getSchemaUid = async (db: Database, migrate = false) => {
|
||||||
if (migrate) {
|
if (migrate) {
|
||||||
const systemSettings = await db.getRepository('systemSettings').findOne();
|
const systemSettings = await db.getRepository('systemSettings').findOne();
|
||||||
const options = systemSettings?.options || {};
|
const options = systemSettings?.options || {};
|
||||||
@ -135,22 +125,22 @@ const sync = async (ctx: Context, next: Next) => {
|
|||||||
ctx.logger.info('Start sync localization resources');
|
ctx.logger.info('Start sync localization resources');
|
||||||
const resourcesInstance = await getResourcesInstance(ctx);
|
const resourcesInstance = await getResourcesInstance(ctx);
|
||||||
const locale = ctx.get('X-Locale') || 'en-US';
|
const locale = ctx.get('X-Locale') || 'en-US';
|
||||||
const { type = [] } = ctx.action.params.values || {};
|
const { types = [] } = ctx.action.params.values || {};
|
||||||
if (!type.length) {
|
if (!types.length) {
|
||||||
ctx.throw(400, ctx.t('Please provide synchronization source.'));
|
ctx.throw(400, ctx.t('Please provide synchronization source.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
let resources: { [module: string]: any } = { client: {} };
|
let resources: { [module: string]: any } = { client: {} };
|
||||||
if (type.includes('local')) {
|
if (types.includes('local')) {
|
||||||
resources = await getResources(ctx);
|
resources = await getResources(ctx);
|
||||||
}
|
}
|
||||||
if (type.includes('menu')) {
|
if (types.includes('menu')) {
|
||||||
const menuTexts = await getTextsFromMenu(ctx.db);
|
const menuTexts = await getTextsFromMenu(ctx.db);
|
||||||
resources[NAMESPACE_MENUS] = {
|
resources[NAMESPACE_MENUS] = {
|
||||||
...menuTexts,
|
...menuTexts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (type.includes('db')) {
|
if (types.includes('db')) {
|
||||||
const dbTexts = await getTextsFromDB(ctx.db);
|
const dbTexts = await getTextsFromDB(ctx.db);
|
||||||
resources[NAMESPACE_COLLECTIONS] = {
|
resources[NAMESPACE_COLLECTIONS] = {
|
||||||
...dbTexts,
|
...dbTexts,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export const compile = (title: string) => (title || '').replace(/{{\s*t\(["|'|`](.*)["|'|`]\)\s*}}/g, '$1');
|
export const compile = (title: string) => (title || '').replace(/{{\s*t\(["|'|`](.*)["|'|`]\)\s*}}/g, '$1');
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
export const getTextsFromUISchema = (schema: any) => {
|
export const getTextsFromUISchema = (schema: any) => {
|
||||||
const texts = [];
|
const texts = [];
|
||||||
const title = compile(schema.title);
|
const title = compile(schema.title);
|
||||||
|
@ -3,6 +3,7 @@ import { MockServer, createMockServer } from '@nocobase/test';
|
|||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
import { authType } from '../../constants';
|
import { authType } from '../../constants';
|
||||||
import { OIDCAuth } from '../oidc-auth';
|
import { OIDCAuth } from '../oidc-auth';
|
||||||
|
import { AppSupervisor } from '@nocobase/server';
|
||||||
|
|
||||||
describe('oidc', () => {
|
describe('oidc', () => {
|
||||||
let app: MockServer;
|
let app: MockServer;
|
||||||
@ -187,9 +188,39 @@ describe('oidc', () => {
|
|||||||
expect(res.body.data.user).toBeDefined();
|
expect(res.body.data.user).toBeDefined();
|
||||||
expect(res.body.data.user.id).toBe(user.id);
|
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({
|
const auth = new OIDCAuth({
|
||||||
authenticator: null,
|
authenticator: null,
|
||||||
ctx: {
|
ctx: {
|
||||||
@ -218,3 +249,35 @@ it('field mapping', () => {
|
|||||||
nickname: 'user1',
|
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');
|
this.app.acl.allow('oidc', '*', 'public');
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
Gateway.getInstance().addAppSelectorMiddleware(async (ctx, next) => {
|
Gateway.getInstance().addAppSelectorMiddleware(async (ctx, next) => {
|
||||||
const { req } = ctx;
|
const { req } = ctx;
|
||||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
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 { SAML } from '@node-saml/node-saml';
|
||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
import { authType } from '../../constants';
|
import { authType } from '../../constants';
|
||||||
|
import { SAMLAuth } from '../saml-auth';
|
||||||
|
import { AppSupervisor } from '@nocobase/server';
|
||||||
|
|
||||||
describe('saml', () => {
|
describe('saml', () => {
|
||||||
let app: MockServer;
|
let app: MockServer;
|
||||||
@ -204,4 +206,34 @@ describe('saml', () => {
|
|||||||
expect(res.body.data.user).toBeDefined();
|
expect(res.body.data.user).toBeDefined();
|
||||||
expect(res.body.data.user.id).toBe(user.id);
|
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') {
|
if (appName && appName !== 'main') {
|
||||||
const appSupervisor = AppSupervisor.getInstance();
|
const appSupervisor = AppSupervisor.getInstance();
|
||||||
if (appSupervisor?.runningMode !== 'single') {
|
if (appSupervisor?.runningMode !== 'single') {
|
||||||
prefix += `/apps/${appName}`;
|
prefix += `apps/${appName}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const auth = (await ctx.app.authManager.get(authenticator, ctx)) as SAMLAuth;
|
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();
|
const { token } = await auth.signIn();
|
||||||
ctx.redirect(`${prefix}${redirect || '/admin'}?authenticator=${authenticator}&token=${token}`);
|
ctx.redirect(`${prefix}${redirect || '/admin'}?authenticator=${authenticator}&token=${token}`);
|
||||||
} catch (error) {
|
} 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();
|
await next();
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { InstallOptions, Plugin } from '@nocobase/server';
|
import { InstallOptions, Plugin } from '@nocobase/server';
|
||||||
import { getAuthUrl } from './actions/getAuthUrl';
|
import { getAuthUrl } from './actions/getAuthUrl';
|
||||||
import { metadata } from './actions/metadata';
|
|
||||||
import { redirect } from './actions/redirect';
|
import { redirect } from './actions/redirect';
|
||||||
import { SAMLAuth } from './saml-auth';
|
import { SAMLAuth } from './saml-auth';
|
||||||
import { authType } from '../constants';
|
import { authType } from '../constants';
|
||||||
@ -29,7 +28,6 @@ export class PluginSAMLServer extends Plugin {
|
|||||||
name: 'saml',
|
name: 'saml',
|
||||||
actions: {
|
actions: {
|
||||||
redirect,
|
redirect,
|
||||||
metadata,
|
|
||||||
getAuthUrl,
|
getAuthUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user