mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Fix repeated logic caused by updateOne & deleteOne overrides (#16433)
* fix updateOne & deleteOne overrides repeated logic * replace remaining jest usages * fix notifications service env mock * simplify mockImplementation with mockResolvedValue * fix types in test Co-authored-by: ian <licitdev@gmail.com>
This commit is contained in:
138
api/src/services/flows.test.ts
Normal file
138
api/src/services/flows.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import knex, { Knex } from 'knex';
|
||||
import { getTracker, MockClient, Tracker } from 'knex-mock-client';
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest';
|
||||
import { FlowsService } from '.';
|
||||
import { getFlowManager } from '../flows';
|
||||
|
||||
vi.mock('../../src/database/index', () => {
|
||||
return { __esModule: true, default: vi.fn(), getDatabaseClient: vi.fn().mockReturnValue('postgres') };
|
||||
});
|
||||
|
||||
vi.mock('../flows', () => {
|
||||
return { getFlowManager: vi.fn().mockReturnValue({ reload: vi.fn() }) };
|
||||
});
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
let db: Knex;
|
||||
let tracker: Tracker;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = knex({ client: MockClient });
|
||||
tracker = getTracker();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
tracker.on.any('directus_flows').response({});
|
||||
tracker.on.update('directus_operations').response({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tracker.reset();
|
||||
});
|
||||
|
||||
describe('Services / Flows', () => {
|
||||
let service: FlowsService;
|
||||
let flowManagerReloadSpy: SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new FlowsService({
|
||||
knex: db,
|
||||
schema: {
|
||||
collections: {
|
||||
directus_flows: {
|
||||
collection: 'directus_flows',
|
||||
primary: 'id',
|
||||
singleton: false,
|
||||
sortField: null,
|
||||
note: null,
|
||||
accountability: null,
|
||||
fields: {
|
||||
id: {
|
||||
field: 'id',
|
||||
defaultValue: null,
|
||||
nullable: false,
|
||||
generated: true,
|
||||
type: 'integer',
|
||||
dbType: 'integer',
|
||||
precision: null,
|
||||
scale: null,
|
||||
special: [],
|
||||
note: null,
|
||||
validation: null,
|
||||
alias: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: [],
|
||||
},
|
||||
});
|
||||
flowManagerReloadSpy = vi.spyOn(getFlowManager(), 'reload');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
flowManagerReloadSpy.mockClear();
|
||||
});
|
||||
|
||||
describe('createOne', () => {
|
||||
it('should reload flows once', async () => {
|
||||
await service.createOne({});
|
||||
expect(flowManagerReloadSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createMany', () => {
|
||||
it('should reload flows once', async () => {
|
||||
await service.createMany([{}]);
|
||||
expect(flowManagerReloadSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOne', () => {
|
||||
it('should reload flows once', async () => {
|
||||
await service.updateOne(1, {});
|
||||
expect(flowManagerReloadSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateBatch', () => {
|
||||
it('should reload flows once', async () => {
|
||||
await service.updateBatch([{ id: 1 }]);
|
||||
expect(flowManagerReloadSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMany', () => {
|
||||
it('should reload flows once', async () => {
|
||||
await service.updateMany([1], {});
|
||||
expect(flowManagerReloadSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should update operations once and reload flows once', async () => {
|
||||
await service.deleteOne(1);
|
||||
|
||||
const updateOperationsCalls = tracker.history.update.filter((query) =>
|
||||
query.sql.includes(`update "directus_operations" set "resolve" = ?, "reject" = ? where "flow"`)
|
||||
).length;
|
||||
|
||||
expect(updateOperationsCalls).toBe(1);
|
||||
expect(flowManagerReloadSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteMany', () => {
|
||||
it('should update operations once and reload flows once', async () => {
|
||||
await service.deleteMany([1]);
|
||||
|
||||
const updateOperationsCalls = tracker.history.update.filter((query) =>
|
||||
query.sql.includes(`update "directus_operations" set "resolve" = ?, "reject" = ? where "flow"`)
|
||||
).length;
|
||||
|
||||
expect(updateOperationsCalls).toBe(1);
|
||||
expect(flowManagerReloadSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -26,15 +26,6 @@ export class FlowsService extends ItemsService<FlowRaw> {
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateOne(key: PrimaryKey, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey> {
|
||||
const flowManager = getFlowManager();
|
||||
|
||||
const result = await super.updateOne(key, data, opts);
|
||||
await flowManager.reload();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateBatch(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
const flowManager = getFlowManager();
|
||||
|
||||
@@ -53,18 +44,6 @@ export class FlowsService extends ItemsService<FlowRaw> {
|
||||
return result;
|
||||
}
|
||||
|
||||
async deleteOne(key: PrimaryKey, opts?: MutationOptions): Promise<PrimaryKey> {
|
||||
const flowManager = getFlowManager();
|
||||
|
||||
// this is to prevent foreign key constraint error on directus_operations resolve/reject during cascade deletion
|
||||
await this.knex('directus_operations').update({ resolve: null, reject: null }).where('flow', key);
|
||||
|
||||
const result = await super.deleteOne(key, opts);
|
||||
await flowManager.reload();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async deleteMany(keys: PrimaryKey[], opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
const flowManager = getFlowManager();
|
||||
|
||||
|
||||
125
api/src/services/operations.test.ts
Normal file
125
api/src/services/operations.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import knex, { Knex } from 'knex';
|
||||
import { getTracker, MockClient, Tracker } from 'knex-mock-client';
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest';
|
||||
import { OperationsService } from '.';
|
||||
import { getFlowManager } from '../flows';
|
||||
|
||||
vi.mock('../../src/database/index', () => {
|
||||
return { __esModule: true, default: vi.fn(), getDatabaseClient: vi.fn().mockReturnValue('postgres') };
|
||||
});
|
||||
|
||||
vi.mock('../flows', () => {
|
||||
return { getFlowManager: vi.fn().mockReturnValue({ reload: vi.fn() }) };
|
||||
});
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
let db: Knex;
|
||||
let tracker: Tracker;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = knex({ client: MockClient });
|
||||
tracker = getTracker();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
tracker.on.any('directus_operations').response({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tracker.reset();
|
||||
});
|
||||
|
||||
describe('Services / Operations', () => {
|
||||
let service: OperationsService;
|
||||
let flowManagerReloadSpy: SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new OperationsService({
|
||||
knex: db,
|
||||
schema: {
|
||||
collections: {
|
||||
directus_operations: {
|
||||
collection: 'directus_operations',
|
||||
primary: 'id',
|
||||
singleton: false,
|
||||
sortField: null,
|
||||
note: null,
|
||||
accountability: null,
|
||||
fields: {
|
||||
id: {
|
||||
field: 'id',
|
||||
defaultValue: null,
|
||||
nullable: false,
|
||||
generated: true,
|
||||
type: 'integer',
|
||||
dbType: 'integer',
|
||||
precision: null,
|
||||
scale: null,
|
||||
special: [],
|
||||
note: null,
|
||||
validation: null,
|
||||
alias: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: [],
|
||||
},
|
||||
});
|
||||
flowManagerReloadSpy = vi.spyOn(getFlowManager(), 'reload');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
flowManagerReloadSpy.mockClear();
|
||||
});
|
||||
|
||||
describe('createOne', () => {
|
||||
it('should reload flows once', async () => {
|
||||
await service.createOne({});
|
||||
expect(flowManagerReloadSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createMany', () => {
|
||||
it('should reload flows once', async () => {
|
||||
await service.createMany([{}]);
|
||||
expect(flowManagerReloadSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOne', () => {
|
||||
it('should reload flows once', async () => {
|
||||
await service.updateOne(1, {});
|
||||
expect(flowManagerReloadSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateBatch', () => {
|
||||
it('should reload flows once', async () => {
|
||||
await service.updateBatch([{ id: 1 }]);
|
||||
expect(flowManagerReloadSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMany', () => {
|
||||
it('should reload flows once', async () => {
|
||||
await service.updateMany([1], {});
|
||||
expect(flowManagerReloadSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should reload flows once', async () => {
|
||||
await service.deleteOne(1);
|
||||
expect(flowManagerReloadSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteMany', () => {
|
||||
it('should reload flows once', async () => {
|
||||
await service.deleteMany([1]);
|
||||
expect(flowManagerReloadSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -26,15 +26,6 @@ export class OperationsService extends ItemsService<OperationRaw> {
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateOne(key: PrimaryKey, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey> {
|
||||
const flowManager = getFlowManager();
|
||||
|
||||
const result = await super.updateOne(key, data, opts);
|
||||
await flowManager.reload();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateBatch(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
const flowManager = getFlowManager();
|
||||
|
||||
@@ -53,15 +44,6 @@ export class OperationsService extends ItemsService<OperationRaw> {
|
||||
return result;
|
||||
}
|
||||
|
||||
async deleteOne(key: PrimaryKey, opts?: MutationOptions): Promise<PrimaryKey> {
|
||||
const flowManager = getFlowManager();
|
||||
|
||||
const result = await super.deleteOne(key, opts);
|
||||
await flowManager.reload();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async deleteMany(keys: PrimaryKey[], opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
const flowManager = getFlowManager();
|
||||
|
||||
|
||||
158
api/src/services/permissions.test.ts
Normal file
158
api/src/services/permissions.test.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import knex, { Knex } from 'knex';
|
||||
import { getTracker, MockClient, Tracker } from 'knex-mock-client';
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest';
|
||||
import { ItemsService, PermissionsService } from '.';
|
||||
import * as cache from '../cache';
|
||||
|
||||
vi.mock('../../src/database/index', () => {
|
||||
return { __esModule: true, default: vi.fn(), getDatabaseClient: vi.fn().mockReturnValue('postgres') };
|
||||
});
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
let db: Knex;
|
||||
let tracker: Tracker;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = knex({ client: MockClient });
|
||||
tracker = getTracker();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
tracker.on.any('directus_permissions').response({});
|
||||
tracker.on
|
||||
.select(
|
||||
/"directus_permissions"."id" from "directus_permissions" order by "directus_permissions"."id" asc limit .*/
|
||||
)
|
||||
.response([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tracker.reset();
|
||||
});
|
||||
|
||||
describe('Services / Permissions', () => {
|
||||
let service: PermissionsService;
|
||||
let clearSystemCacheSpy: SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new PermissionsService({
|
||||
knex: db,
|
||||
schema: {
|
||||
collections: {
|
||||
directus_permissions: {
|
||||
collection: 'directus_permissions',
|
||||
primary: 'id',
|
||||
singleton: false,
|
||||
sortField: null,
|
||||
note: null,
|
||||
accountability: null,
|
||||
fields: {
|
||||
id: {
|
||||
field: 'id',
|
||||
defaultValue: null,
|
||||
nullable: false,
|
||||
generated: true,
|
||||
type: 'integer',
|
||||
dbType: 'integer',
|
||||
precision: null,
|
||||
scale: null,
|
||||
special: [],
|
||||
note: null,
|
||||
validation: null,
|
||||
alias: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: [],
|
||||
},
|
||||
});
|
||||
clearSystemCacheSpy = vi.spyOn(cache, 'clearSystemCache').mockResolvedValue();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearSystemCacheSpy.mockClear();
|
||||
});
|
||||
|
||||
describe('createOne', () => {
|
||||
it('should clearSystemCache once', async () => {
|
||||
await service.createOne({});
|
||||
expect(clearSystemCacheSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createMany', () => {
|
||||
it('should clearSystemCache once', async () => {
|
||||
await service.createMany([{}]);
|
||||
expect(clearSystemCacheSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOne', () => {
|
||||
it('should clearSystemCache once', async () => {
|
||||
await service.updateOne(1, {});
|
||||
expect(clearSystemCacheSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMany', () => {
|
||||
it('should clearSystemCache once', async () => {
|
||||
await service.updateMany([1], {});
|
||||
expect(clearSystemCacheSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateBatch', () => {
|
||||
it('should clearSystemCache once', async () => {
|
||||
await service.updateBatch([{ id: 1 }]);
|
||||
expect(clearSystemCacheSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateByQuery', () => {
|
||||
it('should clearSystemCache once', async () => {
|
||||
// mock return value for following empty query
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
await service.updateByQuery({}, {});
|
||||
expect(clearSystemCacheSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsertOne', () => {
|
||||
it('should clearSystemCache once', async () => {
|
||||
await service.upsertOne({});
|
||||
expect(clearSystemCacheSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsertMany', () => {
|
||||
it('should clearSystemCache once', async () => {
|
||||
await service.upsertMany([{ id: 1 }]);
|
||||
expect(clearSystemCacheSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should clearSystemCache once', async () => {
|
||||
await service.deleteOne(1);
|
||||
expect(clearSystemCacheSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteMany', () => {
|
||||
it('should clearSystemCache once', async () => {
|
||||
await service.deleteMany([1]);
|
||||
expect(clearSystemCacheSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteByQuery', () => {
|
||||
it('should clearSystemCache once', async () => {
|
||||
// mock return value for following empty query
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
await service.deleteByQuery({});
|
||||
expect(clearSystemCacheSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -90,18 +90,6 @@ export class PermissionsService extends ItemsService {
|
||||
return res;
|
||||
}
|
||||
|
||||
async updateByQuery(query: Query, data: Partial<Item>, opts?: MutationOptions) {
|
||||
const res = await super.updateByQuery(query, data, opts);
|
||||
await clearSystemCache();
|
||||
return res;
|
||||
}
|
||||
|
||||
async updateOne(key: PrimaryKey, data: Partial<Item>, opts?: MutationOptions) {
|
||||
const res = await super.updateOne(key, data, opts);
|
||||
await clearSystemCache();
|
||||
return res;
|
||||
}
|
||||
|
||||
async updateBatch(data: Partial<Item>[], opts?: MutationOptions) {
|
||||
const res = await super.updateBatch(data, opts);
|
||||
await clearSystemCache();
|
||||
@@ -114,30 +102,12 @@ export class PermissionsService extends ItemsService {
|
||||
return res;
|
||||
}
|
||||
|
||||
async upsertOne(payload: Partial<Item>, opts?: MutationOptions) {
|
||||
const res = await super.upsertOne(payload, opts);
|
||||
await clearSystemCache();
|
||||
return res;
|
||||
}
|
||||
|
||||
async upsertMany(payloads: Partial<Item>[], opts?: MutationOptions) {
|
||||
const res = await super.upsertMany(payloads, opts);
|
||||
await clearSystemCache();
|
||||
return res;
|
||||
}
|
||||
|
||||
async deleteByQuery(query: Query, opts?: MutationOptions) {
|
||||
const res = await super.deleteByQuery(query, opts);
|
||||
await clearSystemCache();
|
||||
return res;
|
||||
}
|
||||
|
||||
async deleteOne(key: PrimaryKey, opts?: MutationOptions) {
|
||||
const res = await super.deleteOne(key, opts);
|
||||
await clearSystemCache();
|
||||
return res;
|
||||
}
|
||||
|
||||
async deleteMany(keys: PrimaryKey[], opts?: MutationOptions) {
|
||||
const res = await super.deleteMany(keys, opts);
|
||||
await clearSystemCache();
|
||||
|
||||
183
api/src/services/roles.test.ts
Normal file
183
api/src/services/roles.test.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import knex, { Knex } from 'knex';
|
||||
import { getTracker, MockClient, Tracker } from 'knex-mock-client';
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest';
|
||||
import { ItemsService, PermissionsService, PresetsService, RolesService, UsersService } from '.';
|
||||
|
||||
vi.mock('../../src/database/index', () => {
|
||||
return { __esModule: true, default: vi.fn(), getDatabaseClient: vi.fn().mockReturnValue('postgres') };
|
||||
});
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
let db: Knex;
|
||||
let tracker: Tracker;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = knex({ client: MockClient });
|
||||
tracker = getTracker();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
tracker.on.any('directus_roles').response({});
|
||||
tracker.on
|
||||
.select(/"directus_roles"."id" from "directus_roles" order by "directus_roles"."id" asc limit .*/)
|
||||
.response([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tracker.reset();
|
||||
});
|
||||
|
||||
describe('Services / Roles', () => {
|
||||
let service: RolesService;
|
||||
let checkForOtherAdminRolesSpy: SpyInstance;
|
||||
let checkForOtherAdminUsersSpy: SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new RolesService({
|
||||
knex: db,
|
||||
schema: {
|
||||
collections: {
|
||||
directus_roles: {
|
||||
collection: 'directus_roles',
|
||||
primary: 'id',
|
||||
singleton: false,
|
||||
sortField: null,
|
||||
note: null,
|
||||
accountability: null,
|
||||
fields: {
|
||||
id: {
|
||||
field: 'id',
|
||||
defaultValue: null,
|
||||
nullable: false,
|
||||
generated: true,
|
||||
type: 'integer',
|
||||
dbType: 'integer',
|
||||
precision: null,
|
||||
scale: null,
|
||||
special: [],
|
||||
note: null,
|
||||
validation: null,
|
||||
alias: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: [],
|
||||
},
|
||||
});
|
||||
|
||||
vi.spyOn(PermissionsService.prototype, 'deleteByQuery').mockResolvedValue([]);
|
||||
vi.spyOn(PresetsService.prototype, 'deleteByQuery').mockResolvedValue([]);
|
||||
vi.spyOn(UsersService.prototype, 'updateByQuery').mockResolvedValue([]);
|
||||
vi.spyOn(UsersService.prototype, 'deleteByQuery').mockResolvedValue([]);
|
||||
|
||||
// "as any" are needed since these are private methods
|
||||
checkForOtherAdminRolesSpy = vi
|
||||
.spyOn(RolesService.prototype as any, 'checkForOtherAdminRoles')
|
||||
.mockImplementation(() => vi.fn());
|
||||
checkForOtherAdminUsersSpy = vi
|
||||
.spyOn(RolesService.prototype as any, 'checkForOtherAdminUsers')
|
||||
.mockImplementation(() => vi.fn());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
checkForOtherAdminRolesSpy.mockClear();
|
||||
checkForOtherAdminUsersSpy.mockClear();
|
||||
});
|
||||
|
||||
describe('createOne', () => {
|
||||
it('should not checkForOtherAdminRoles', async () => {
|
||||
await service.createOne({});
|
||||
expect(checkForOtherAdminRolesSpy).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createMany', () => {
|
||||
it('should not checkForOtherAdminRoles', async () => {
|
||||
await service.createMany([{}]);
|
||||
expect(checkForOtherAdminRolesSpy).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOne', () => {
|
||||
it('should not checkForOtherAdminRoles', async () => {
|
||||
await service.updateOne(1, {});
|
||||
expect(checkForOtherAdminRolesSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkForOtherAdminRoles once and not checkForOtherAdminUsersSpy', async () => {
|
||||
await service.updateOne(1, { admin_access: false });
|
||||
expect(checkForOtherAdminRolesSpy).toBeCalledTimes(1);
|
||||
expect(checkForOtherAdminUsersSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkForOtherAdminRoles and checkForOtherAdminUsersSpy once', async () => {
|
||||
await service.updateOne(1, { admin_access: false, users: [1] });
|
||||
expect(checkForOtherAdminRolesSpy).toBeCalledTimes(1);
|
||||
expect(checkForOtherAdminUsersSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMany', () => {
|
||||
it('should not checkForOtherAdminRoles', async () => {
|
||||
await service.updateMany([1], {});
|
||||
expect(checkForOtherAdminRolesSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkForOtherAdminRoles once', async () => {
|
||||
await service.updateMany([1], { admin_access: false });
|
||||
expect(checkForOtherAdminRolesSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateBatch', () => {
|
||||
it('should not checkForOtherAdminRoles', async () => {
|
||||
await service.updateBatch([{ id: 1 }]);
|
||||
expect(checkForOtherAdminRolesSpy).not.toBeCalled();
|
||||
});
|
||||
it('should checkForOtherAdminRoles once', async () => {
|
||||
await service.updateBatch([{ id: 1, admin_access: false }]);
|
||||
expect(checkForOtherAdminRolesSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateByQuery', () => {
|
||||
it('should not checkForOtherAdminRoles', async () => {
|
||||
// mock return value for the following empty query
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
await service.updateByQuery({}, {});
|
||||
expect(checkForOtherAdminRolesSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkForOtherAdminRoles once', async () => {
|
||||
// mock return value for the following empty query
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
await service.updateByQuery({}, { admin_access: false });
|
||||
expect(checkForOtherAdminRolesSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should checkForOtherAdminRoles once', async () => {
|
||||
await service.deleteOne(1);
|
||||
expect(checkForOtherAdminRolesSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteMany', () => {
|
||||
it('should checkForOtherAdminRoles once', async () => {
|
||||
await service.deleteMany([1]);
|
||||
expect(checkForOtherAdminRolesSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteByQuery', () => {
|
||||
it('should checkForOtherAdminRoles once', async () => {
|
||||
// mock return value for the following empty query
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
await service.deleteByQuery({});
|
||||
expect(checkForOtherAdminRolesSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -68,10 +68,6 @@ export class RolesService extends ItemsService {
|
||||
}
|
||||
|
||||
async updateOne(key: PrimaryKey, data: Record<string, any>, opts?: MutationOptions): Promise<PrimaryKey> {
|
||||
if ('admin_access' in data && data.admin_access === false) {
|
||||
await this.checkForOtherAdminRoles([key]);
|
||||
}
|
||||
|
||||
if ('users' in data) {
|
||||
await this.checkForOtherAdminUsers(key, data.users);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import knex, { Knex } from 'knex';
|
||||
import { getTracker, MockClient, Tracker } from 'knex-mock-client';
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest';
|
||||
import { CollectionsService, FieldsService, RelationsService, SpecificationService } from '../../src/services';
|
||||
import { describe, beforeAll, afterEach, it, expect, vi, beforeEach, MockedFunction } from 'vitest';
|
||||
import { Collection } from '../types';
|
||||
|
||||
class Client_PG extends MockClient {}
|
||||
|
||||
@@ -34,49 +35,121 @@ describe('Integration Tests', () => {
|
||||
|
||||
describe('schema', () => {
|
||||
it('returns untyped schema for json fields', async () => {
|
||||
vi.spyOn(CollectionsService.prototype, 'readByQuery').mockImplementation(
|
||||
vi.fn().mockReturnValue([
|
||||
{
|
||||
vi.spyOn(CollectionsService.prototype, 'readByQuery').mockResolvedValue([
|
||||
{
|
||||
collection: 'test_table',
|
||||
meta: {
|
||||
accountability: 'all',
|
||||
collection: 'test_table',
|
||||
meta: {
|
||||
accountability: 'all',
|
||||
collection: 'test_table',
|
||||
group: null,
|
||||
hidden: false,
|
||||
icon: null,
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: null,
|
||||
},
|
||||
schema: {
|
||||
name: 'test_table',
|
||||
},
|
||||
group: null,
|
||||
hidden: false,
|
||||
icon: null,
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: null,
|
||||
},
|
||||
])
|
||||
);
|
||||
schema: {
|
||||
name: 'test_table',
|
||||
},
|
||||
},
|
||||
] as any[]);
|
||||
|
||||
vi.spyOn(FieldsService.prototype, 'readAll').mockImplementation(
|
||||
vi.fn().mockReturnValue([
|
||||
{
|
||||
vi.spyOn(FieldsService.prototype, 'readAll').mockResolvedValue([
|
||||
{
|
||||
collection: 'test_table',
|
||||
field: 'id',
|
||||
name: 'id',
|
||||
type: 'integer',
|
||||
meta: {
|
||||
id: 1,
|
||||
collection: 'test_table',
|
||||
conditions: null,
|
||||
display: null,
|
||||
display_options: null,
|
||||
field: 'id',
|
||||
type: 'integer',
|
||||
schema: {
|
||||
is_nullable: false,
|
||||
},
|
||||
group: null,
|
||||
hidden: true,
|
||||
interface: null,
|
||||
note: null,
|
||||
options: null,
|
||||
readonly: false,
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
},
|
||||
{
|
||||
schema: {
|
||||
comment: null,
|
||||
data_type: 'integer',
|
||||
default_value: null,
|
||||
foreign_key_column: null,
|
||||
foreign_key_schema: null,
|
||||
foreign_key_table: null,
|
||||
generation_expression: null,
|
||||
has_auto_increment: false,
|
||||
is_generated: false,
|
||||
is_nullable: false,
|
||||
is_primary_key: true,
|
||||
is_unique: true,
|
||||
max_length: null,
|
||||
name: 'id',
|
||||
numeric_precision: null,
|
||||
numeric_scale: null,
|
||||
table: 'test_table',
|
||||
},
|
||||
},
|
||||
{
|
||||
collection: 'test_table',
|
||||
field: 'blob',
|
||||
name: 'blob',
|
||||
type: 'json',
|
||||
meta: {
|
||||
id: 2,
|
||||
collection: 'test_table',
|
||||
conditions: null,
|
||||
display: null,
|
||||
display_options: null,
|
||||
field: 'blob',
|
||||
type: 'json',
|
||||
schema: {
|
||||
is_nullable: true,
|
||||
},
|
||||
group: null,
|
||||
hidden: true,
|
||||
interface: null,
|
||||
note: null,
|
||||
options: null,
|
||||
readonly: false,
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
},
|
||||
])
|
||||
);
|
||||
vi.spyOn(RelationsService.prototype, 'readAll').mockImplementation(vi.fn().mockReturnValue([]));
|
||||
schema: {
|
||||
comment: null,
|
||||
data_type: 'json',
|
||||
default_value: null,
|
||||
foreign_key_column: null,
|
||||
foreign_key_schema: null,
|
||||
foreign_key_table: null,
|
||||
generation_expression: null,
|
||||
has_auto_increment: false,
|
||||
is_generated: false,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
is_unique: false,
|
||||
max_length: null,
|
||||
name: 'blob',
|
||||
numeric_precision: null,
|
||||
numeric_scale: null,
|
||||
table: 'test_table',
|
||||
},
|
||||
},
|
||||
]);
|
||||
vi.spyOn(RelationsService.prototype, 'readAll').mockResolvedValue([]);
|
||||
|
||||
const spec = await service.oas.generate();
|
||||
expect(spec.components?.schemas).toEqual({
|
||||
@@ -99,7 +172,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
describe('path', () => {
|
||||
it('requestBody for CreateItems POST path should not have type in schema', async () => {
|
||||
const collection = {
|
||||
const collection: Collection = {
|
||||
collection: 'test_table',
|
||||
meta: {
|
||||
accountability: 'all',
|
||||
@@ -110,30 +183,64 @@ describe('Integration Tests', () => {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: null,
|
||||
translations: {},
|
||||
},
|
||||
schema: {
|
||||
name: 'test_table',
|
||||
},
|
||||
};
|
||||
|
||||
vi.spyOn(CollectionsService.prototype, 'readByQuery').mockImplementation(
|
||||
vi.fn().mockReturnValue([collection])
|
||||
);
|
||||
vi.spyOn(CollectionsService.prototype, 'readByQuery').mockResolvedValue([collection]);
|
||||
|
||||
vi.spyOn(FieldsService.prototype, 'readAll').mockImplementation(
|
||||
vi.fn().mockReturnValue([
|
||||
{
|
||||
collection: collection.collection,
|
||||
vi.spyOn(FieldsService.prototype, 'readAll').mockResolvedValue([
|
||||
{
|
||||
collection: collection.collection,
|
||||
field: 'id',
|
||||
name: 'id',
|
||||
type: 'integer',
|
||||
meta: {
|
||||
id: 1,
|
||||
collection: 'test_table',
|
||||
conditions: null,
|
||||
display: null,
|
||||
display_options: null,
|
||||
field: 'id',
|
||||
type: 'integer',
|
||||
schema: {
|
||||
is_nullable: false,
|
||||
},
|
||||
group: null,
|
||||
hidden: true,
|
||||
interface: null,
|
||||
note: null,
|
||||
options: null,
|
||||
readonly: false,
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
},
|
||||
])
|
||||
);
|
||||
vi.spyOn(RelationsService.prototype, 'readAll').mockImplementation(vi.fn().mockReturnValue([]));
|
||||
schema: {
|
||||
comment: null,
|
||||
data_type: 'integer',
|
||||
default_value: null,
|
||||
foreign_key_column: null,
|
||||
foreign_key_schema: null,
|
||||
foreign_key_table: null,
|
||||
generation_expression: null,
|
||||
has_auto_increment: false,
|
||||
is_generated: false,
|
||||
is_nullable: false,
|
||||
is_primary_key: true,
|
||||
is_unique: true,
|
||||
max_length: null,
|
||||
name: 'id',
|
||||
numeric_precision: null,
|
||||
numeric_scale: null,
|
||||
table: 'test_table',
|
||||
},
|
||||
},
|
||||
]);
|
||||
vi.spyOn(RelationsService.prototype, 'readAll').mockResolvedValue([]);
|
||||
|
||||
const spec = await service.oas.generate();
|
||||
|
||||
|
||||
@@ -4,12 +4,15 @@ import { getTracker, MockClient, Tracker } from 'knex-mock-client';
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, MockedFunction, SpyInstance, vi } from 'vitest';
|
||||
import { ItemsService, UsersService } from '.';
|
||||
import { InvalidPayloadException } from '../exceptions';
|
||||
import { RecordNotUniqueException } from '../exceptions/database/record-not-unique';
|
||||
|
||||
vi.mock('../../src/database/index', () => ({
|
||||
default: vi.fn(),
|
||||
getDatabaseClient: vi.fn().mockReturnValue('postgres'),
|
||||
}));
|
||||
|
||||
const testRoleId = '4ccdb196-14b3-4ed1-b9da-c1978be07ca2';
|
||||
|
||||
const testSchema = {
|
||||
collections: {
|
||||
directus_users: {
|
||||
@@ -49,18 +52,174 @@ describe('Integration Tests', () => {
|
||||
tracker = getTracker();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
tracker.on.any('directus_users').response({});
|
||||
|
||||
// mock notifications update query in deleteOne/deleteMany/deleteByQuery methods
|
||||
tracker.on.update('directus_notifications').response({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tracker.reset();
|
||||
});
|
||||
|
||||
describe('Services / Users', () => {
|
||||
let service: UsersService;
|
||||
let superUpdateManySpy: SpyInstance;
|
||||
let checkUniqueEmailsSpy: SpyInstance;
|
||||
let checkPasswordPolicySpy: SpyInstance;
|
||||
let checkRemainingAdminExistenceSpy: SpyInstance;
|
||||
let checkRemainingActiveAdminSpy: SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new UsersService({
|
||||
knex: db,
|
||||
schema: {
|
||||
collections: {
|
||||
directus_users: {
|
||||
collection: 'directus_users',
|
||||
primary: 'id',
|
||||
singleton: false,
|
||||
sortField: null,
|
||||
note: null,
|
||||
accountability: null,
|
||||
fields: {
|
||||
id: {
|
||||
field: 'id',
|
||||
defaultValue: null,
|
||||
nullable: false,
|
||||
generated: true,
|
||||
type: 'integer',
|
||||
dbType: 'integer',
|
||||
precision: null,
|
||||
scale: null,
|
||||
special: [],
|
||||
note: null,
|
||||
validation: null,
|
||||
alias: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: [],
|
||||
},
|
||||
});
|
||||
|
||||
superUpdateManySpy = vi.spyOn(ItemsService.prototype, 'updateMany');
|
||||
|
||||
// "as any" are needed since these are private methods
|
||||
checkUniqueEmailsSpy = vi
|
||||
.spyOn(UsersService.prototype as any, 'checkUniqueEmails')
|
||||
.mockImplementation(() => vi.fn());
|
||||
checkPasswordPolicySpy = vi
|
||||
.spyOn(UsersService.prototype as any, 'checkPasswordPolicy')
|
||||
.mockResolvedValue(() => vi.fn());
|
||||
checkRemainingAdminExistenceSpy = vi
|
||||
.spyOn(UsersService.prototype as any, 'checkRemainingAdminExistence')
|
||||
.mockResolvedValue(() => vi.fn());
|
||||
checkRemainingActiveAdminSpy = vi
|
||||
.spyOn(UsersService.prototype as any, 'checkRemainingActiveAdmin')
|
||||
.mockResolvedValue(() => vi.fn());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
checkUniqueEmailsSpy.mockClear();
|
||||
checkPasswordPolicySpy.mockClear();
|
||||
checkRemainingAdminExistenceSpy.mockClear();
|
||||
checkRemainingActiveAdminSpy.mockClear();
|
||||
});
|
||||
|
||||
describe('createOne', () => {
|
||||
it('should not checkUniqueEmails', async () => {
|
||||
await service.createOne({});
|
||||
expect(checkUniqueEmailsSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkUniqueEmails once', async () => {
|
||||
await service.createOne({ email: 'test@example.com' });
|
||||
expect(checkUniqueEmailsSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not checkPasswordPolicy', async () => {
|
||||
await service.createOne({});
|
||||
expect(checkPasswordPolicySpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkPasswordPolicy once', async () => {
|
||||
await service.createOne({ password: 'testpassword' });
|
||||
expect(checkPasswordPolicySpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createMany', () => {
|
||||
it('should not checkUniqueEmails', async () => {
|
||||
await service.createMany([{}]);
|
||||
expect(checkUniqueEmailsSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkUniqueEmails once', async () => {
|
||||
await service.createMany([{ email: 'test@example.com' }]);
|
||||
expect(checkUniqueEmailsSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not checkPasswordPolicy', async () => {
|
||||
await service.createMany([{}]);
|
||||
expect(checkPasswordPolicySpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkPasswordPolicy once', async () => {
|
||||
await service.createMany([{ password: 'testpassword' }]);
|
||||
expect(checkPasswordPolicySpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOne', () => {
|
||||
it('should not checkRemainingAdminExistence', async () => {
|
||||
// mock newRole query in updateMany (called by ItemsService updateOne)
|
||||
tracker.on.select(/select "admin_access" from "directus_roles"/).response({ admin_access: true });
|
||||
|
||||
await service.updateOne(1, { role: testRoleId });
|
||||
expect(checkRemainingAdminExistenceSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkRemainingAdminExistence once', async () => {
|
||||
// mock newRole query in updateMany (called by ItemsService updateOne)
|
||||
tracker.on.select(/select "admin_access" from "directus_roles"/).response({ admin_access: false });
|
||||
|
||||
await service.updateOne(1, { role: testRoleId });
|
||||
expect(checkRemainingAdminExistenceSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not checkRemainingActiveAdmin', async () => {
|
||||
await service.updateOne(1, {});
|
||||
expect(checkRemainingActiveAdminSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkRemainingActiveAdmin once', async () => {
|
||||
await service.updateOne(1, { status: 'inactive' });
|
||||
expect(checkRemainingActiveAdminSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not checkUniqueEmails', async () => {
|
||||
await service.updateOne(1, {});
|
||||
expect(checkUniqueEmailsSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkUniqueEmails once', async () => {
|
||||
await service.updateOne(1, { email: 'test@example.com' });
|
||||
expect(checkUniqueEmailsSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not checkPasswordPolicy', async () => {
|
||||
await service.updateOne(1, {});
|
||||
expect(checkPasswordPolicySpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkPasswordPolicy once', async () => {
|
||||
await service.updateOne(1, { password: 'testpassword' });
|
||||
expect(checkPasswordPolicySpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each(['provider', 'external_identifier'])(
|
||||
'should throw InvalidPayloadException for non-admin users when updating "%s" field',
|
||||
async (field) => {
|
||||
@@ -113,6 +272,63 @@ describe('Integration Tests', () => {
|
||||
});
|
||||
|
||||
describe('updateMany', () => {
|
||||
it('should not checkRemainingAdminExistence', async () => {
|
||||
// mock newRole query in updateMany
|
||||
tracker.on.select(/select "admin_access" from "directus_roles"/).response({ admin_access: true });
|
||||
|
||||
await service.updateMany([1], { role: testRoleId });
|
||||
expect(checkRemainingAdminExistenceSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkRemainingAdminExistence once', async () => {
|
||||
// mock newRole query in updateMany
|
||||
tracker.on.select(/select "admin_access" from "directus_roles"/).response({ admin_access: false });
|
||||
|
||||
await service.updateMany([1], { role: testRoleId });
|
||||
expect(checkRemainingAdminExistenceSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not checkRemainingActiveAdmin', async () => {
|
||||
await service.updateMany([1], {});
|
||||
expect(checkRemainingActiveAdminSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkRemainingActiveAdmin once', async () => {
|
||||
await service.updateMany([1], { status: 'inactive' });
|
||||
expect(checkRemainingActiveAdminSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not checkUniqueEmails', async () => {
|
||||
await service.updateMany([1], {});
|
||||
expect(checkUniqueEmailsSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkUniqueEmails once', async () => {
|
||||
await service.updateMany([1], { email: 'test@example.com' });
|
||||
expect(checkUniqueEmailsSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw RecordNotUniqueException for multiple keys with same email', async () => {
|
||||
expect.assertions(2); // to ensure both assertions in the catch block are reached
|
||||
|
||||
try {
|
||||
await service.updateMany([1, 2], { email: 'test@example.com' });
|
||||
} catch (err: any) {
|
||||
expect(err.message).toBe(`Field "email" has to be unique.`);
|
||||
expect(err).toBeInstanceOf(RecordNotUniqueException);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not checkPasswordPolicy', async () => {
|
||||
await service.updateMany([1], {});
|
||||
expect(checkPasswordPolicySpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkPasswordPolicy once', async () => {
|
||||
await service.updateMany([1], { password: 'testpassword' });
|
||||
expect(checkPasswordPolicySpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each(['provider', 'external_identifier'])(
|
||||
'should throw InvalidPayloadException for non-admin users when updating "%s" field',
|
||||
async (field) => {
|
||||
@@ -165,6 +381,81 @@ describe('Integration Tests', () => {
|
||||
});
|
||||
|
||||
describe('updateByQuery', () => {
|
||||
it('should not checkRemainingAdminExistence', async () => {
|
||||
// mock newRole query in updateMany (called by ItemsService updateByQuery)
|
||||
tracker.on.select(/select "admin_access" from "directus_roles"/).response({ admin_access: true });
|
||||
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
|
||||
await service.updateByQuery({}, { role: testRoleId });
|
||||
expect(checkRemainingAdminExistenceSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkRemainingAdminExistence once', async () => {
|
||||
// mock newRole query in updateMany (called by ItemsService updateByQuery)
|
||||
tracker.on.select(/select "admin_access" from "directus_roles"/).response({ admin_access: false });
|
||||
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
|
||||
await service.updateByQuery({}, { role: testRoleId });
|
||||
expect(checkRemainingAdminExistenceSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not checkRemainingActiveAdmin', async () => {
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
|
||||
await service.updateByQuery({}, {});
|
||||
expect(checkRemainingActiveAdminSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkRemainingActiveAdmin once', async () => {
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
|
||||
await service.updateByQuery({}, { status: 'inactive' });
|
||||
expect(checkRemainingActiveAdminSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not checkUniqueEmails', async () => {
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
|
||||
await service.updateByQuery({}, {});
|
||||
expect(checkUniqueEmailsSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkUniqueEmails once', async () => {
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
|
||||
await service.updateByQuery({}, { email: 'test@example.com' });
|
||||
expect(checkUniqueEmailsSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw RecordNotUniqueException for multiple keys with same email', async () => {
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1, 2]);
|
||||
|
||||
expect.assertions(2); // to ensure both assertions in the catch block are reached
|
||||
|
||||
try {
|
||||
await service.updateByQuery({}, { email: 'test@example.com' });
|
||||
} catch (err: any) {
|
||||
expect(err.message).toBe(`Field "email" has to be unique.`);
|
||||
expect(err).toBeInstanceOf(RecordNotUniqueException);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not checkPasswordPolicy', async () => {
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
|
||||
await service.updateByQuery({}, {});
|
||||
expect(checkPasswordPolicySpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkPasswordPolicy once', async () => {
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
|
||||
await service.updateByQuery({}, { password: 'testpassword' });
|
||||
expect(checkPasswordPolicySpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each(['provider', 'external_identifier'])(
|
||||
'should throw InvalidPayloadException for non-admin users when updating "%s" field',
|
||||
async (field) => {
|
||||
@@ -174,7 +465,7 @@ describe('Integration Tests', () => {
|
||||
accountability: { role: 'test', admin: false },
|
||||
});
|
||||
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockImplementation(vi.fn(() => Promise.resolve([1])));
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
|
||||
const promise = service.updateByQuery({}, { [field]: 'test' });
|
||||
|
||||
@@ -196,7 +487,7 @@ describe('Integration Tests', () => {
|
||||
accountability: { role: 'admin', admin: true },
|
||||
});
|
||||
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockImplementation(vi.fn(() => Promise.resolve([1])));
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
|
||||
const promise = service.updateByQuery({}, { [field]: 'test' });
|
||||
|
||||
@@ -212,7 +503,7 @@ describe('Integration Tests', () => {
|
||||
schema: testSchema,
|
||||
});
|
||||
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockImplementation(vi.fn(() => Promise.resolve([1])));
|
||||
vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockResolvedValue([1]);
|
||||
|
||||
const promise = service.updateByQuery({}, { [field]: 'test' });
|
||||
|
||||
@@ -221,5 +512,29 @@ describe('Integration Tests', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should checkRemainingAdminExistence once', async () => {
|
||||
await service.deleteOne(1);
|
||||
expect(checkRemainingAdminExistenceSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteMany', () => {
|
||||
it('should checkRemainingAdminExistence once', async () => {
|
||||
await service.deleteMany([1]);
|
||||
expect(checkRemainingAdminExistenceSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteByQuery', () => {
|
||||
it('should checkRemainingAdminExistence once', async () => {
|
||||
// mock return value for the following empty query
|
||||
vi.spyOn(ItemsService.prototype, 'readByQuery').mockResolvedValue([{ id: 1 }]);
|
||||
|
||||
await service.deleteByQuery({});
|
||||
expect(checkRemainingAdminExistenceSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
118
api/src/services/webhooks.test.ts
Normal file
118
api/src/services/webhooks.test.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import knex, { Knex } from 'knex';
|
||||
import { getTracker, MockClient, Tracker } from 'knex-mock-client';
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest';
|
||||
import { WebhooksService } from '.';
|
||||
import { getMessenger } from '../messenger';
|
||||
|
||||
vi.mock('../../src/database/index', () => {
|
||||
return { __esModule: true, default: vi.fn(), getDatabaseClient: vi.fn().mockReturnValue('postgres') };
|
||||
});
|
||||
|
||||
vi.mock('../messenger', () => {
|
||||
return { getMessenger: vi.fn().mockReturnValue({ publish: vi.fn() }) };
|
||||
});
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
let db: Knex;
|
||||
let tracker: Tracker;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = knex({ client: MockClient });
|
||||
tracker = getTracker();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
tracker.on.any('directus_webhooks').response({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tracker.reset();
|
||||
});
|
||||
|
||||
describe('Services / Webhooks', () => {
|
||||
let service: WebhooksService;
|
||||
let messengerPublishSpy: SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new WebhooksService({
|
||||
knex: db,
|
||||
schema: {
|
||||
collections: {
|
||||
directus_webhooks: {
|
||||
collection: 'directus_webhooks',
|
||||
primary: 'id',
|
||||
singleton: false,
|
||||
sortField: null,
|
||||
note: null,
|
||||
accountability: null,
|
||||
fields: {
|
||||
id: {
|
||||
field: 'id',
|
||||
defaultValue: null,
|
||||
nullable: false,
|
||||
generated: true,
|
||||
type: 'integer',
|
||||
dbType: 'integer',
|
||||
precision: null,
|
||||
scale: null,
|
||||
special: [],
|
||||
note: null,
|
||||
validation: null,
|
||||
alias: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: [],
|
||||
},
|
||||
});
|
||||
messengerPublishSpy = vi.spyOn(getMessenger(), 'publish');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
messengerPublishSpy.mockClear();
|
||||
});
|
||||
|
||||
describe('createOne', () => {
|
||||
it('should publish webhooks reload message once', async () => {
|
||||
await service.createOne({});
|
||||
expect(messengerPublishSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createMany', () => {
|
||||
it('should publish webhooks reload message once', async () => {
|
||||
await service.createMany([{}]);
|
||||
expect(messengerPublishSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOne', () => {
|
||||
it('should publish webhooks reload message once', async () => {
|
||||
await service.updateOne(1, {});
|
||||
expect(messengerPublishSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMany', () => {
|
||||
it('should publish webhooks reload message once', async () => {
|
||||
await service.updateMany([1], {});
|
||||
expect(messengerPublishSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should publish webhooks reload message once', async () => {
|
||||
await service.deleteOne(1);
|
||||
expect(messengerPublishSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteMany', () => {
|
||||
it('should publish webhooks reload message once', async () => {
|
||||
await service.deleteMany([1]);
|
||||
expect(messengerPublishSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,24 +22,12 @@ export class WebhooksService extends ItemsService<Webhook> {
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateOne(key: PrimaryKey, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey> {
|
||||
const result = await super.updateOne(key, data, opts);
|
||||
this.messenger.publish('webhooks', { type: 'reload' });
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
const result = await super.updateMany(keys, data, opts);
|
||||
this.messenger.publish('webhooks', { type: 'reload' });
|
||||
return result;
|
||||
}
|
||||
|
||||
async deleteOne(key: PrimaryKey, opts?: MutationOptions): Promise<PrimaryKey> {
|
||||
const result = await super.deleteOne(key, opts);
|
||||
this.messenger.publish('webhooks', { type: 'reload' });
|
||||
return result;
|
||||
}
|
||||
|
||||
async deleteMany(keys: PrimaryKey[], opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
const result = await super.deleteMany(keys, opts);
|
||||
this.messenger.publish('webhooks', { type: 'reload' });
|
||||
|
||||
@@ -103,12 +103,8 @@ describe('applySnapshot', () => {
|
||||
// Stop call to db later on in apply-snapshot
|
||||
vi.spyOn(getSchema, 'getSchema').mockReturnValue(Promise.resolve(snapshotApplyTestSchema));
|
||||
// We are not actually testing that createOne works, just that is is called correctly
|
||||
const createOneCollectionSpy = vi
|
||||
.spyOn(CollectionsService.prototype, 'createOne')
|
||||
.mockImplementation(vi.fn().mockReturnValue([]));
|
||||
const createFieldSpy = vi
|
||||
.spyOn(FieldsService.prototype, 'createField')
|
||||
.mockImplementation(vi.fn().mockReturnValue([]));
|
||||
const createOneCollectionSpy = vi.spyOn(CollectionsService.prototype, 'createOne').mockResolvedValue('test');
|
||||
const createFieldSpy = vi.spyOn(FieldsService.prototype, 'createField').mockResolvedValue();
|
||||
|
||||
await applySnapshot(snapshotCreateCollectionNotNested, {
|
||||
database: db,
|
||||
@@ -254,12 +250,8 @@ describe('applySnapshot', () => {
|
||||
// Stop call to db later on in apply-snapshot
|
||||
vi.spyOn(getSchema, 'getSchema').mockReturnValue(Promise.resolve(snapshotApplyTestSchema));
|
||||
// We are not actually testing that createOne works, just that is is called correctly
|
||||
const createOneCollectionSpy = vi
|
||||
.spyOn(CollectionsService.prototype, 'createOne')
|
||||
.mockImplementation(vi.fn().mockReturnValue([]));
|
||||
const createFieldSpy = vi
|
||||
.spyOn(FieldsService.prototype, 'createField')
|
||||
.mockImplementation(vi.fn().mockReturnValue([]));
|
||||
const createOneCollectionSpy = vi.spyOn(CollectionsService.prototype, 'createOne').mockResolvedValue('test');
|
||||
const createFieldSpy = vi.spyOn(FieldsService.prototype, 'createField').mockResolvedValue();
|
||||
|
||||
await applySnapshot(snapshotCreateCollection, {
|
||||
database: db,
|
||||
@@ -290,9 +282,7 @@ describe('applySnapshot', () => {
|
||||
// Stop call to db later on in apply-snapshot
|
||||
vi.spyOn(getSchema, 'getSchema').mockReturnValue(Promise.resolve(snapshotApplyTestSchema));
|
||||
// We are not actually testing that deleteOne works, just that is is called correctly
|
||||
const deleteOneCollectionSpy = vi
|
||||
.spyOn(CollectionsService.prototype, 'deleteOne')
|
||||
.mockImplementation(vi.fn().mockReturnValue([]));
|
||||
const deleteOneCollectionSpy = vi.spyOn(CollectionsService.prototype, 'deleteOne').mockResolvedValue('test');
|
||||
|
||||
await applySnapshot(snapshotToApply, {
|
||||
database: db,
|
||||
|
||||
Reference in New Issue
Block a user