Skip to content

Commit

Permalink
N21-2045 CTL Admin API (#5172)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarvinOehlerkingCap authored Aug 9, 2024
1 parent 8c4646e commit 4c7de71
Show file tree
Hide file tree
Showing 20 changed files with 817 additions and 1 deletion.
4 changes: 3 additions & 1 deletion apps/server/src/modules/server/admin-api.server.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { MikroOrmModule } from '@mikro-orm/nestjs';
import { DeletionApiModule } from '@modules/deletion/deletion-api.module';
import { FileEntity } from '@modules/files/entity';
import { LegacySchoolAdminApiModule } from '@modules/legacy-school/legacy-school-admin.api-module';
import { ToolAdminApiModule } from '@modules/tool/tool-admin-api.module';
import { UserAdminApiModule } from '@modules/user/user-admin-api.module';
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { CqrsModule } from '@nestjs/cqrs';
import { ALL_ENTITIES } from '@shared/domain/entity';
import { DB_PASSWORD, DB_URL, DB_USERNAME, createConfigModuleOptions } from '@src/config';
import { createConfigModuleOptions, DB_PASSWORD, DB_URL, DB_USERNAME } from '@src/config';
import { LoggerModule } from '@src/core/logger';
import { MongoDatabaseModuleOptions, MongoMemoryDatabaseModule } from '@src/infra/database';
import { EtherpadClientModule } from '@src/infra/etherpad-client';
Expand All @@ -21,6 +22,7 @@ const serverModules = [
DeletionApiModule,
LegacySchoolAdminApiModule,
UserAdminApiModule,
ToolAdminApiModule,
EtherpadClientModule.register({
apiKey: Configuration.has('ETHERPAD__API_KEY') ? (Configuration.get('ETHERPAD__API_KEY') as string) : undefined,
basePath: Configuration.has('ETHERPAD__URI') ? (Configuration.get('ETHERPAD__URI') as string) : undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ContextExternalTool } from '../domain';
import { ContextExternalToolRequestMapper, ContextExternalToolResponseMapper } from '../mapper';
import { AdminApiContextExternalToolUc } from '../uc';
import { ContextExternalToolDto } from '../uc/dto/context-external-tool.types';
import { ContextExternalToolPostParams, ContextExternalToolResponse } from './dto';

@ApiTags('AdminApi: Context External Tool')
@UseGuards(AuthGuard('api-key'))
@Controller('admin/tools/context-external-tools')
export class AdminApiContextExternalToolController {
constructor(private readonly adminApiContextExternalToolUc: AdminApiContextExternalToolUc) {}

@Post()
@ApiOperation({ summary: 'Creates a ContextExternalTool' })
async createContextExternalTool(@Body() body: ContextExternalToolPostParams): Promise<ContextExternalToolResponse> {
const contextExternalToolProps: ContextExternalToolDto =
ContextExternalToolRequestMapper.mapContextExternalToolRequest(body);

const contextExternalTool: ContextExternalTool = await this.adminApiContextExternalToolUc.createContextExternalTool(
contextExternalToolProps
);

const response: ContextExternalToolResponse =
ContextExternalToolResponseMapper.mapContextExternalToolResponse(contextExternalTool);

return response;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { EntityManager, MikroORM } from '@mikro-orm/core';
import { serverConfig } from '@modules/server';
import { AdminApiServerTestModule } from '@modules/server/admin-api.server.module';
import { HttpStatus, INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { Course, SchoolEntity } from '@shared/domain/entity';
import { courseFactory, schoolEntityFactory, TestApiClient } from '@shared/testing';
import { ToolContextType } from '../../../common/enum';
import { ExternalToolResponse } from '../../../external-tool/controller/dto';
import { CustomParameterScope, CustomParameterType, ExternalToolEntity } from '../../../external-tool/entity';
import { customParameterEntityFactory, externalToolEntityFactory } from '../../../external-tool/testing';
import { SchoolExternalToolEntity } from '../../../school-external-tool/entity';
import { schoolExternalToolEntityFactory } from '../../../school-external-tool/testing';
import { ContextExternalToolEntity } from '../../entity';
import { ContextExternalToolPostParams, ContextExternalToolResponse } from '../dto';

describe('AdminApiContextExternalTool (API)', () => {
let app: INestApplication;
let em: EntityManager;
let orm: MikroORM;
let testApiClient: TestApiClient;

const apiKey = 'validApiKey';

const basePath = 'admin/tools/context-external-tools';

beforeAll(async () => {
serverConfig().ADMIN_API__ALLOWED_API_KEYS = [apiKey];

const module: TestingModule = await Test.createTestingModule({
imports: [AdminApiServerTestModule],
}).compile();

app = module.createNestApplication();
await app.init();
em = module.get(EntityManager);
orm = app.get(MikroORM);
testApiClient = new TestApiClient(app, basePath, apiKey, true);
});

afterAll(async () => {
await app.close();
});

afterEach(async () => {
await orm.getSchemaGenerator().clearDatabase();
});

describe('[POST] admin/tools/context-external-tools', () => {
describe('when authenticating without an api token', () => {
it('should return unauthorized', async () => {
const client = new TestApiClient(app, basePath);

const response = await client.post();

expect(response.status).toEqual(HttpStatus.UNAUTHORIZED);
});
});

describe('when authenticating with an invalid api token', () => {
it('should return unauthorized', async () => {
const client = new TestApiClient(app, basePath, 'invalidApiKey', true);

const response = await client.post();

expect(response.status).toEqual(HttpStatus.UNAUTHORIZED);
});
});

describe('when authenticating with a valid api token', () => {
const setup = async () => {
const school: SchoolEntity = schoolEntityFactory.buildWithId();
const course: Course = courseFactory.buildWithId({ school });
const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({
parameters: [
customParameterEntityFactory.build({
name: 'param1',
scope: CustomParameterScope.CONTEXT,
type: CustomParameterType.STRING,
isOptional: false,
}),
customParameterEntityFactory.build({
name: 'param2',
scope: CustomParameterScope.CONTEXT,
type: CustomParameterType.BOOLEAN,
isOptional: true,
}),
],
});
const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({
tool: externalToolEntity,
school,
schoolParameters: [],
});

const postParams: ContextExternalToolPostParams = {
schoolToolId: schoolExternalToolEntity.id,
contextId: course.id,
displayName: course.name,
contextType: ToolContextType.COURSE,
parameters: [
{ name: 'param1', value: 'value' },
{ name: 'param2', value: 'true' },
],
};

await em.persistAndFlush([school, externalToolEntity]);
em.clear();

return {
postParams,
};
};

it('should create a context external tool', async () => {
const { postParams } = await setup();

const response = await testApiClient.post().send(postParams);

const body: ExternalToolResponse = response.body as ExternalToolResponse;

expect(response.statusCode).toEqual(HttpStatus.CREATED);
expect(body).toEqual<ContextExternalToolResponse>({
id: expect.any(String),
schoolToolId: postParams.schoolToolId,
contextId: postParams.contextId,
displayName: postParams.displayName,
contextType: postParams.contextType,
parameters: [
{ name: 'param1', value: 'value' },
{ name: 'param2', value: 'true' },
],
});

const contextExternalTool: ContextExternalToolEntity | null = await em.findOne(ContextExternalToolEntity, {
id: body.id,
});
expect(contextExternalTool).toBeDefined();
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './tool-context.controller';
export { AdminApiContextExternalToolController } from './admin-api-context-external-tool.controller';
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { ContextExternalToolService } from '../service';
import { contextExternalToolFactory } from '../testing';
import { AdminApiContextExternalToolUc } from './admin-api-context-external-tool.uc';

describe(AdminApiContextExternalToolUc.name, () => {
let module: TestingModule;
let uc: AdminApiContextExternalToolUc;

let contextExternalToolService: DeepMocked<ContextExternalToolService>;

beforeAll(async () => {
module = await Test.createTestingModule({
providers: [
AdminApiContextExternalToolUc,
{
provide: ContextExternalToolService,
useValue: createMock<ContextExternalToolService>(),
},
],
}).compile();

uc = module.get(AdminApiContextExternalToolUc);
contextExternalToolService = module.get(ContextExternalToolService);
});

afterAll(async () => {
await module.close();
});

afterEach(() => {
jest.resetAllMocks();
});

describe('createExternalTool', () => {
describe('when creating a tool', () => {
const setup = () => {
const contextExternalTool = contextExternalToolFactory.build();

contextExternalToolService.saveContextExternalTool.mockResolvedValueOnce(contextExternalTool);

return {
contextExternalTool,
};
};

it('should save the tool', async () => {
const { contextExternalTool } = setup();

await uc.createContextExternalTool(contextExternalTool.getProps());

expect(contextExternalToolService.saveContextExternalTool).toHaveBeenCalledWith(contextExternalTool);
});

it('should return the tool', async () => {
const { contextExternalTool } = setup();

const result = await uc.createContextExternalTool(contextExternalTool.getProps());

expect(result).toEqual(contextExternalTool);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { ContextExternalTool } from '../domain';
import { ContextExternalToolService } from '../service';
import { ContextExternalToolDto } from './dto/context-external-tool.types';

@Injectable()
export class AdminApiContextExternalToolUc {
constructor(private readonly contextExternalToolService: ContextExternalToolService) {}

async createContextExternalTool(contextExternalToolProps: ContextExternalToolDto): Promise<ContextExternalTool> {
const contextExternalTool: ContextExternalTool = new ContextExternalTool(contextExternalToolProps);

const createdTool: ContextExternalTool = await this.contextExternalToolService.saveContextExternalTool(
contextExternalTool
);

return createdTool;
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { AdminApiContextExternalToolUc } from './admin-api-context-external-tool.uc';
export * from './context-external-tool.uc';
export * from './tool-reference.uc';
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ExternalTool } from '../domain';

import { ExternalToolRequestMapper, ExternalToolResponseMapper } from '../mapper';
import { AdminApiExternalToolUc, ExternalToolCreate } from '../uc';
import { ExternalToolCreateParams, ExternalToolResponse } from './dto';

@ApiTags('AdminApi: External Tools')
@UseGuards(AuthGuard('api-key'))
@Controller('admin/tools/external-tools')
export class AdminApiExternalToolController {
constructor(
private readonly adminApiExternalToolUc: AdminApiExternalToolUc,
private readonly externalToolDOMapper: ExternalToolRequestMapper
) {}

@Post()
@ApiOperation({ summary: 'Creates an ExternalTool' })
async createExternalTool(@Body() externalToolParams: ExternalToolCreateParams): Promise<ExternalToolResponse> {
const externalTool: ExternalToolCreate = this.externalToolDOMapper.mapCreateRequest(externalToolParams);

const created: ExternalTool = await this.adminApiExternalToolUc.createExternalTool(externalTool);

const mapped: ExternalToolResponse = ExternalToolResponseMapper.mapToExternalToolResponse(created);

return mapped;
}
}
Loading

0 comments on commit 4c7de71

Please sign in to comment.