first commit
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
export async function createConnection() {
|
||||
return await mysql.createConnection({
|
||||
host: '127.0.0.1',
|
||||
port: 13307,
|
||||
user: 'root',
|
||||
password: 'userfeedback',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { join } from 'path';
|
||||
import { createConnection } from 'typeorm';
|
||||
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
|
||||
|
||||
import { createConnection as connect } from './database-utils';
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.MYSQL_PRIMARY_URL =
|
||||
'mysql://root:userfeedback@localhost:13307/integration';
|
||||
process.env.MYSQL_SECONDARY_URLS = JSON.stringify([
|
||||
'mysql://root:userfeedback@localhost:13307/integration',
|
||||
]);
|
||||
process.env.MASTER_API_KEY = 'master-api-key';
|
||||
process.env.AUTO_FEEDBACK_DELETION_ENABLED = 'true';
|
||||
process.env.AUTO_FEEDBACK_DELETION_PERIOD_DAYS = '30';
|
||||
|
||||
async function createTestDatabase() {
|
||||
const connection = await connect();
|
||||
|
||||
await connection.query(`DROP DATABASE IF EXISTS integration;`);
|
||||
await connection.query(`CREATE DATABASE IF NOT EXISTS integration;`);
|
||||
await connection.end();
|
||||
}
|
||||
|
||||
async function runMigrations() {
|
||||
const connection = await createConnection({
|
||||
type: 'mysql',
|
||||
host: '127.0.0.1',
|
||||
port: 13307,
|
||||
username: 'root',
|
||||
password: 'userfeedback',
|
||||
database: 'integration',
|
||||
migrations: [
|
||||
join(
|
||||
__dirname,
|
||||
'../src/configs/modules/typeorm-config/migrations/*.{ts,js}',
|
||||
),
|
||||
],
|
||||
migrationsTableName: 'migrations',
|
||||
namingStrategy: new SnakeNamingStrategy(),
|
||||
timezone: '+00:00',
|
||||
});
|
||||
|
||||
await connection.runMigrations();
|
||||
await connection.close();
|
||||
}
|
||||
|
||||
export default async () => {
|
||||
await createTestDatabase();
|
||||
await runMigrations();
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { createConnection as connect } from './database-utils';
|
||||
|
||||
async function dropTestDatabase() {
|
||||
const connection = await connect();
|
||||
|
||||
await connection.query(`DROP DATABASE IF EXISTS integration;`);
|
||||
await connection.end();
|
||||
}
|
||||
|
||||
export default async () => {
|
||||
await dropTestDatabase();
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"displayName": "api-integration",
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"moduleNameMapper": {
|
||||
"^@/(.*)$": ["<rootDir>/../src/$1"]
|
||||
},
|
||||
"testRegex": ".integration-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"transformIgnorePatterns": ["node_modules/(?!@faker-js|uuid)"],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/../integration-test/jest-integration.setup.ts"
|
||||
],
|
||||
"globalSetup": "<rootDir>/../integration-test/global.setup.ts",
|
||||
"globalTeardown": "<rootDir>/../integration-test/global.teardown.ts"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
jest.mock('@nestjs-modules/mailer/dist/adapters/handlebars.adapter', () => {
|
||||
return {
|
||||
HandlebarsAdapter: jest.fn(),
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Server } from 'net';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
import request from 'supertest';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import { initializeTransactionalContext } from 'typeorm-transactional';
|
||||
|
||||
import { AppModule } from '@/app.module';
|
||||
import { OpensearchRepository } from '@/common/repositories';
|
||||
import { AuthService } from '@/domains/admin/auth/auth.service';
|
||||
import { ApiKeyService } from '@/domains/admin/project/api-key/api-key.service';
|
||||
import { CreateApiKeyRequestDto } from '@/domains/admin/project/api-key/dtos/requests';
|
||||
import type { FindApiKeysResponseDto } from '@/domains/admin/project/api-key/dtos/responses';
|
||||
import type { ProjectEntity } from '@/domains/admin/project/project/project.entity';
|
||||
import { ProjectService } from '@/domains/admin/project/project/project.service';
|
||||
import { SetupTenantRequestDto } from '@/domains/admin/tenant/dtos/requests';
|
||||
import { TenantService } from '@/domains/admin/tenant/tenant.service';
|
||||
import { clearAllEntities, signInTestUser } from '@/test-utils/util-functions';
|
||||
|
||||
describe('ApiKeyController (integration)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let dataSource: DataSource;
|
||||
let authService: AuthService;
|
||||
let tenantService: TenantService;
|
||||
let projectService: ProjectService;
|
||||
let _apiKeyService: ApiKeyService;
|
||||
let configService: ConfigService;
|
||||
let opensearchRepository: OpensearchRepository;
|
||||
|
||||
let project: ProjectEntity;
|
||||
let accessToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
initializeTransactionalContext();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
await app.init();
|
||||
|
||||
dataSource = module.get(getDataSourceToken());
|
||||
authService = module.get(AuthService);
|
||||
tenantService = module.get(TenantService);
|
||||
projectService = module.get(ProjectService);
|
||||
_apiKeyService = module.get(ApiKeyService);
|
||||
configService = module.get(ConfigService);
|
||||
opensearchRepository = module.get(OpensearchRepository);
|
||||
|
||||
await clearAllEntities(module);
|
||||
if (configService.get('opensearch.use')) {
|
||||
await opensearchRepository.deleteAllIndexes();
|
||||
}
|
||||
|
||||
const dto = new SetupTenantRequestDto();
|
||||
dto.siteName = faker.string.sample();
|
||||
dto.password = '12345678';
|
||||
await tenantService.create(dto);
|
||||
|
||||
project = await projectService.create({
|
||||
name: faker.lorem.words(),
|
||||
description: faker.lorem.lines(1),
|
||||
timezone: {
|
||||
countryCode: 'KR',
|
||||
name: 'Asia/Seoul',
|
||||
offset: '+09:00',
|
||||
},
|
||||
});
|
||||
|
||||
const { jwt } = await signInTestUser(dataSource, authService);
|
||||
accessToken = jwt.accessToken;
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/api-keys (POST)', () => {
|
||||
it('should create an API key', async () => {
|
||||
const dto = new CreateApiKeyRequestDto();
|
||||
dto.value = 'TestApiKey1234567890';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/api-keys`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(201)
|
||||
.then(
|
||||
({
|
||||
body,
|
||||
}: {
|
||||
body: {
|
||||
id: number;
|
||||
value: string;
|
||||
createdAt: Date;
|
||||
};
|
||||
}) => {
|
||||
expect(body).toHaveProperty('id');
|
||||
expect(body).toHaveProperty('value');
|
||||
expect(body).toHaveProperty('createdAt');
|
||||
expect(body.value).toBe('TestApiKey1234567890');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should create an API key with auto-generated value when not provided', async () => {
|
||||
const dto = new CreateApiKeyRequestDto();
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/api-keys`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(201)
|
||||
.then(
|
||||
({
|
||||
body,
|
||||
}: {
|
||||
body: {
|
||||
id: number;
|
||||
value: string;
|
||||
createdAt: Date;
|
||||
};
|
||||
}) => {
|
||||
expect(body).toHaveProperty('id');
|
||||
expect(body).toHaveProperty('value');
|
||||
expect(body).toHaveProperty('createdAt');
|
||||
expect(body.value).toMatch(/^[A-F0-9]{20}$/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should return 400 for invalid API key length', async () => {
|
||||
const dto = new CreateApiKeyRequestDto();
|
||||
dto.value = 'ShortKey';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/api-keys`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
const dto = new CreateApiKeyRequestDto();
|
||||
dto.value = 'TestApiKey1234567890';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/api-keys`)
|
||||
.send(dto)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/api-keys (GET)', () => {
|
||||
beforeEach(async () => {
|
||||
const dto = new CreateApiKeyRequestDto();
|
||||
dto.value = 'TestApiKeyForList123';
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/api-keys`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto);
|
||||
});
|
||||
|
||||
it('should find API keys by project id', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/api-keys`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: FindApiKeysResponseDto }) => {
|
||||
const responseBody = body;
|
||||
expect(responseBody.items.length).toBeGreaterThan(0);
|
||||
expect(responseBody.items[0]).toHaveProperty('id');
|
||||
expect(responseBody.items[0]).toHaveProperty('value');
|
||||
expect(responseBody.items[0]).toHaveProperty('createdAt');
|
||||
expect(responseBody.items[0]).toHaveProperty('deletedAt');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/api-keys`)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/api-keys/:apiKeyId (DELETE)', () => {
|
||||
let apiKeyId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dto = new CreateApiKeyRequestDto();
|
||||
dto.value = 'TestApiKeyForDelete1';
|
||||
|
||||
const response = await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/api-keys`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto);
|
||||
|
||||
apiKeyId = (response.body as { id: number }).id;
|
||||
});
|
||||
|
||||
it('should delete API key', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/api-keys/${apiKeyId}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/api-keys/${apiKeyId}`)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const delay = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
await delay(500);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Server } from 'net';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
import request from 'supertest';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import { initializeTransactionalContext } from 'typeorm-transactional';
|
||||
|
||||
import { AppModule } from '@/app.module';
|
||||
import { OpensearchRepository } from '@/common/repositories';
|
||||
import { AuthService } from '@/domains/admin/auth/auth.service';
|
||||
import {
|
||||
EmailUserSignInRequestDto,
|
||||
EmailUserSignUpRequestDto,
|
||||
EmailVerificationCodeRequestDto,
|
||||
InvitationUserSignUpRequestDto,
|
||||
} from '@/domains/admin/auth/dtos/requests';
|
||||
import { SetupTenantRequestDto } from '@/domains/admin/tenant/dtos/requests';
|
||||
import { TenantService } from '@/domains/admin/tenant/tenant.service';
|
||||
import { clearAllEntities } from '@/test-utils/util-functions';
|
||||
|
||||
describe('AuthController (integration)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let _dataSource: DataSource;
|
||||
let _authService: AuthService;
|
||||
let tenantService: TenantService;
|
||||
let configService: ConfigService;
|
||||
let opensearchRepository: OpensearchRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
initializeTransactionalContext();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
await app.init();
|
||||
|
||||
_dataSource = module.get(getDataSourceToken());
|
||||
_authService = module.get(AuthService);
|
||||
tenantService = module.get(TenantService);
|
||||
configService = module.get(ConfigService);
|
||||
opensearchRepository = module.get(OpensearchRepository);
|
||||
|
||||
await clearAllEntities(module);
|
||||
if (configService.get('opensearch.use')) {
|
||||
await opensearchRepository.deleteAllIndexes();
|
||||
}
|
||||
|
||||
const dto = new SetupTenantRequestDto();
|
||||
dto.siteName = faker.string.sample();
|
||||
dto.password = '12345678';
|
||||
await tenantService.create(dto);
|
||||
});
|
||||
|
||||
describe('/admin/auth/email/code/verify (POST)', () => {
|
||||
it('should verify email code successfully', async () => {
|
||||
const dto = new EmailVerificationCodeRequestDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.code = '123456';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post('/admin/auth/email/code/verify')
|
||||
.send(dto)
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/auth/signUp/email (POST)', () => {
|
||||
it('should sign up user with email', async () => {
|
||||
const email = faker.internet.email();
|
||||
|
||||
const dto = new EmailUserSignUpRequestDto();
|
||||
dto.email = email;
|
||||
dto.password = 'password123';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post('/admin/auth/signUp/email')
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 400 for weak password', async () => {
|
||||
const dto = new EmailUserSignUpRequestDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = '123';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post('/admin/auth/signUp/email')
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 400 for invalid email format', async () => {
|
||||
const dto = new EmailUserSignUpRequestDto();
|
||||
dto.email = 'invalid-email';
|
||||
dto.password = 'password123';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post('/admin/auth/signUp/email')
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 409 for duplicate email', async () => {
|
||||
const email = faker.internet.email();
|
||||
const dto = new EmailUserSignUpRequestDto();
|
||||
dto.email = email;
|
||||
dto.password = 'password123';
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post('/admin/auth/signUp/email')
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post('/admin/auth/signUp/email')
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/auth/signIn/email (POST)', () => {
|
||||
it('should sign in user with email and password', async () => {
|
||||
const dto = new EmailUserSignInRequestDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = 'password123';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post('/admin/auth/signIn/email')
|
||||
.send(dto)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 401 for wrong password', async () => {
|
||||
const dto = new EmailUserSignInRequestDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = 'wrong-password';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post('/admin/auth/signIn/email')
|
||||
.send(dto)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent email', async () => {
|
||||
const dto = new EmailUserSignInRequestDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = 'password123';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post('/admin/auth/signIn/email')
|
||||
.send(dto)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 400 for invalid email format', async () => {
|
||||
const dto = new EmailUserSignInRequestDto();
|
||||
dto.email = 'invalid-email';
|
||||
dto.password = 'password123';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post('/admin/auth/signIn/email')
|
||||
.send(dto)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/auth/signUp/invitation (POST)', () => {
|
||||
it('should sign up user with invitation code', async () => {
|
||||
const dto = new InvitationUserSignUpRequestDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = 'password123';
|
||||
dto.code = 'invitation-code-123';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post('/admin/auth/signUp/invitation')
|
||||
.send(dto)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 400 for invalid invitation code', async () => {
|
||||
const dto = new InvitationUserSignUpRequestDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = 'password123';
|
||||
dto.code = 'invalid-code';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post('/admin/auth/signUp/invitation')
|
||||
.send(dto)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 400 for expired invitation code', async () => {
|
||||
const dto = new InvitationUserSignUpRequestDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = 'password123';
|
||||
dto.code = 'expired-code';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post('/admin/auth/signUp/invitation')
|
||||
.send(dto)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const delay = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
await delay(500);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Server } from 'net';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
import request from 'supertest';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import { initializeTransactionalContext } from 'typeorm-transactional';
|
||||
|
||||
import { AppModule } from '@/app.module';
|
||||
import { OpensearchRepository } from '@/common/repositories';
|
||||
import { AuthService } from '@/domains/admin/auth/auth.service';
|
||||
import { CategoryService } from '@/domains/admin/project/category/category.service';
|
||||
import {
|
||||
CreateCategoryRequestDto,
|
||||
UpdateCategoryRequestDto,
|
||||
} from '@/domains/admin/project/category/dtos/requests';
|
||||
import type { GetAllCategoriesResponseDto } from '@/domains/admin/project/category/dtos/responses';
|
||||
import type { ProjectEntity } from '@/domains/admin/project/project/project.entity';
|
||||
import { ProjectService } from '@/domains/admin/project/project/project.service';
|
||||
import { SetupTenantRequestDto } from '@/domains/admin/tenant/dtos/requests';
|
||||
import { TenantService } from '@/domains/admin/tenant/tenant.service';
|
||||
import { clearAllEntities, signInTestUser } from '@/test-utils/util-functions';
|
||||
|
||||
describe('CategoryController (integration)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let dataSource: DataSource;
|
||||
let authService: AuthService;
|
||||
let tenantService: TenantService;
|
||||
let projectService: ProjectService;
|
||||
let _categoryService: CategoryService;
|
||||
let configService: ConfigService;
|
||||
let opensearchRepository: OpensearchRepository;
|
||||
|
||||
let project: ProjectEntity;
|
||||
let accessToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
initializeTransactionalContext();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
await app.init();
|
||||
|
||||
dataSource = module.get(getDataSourceToken());
|
||||
authService = module.get(AuthService);
|
||||
tenantService = module.get(TenantService);
|
||||
projectService = module.get(ProjectService);
|
||||
_categoryService = module.get(CategoryService);
|
||||
configService = module.get(ConfigService);
|
||||
opensearchRepository = module.get(OpensearchRepository);
|
||||
|
||||
await clearAllEntities(module);
|
||||
if (configService.get('opensearch.use')) {
|
||||
await opensearchRepository.deleteAllIndexes();
|
||||
}
|
||||
|
||||
const dto = new SetupTenantRequestDto();
|
||||
dto.siteName = faker.string.sample();
|
||||
dto.password = '12345678';
|
||||
await tenantService.create(dto);
|
||||
|
||||
project = await projectService.create({
|
||||
name: faker.lorem.words(),
|
||||
description: faker.lorem.lines(1),
|
||||
timezone: {
|
||||
countryCode: 'KR',
|
||||
name: 'Asia/Seoul',
|
||||
offset: '+09:00',
|
||||
},
|
||||
});
|
||||
|
||||
const { jwt } = await signInTestUser(dataSource, authService);
|
||||
accessToken = jwt.accessToken;
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/categories (POST)', () => {
|
||||
it('should create a category', async () => {
|
||||
const dto = new CreateCategoryRequestDto();
|
||||
dto.name = 'TestCategory';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/categories`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(201)
|
||||
.then(({ body }: { body: { id: number } }) => {
|
||||
expect(body).toHaveProperty('id');
|
||||
expect(typeof body.id).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
const dto = new CreateCategoryRequestDto();
|
||||
dto.name = 'TestCategory';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/categories`)
|
||||
.send(dto)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/categories/search (POST)', () => {
|
||||
beforeEach(async () => {
|
||||
const dto = new CreateCategoryRequestDto();
|
||||
dto.name = 'TestCategoryForList';
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/categories`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto);
|
||||
});
|
||||
|
||||
it('should find categories by project id', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/categories/search`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({
|
||||
categoryName: 'TestCategory',
|
||||
page: 1,
|
||||
limit: 10,
|
||||
})
|
||||
.expect(201)
|
||||
.then(({ body }: { body: GetAllCategoriesResponseDto }) => {
|
||||
const responseBody = body;
|
||||
expect(responseBody.items.length).toBeGreaterThan(0);
|
||||
expect(responseBody.items[0]).toHaveProperty('id');
|
||||
expect(responseBody.items[0]).toHaveProperty('name');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty list when no categories match search', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/categories/search`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({
|
||||
categoryName: 'NonExistentCategory',
|
||||
page: 1,
|
||||
limit: 10,
|
||||
})
|
||||
.expect(201)
|
||||
.then(({ body }: { body: GetAllCategoriesResponseDto }) => {
|
||||
const responseBody = body;
|
||||
expect(responseBody.items.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/categories/search`)
|
||||
.send({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
})
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/categories/:categoryId (PUT)', () => {
|
||||
let categoryId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dto = new CreateCategoryRequestDto();
|
||||
dto.name = 'TestCategoryForUpdate';
|
||||
|
||||
const response = await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/categories`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto);
|
||||
|
||||
categoryId = (response.body as { id: number }).id;
|
||||
});
|
||||
|
||||
it('should update category', async () => {
|
||||
const dto = new UpdateCategoryRequestDto();
|
||||
dto.name = 'UpdatedTestCategory';
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/categories/${categoryId}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(200);
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/categories/search`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({
|
||||
categoryName: 'UpdatedTestCategory',
|
||||
page: 1,
|
||||
limit: 10,
|
||||
})
|
||||
.expect(201)
|
||||
.then(({ body }: { body: GetAllCategoriesResponseDto }) => {
|
||||
expect(body.items.length).toBeGreaterThan(0);
|
||||
expect(body.items[0].name).toBe('UpdatedTestCategory');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent category', async () => {
|
||||
const dto = new UpdateCategoryRequestDto();
|
||||
dto.name = 'UpdatedCategory';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/categories/999`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
const dto = new UpdateCategoryRequestDto();
|
||||
dto.name = 'UpdatedCategory';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/categories/${categoryId}`)
|
||||
.send(dto)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/categories/:categoryId (DELETE)', () => {
|
||||
let categoryId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dto = new CreateCategoryRequestDto();
|
||||
dto.name = 'TestCategoryForDelete';
|
||||
|
||||
const response = await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/categories`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto);
|
||||
|
||||
categoryId = (response.body as { id: number }).id;
|
||||
});
|
||||
|
||||
it('should delete category', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/categories/${categoryId}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/categories/search`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({
|
||||
categoryName: 'TestCategoryForDelete',
|
||||
page: 1,
|
||||
limit: 10,
|
||||
})
|
||||
.expect(201)
|
||||
.then(({ body }: { body: GetAllCategoriesResponseDto }) => {
|
||||
expect(body.items.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 when deleting non-existent category', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/categories/999`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/categories/${categoryId}`)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const delay = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
await delay(500);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Server } from 'net';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
import request from 'supertest';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import { initializeTransactionalContext } from 'typeorm-transactional';
|
||||
|
||||
import { AppModule } from '@/app.module';
|
||||
import {
|
||||
FieldFormatEnum,
|
||||
FieldPropertyEnum,
|
||||
FieldStatusEnum,
|
||||
} from '@/common/enums';
|
||||
import { OpensearchRepository } from '@/common/repositories';
|
||||
import { AuthService } from '@/domains/admin/auth/auth.service';
|
||||
import {
|
||||
CreateChannelRequestDto,
|
||||
CreateChannelRequestFieldDto,
|
||||
FindChannelsByProjectIdRequestDto,
|
||||
UpdateChannelFieldsRequestDto,
|
||||
UpdateChannelRequestDto,
|
||||
UpdateChannelRequestFieldDto,
|
||||
} from '@/domains/admin/channel/channel/dtos/requests';
|
||||
import type {
|
||||
FindChannelByIdResponseDto,
|
||||
FindChannelsByProjectIdResponseDto,
|
||||
} from '@/domains/admin/channel/channel/dtos/responses';
|
||||
import type { ProjectEntity } from '@/domains/admin/project/project/project.entity';
|
||||
import { ProjectService } from '@/domains/admin/project/project/project.service';
|
||||
import { SetupTenantRequestDto } from '@/domains/admin/tenant/dtos/requests';
|
||||
import { TenantService } from '@/domains/admin/tenant/tenant.service';
|
||||
import { clearAllEntities, signInTestUser } from '@/test-utils/util-functions';
|
||||
|
||||
describe('ChannelController (integration)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let dataSource: DataSource;
|
||||
|
||||
let authService: AuthService;
|
||||
let tenantService: TenantService;
|
||||
let projectService: ProjectService;
|
||||
let configService: ConfigService;
|
||||
|
||||
let opensearchRepository: OpensearchRepository;
|
||||
|
||||
let project: ProjectEntity;
|
||||
|
||||
let accessToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
initializeTransactionalContext();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
await app.init();
|
||||
|
||||
dataSource = module.get(getDataSourceToken());
|
||||
|
||||
authService = module.get(AuthService);
|
||||
tenantService = module.get(TenantService);
|
||||
projectService = module.get(ProjectService);
|
||||
configService = module.get(ConfigService);
|
||||
|
||||
opensearchRepository = module.get(OpensearchRepository);
|
||||
|
||||
await clearAllEntities(module);
|
||||
if (configService.get('opensearch.use')) {
|
||||
await opensearchRepository.deleteAllIndexes();
|
||||
}
|
||||
|
||||
const dto = new SetupTenantRequestDto();
|
||||
dto.siteName = faker.string.sample();
|
||||
dto.password = '12345678';
|
||||
await tenantService.create(dto);
|
||||
|
||||
project = await projectService.create({
|
||||
name: faker.lorem.words(),
|
||||
description: faker.lorem.lines(1),
|
||||
timezone: {
|
||||
countryCode: 'KR',
|
||||
name: 'Asia/Seoul',
|
||||
offset: '+09:00',
|
||||
},
|
||||
});
|
||||
|
||||
const { jwt } = await signInTestUser(dataSource, authService);
|
||||
accessToken = jwt.accessToken;
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/channels (POST)', () => {
|
||||
it('should create a channel', async () => {
|
||||
const dto = new CreateChannelRequestDto();
|
||||
dto.name = 'TestChannel';
|
||||
|
||||
const fieldDto = new CreateChannelRequestFieldDto();
|
||||
fieldDto.name = 'TestField';
|
||||
fieldDto.key = 'testField';
|
||||
fieldDto.format = FieldFormatEnum.text;
|
||||
fieldDto.property = FieldPropertyEnum.EDITABLE;
|
||||
fieldDto.status = FieldStatusEnum.ACTIVE;
|
||||
|
||||
dto.fields = [fieldDto];
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/channels`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(201);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/channels (GET)', () => {
|
||||
it('should find channels by project id', async () => {
|
||||
const dto = new FindChannelsByProjectIdRequestDto();
|
||||
dto.searchText = 'TestChannel';
|
||||
dto.page = 1;
|
||||
dto.limit = 10;
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/channels`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.query(dto)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: FindChannelsByProjectIdResponseDto }) => {
|
||||
expect(body.items.length).toBe(1);
|
||||
expect(body.items[0].name).toBe('TestChannel');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/channels/:channelId (GET)', () => {
|
||||
it('should find channel by id', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/channels/1`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: FindChannelByIdResponseDto }) => {
|
||||
expect(body.name).toBe('TestChannel');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/channels/:channelId (PUT)', () => {
|
||||
it('should update channel', async () => {
|
||||
const dto = new UpdateChannelRequestDto();
|
||||
dto.name = 'TestChannelUpdated';
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/channels/1`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(200);
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/channels/1`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: FindChannelByIdResponseDto }) => {
|
||||
expect(body.name).toBe('TestChannelUpdated');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/channels/:channelId/fields (PUT)', () => {
|
||||
it('should update channel fields', async () => {
|
||||
const dto = new UpdateChannelFieldsRequestDto();
|
||||
const fieldDto = new UpdateChannelRequestFieldDto();
|
||||
fieldDto.id = 5;
|
||||
fieldDto.format = FieldFormatEnum.text;
|
||||
fieldDto.key = 'testField';
|
||||
fieldDto.name = 'TestFieldUpdated';
|
||||
dto.fields = [fieldDto];
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/channels/1/fields`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(200);
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/channels/1`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: FindChannelByIdResponseDto }) => {
|
||||
expect(body.fields.length).toBe(5);
|
||||
expect(body.fields[4].name).toBe('TestFieldUpdated');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 error when update channel field key with special character', async () => {
|
||||
const dto = new UpdateChannelFieldsRequestDto();
|
||||
const fieldDto = new UpdateChannelRequestFieldDto();
|
||||
fieldDto.id = 5;
|
||||
fieldDto.format = FieldFormatEnum.text;
|
||||
fieldDto.key = 'testField!';
|
||||
fieldDto.name = 'testField!';
|
||||
dto.fields = [fieldDto];
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/channels/1/fields`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/channels/:channelId (DELETE)', () => {
|
||||
it('should delete channel', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/channels/1`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const dto = new FindChannelsByProjectIdRequestDto();
|
||||
dto.page = 1;
|
||||
dto.limit = 10;
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/channels`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.query(dto)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: FindChannelsByProjectIdResponseDto }) => {
|
||||
expect(body.items.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/channels/1`)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Channel validation tests', () => {
|
||||
it('should return 400 when creating channel with invalid field key', async () => {
|
||||
const dto = new CreateChannelRequestDto();
|
||||
dto.name = 'TestChannel';
|
||||
|
||||
const fieldDto = new CreateChannelRequestFieldDto();
|
||||
fieldDto.name = 'TestField';
|
||||
fieldDto.key = 'invalid-key!@#';
|
||||
fieldDto.format = FieldFormatEnum.text;
|
||||
fieldDto.property = FieldPropertyEnum.EDITABLE;
|
||||
fieldDto.status = FieldStatusEnum.ACTIVE;
|
||||
|
||||
dto.fields = [fieldDto];
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/channels`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 400 when updating channel with invalid data', async () => {
|
||||
const dto = new UpdateChannelRequestDto();
|
||||
dto.name = '';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/channels/1`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const delay = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
await delay(500);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,420 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Server } from 'net';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm';
|
||||
import type { Client } from '@opensearch-project/opensearch';
|
||||
import { DateTime } from 'luxon';
|
||||
import request from 'supertest';
|
||||
import type { DataSource, Repository } from 'typeorm';
|
||||
import { initializeTransactionalContext } from 'typeorm-transactional';
|
||||
|
||||
import { AppModule } from '@/app.module';
|
||||
import { FieldFormatEnum, QueryV2ConditionsEnum } from '@/common/enums';
|
||||
import { OpensearchRepository } from '@/common/repositories';
|
||||
import { AuthService } from '@/domains/admin/auth/auth.service';
|
||||
import { ChannelEntity } from '@/domains/admin/channel/channel/channel.entity';
|
||||
import { ChannelService } from '@/domains/admin/channel/channel/channel.service';
|
||||
import { FieldEntity } from '@/domains/admin/channel/field/field.entity';
|
||||
import type { CreateFeedbackDto } from '@/domains/admin/feedback/dtos';
|
||||
import type { FindFeedbacksByChannelIdRequestDtoV2 } from '@/domains/admin/feedback/dtos/requests/find-feedbacks-by-channel-id-request-v2.dto';
|
||||
import type { FindFeedbacksByChannelIdResponseDto } from '@/domains/admin/feedback/dtos/responses';
|
||||
import { FeedbackService } from '@/domains/admin/feedback/feedback.service';
|
||||
import { ProjectEntity } from '@/domains/admin/project/project/project.entity';
|
||||
import { ProjectService } from '@/domains/admin/project/project/project.service';
|
||||
import { TenantEntity } from '@/domains/admin/tenant/tenant.entity';
|
||||
import { TenantService } from '@/domains/admin/tenant/tenant.service';
|
||||
import { getRandomValue } from '@/test-utils/fixtures';
|
||||
import {
|
||||
clearAllEntities,
|
||||
clearEntities,
|
||||
createChannel,
|
||||
createProject,
|
||||
createTenant,
|
||||
signInTestUser,
|
||||
} from '@/test-utils/util-functions';
|
||||
|
||||
describe('FeedbackController (integration)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let dataSource: DataSource;
|
||||
let authService: AuthService;
|
||||
|
||||
let tenantService: TenantService;
|
||||
let projectService: ProjectService;
|
||||
let channelService: ChannelService;
|
||||
let feedbackService: FeedbackService;
|
||||
let configService: ConfigService;
|
||||
|
||||
let tenantRepo: Repository<TenantEntity>;
|
||||
let projectRepo: Repository<ProjectEntity>;
|
||||
let channelRepo: Repository<ChannelEntity>;
|
||||
let fieldRepo: Repository<FieldEntity>;
|
||||
let osService: Client;
|
||||
let opensearchRepository: OpensearchRepository;
|
||||
|
||||
let project: ProjectEntity;
|
||||
let channel: ChannelEntity;
|
||||
let fields: FieldEntity[];
|
||||
|
||||
let accessToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
initializeTransactionalContext();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
await app.init();
|
||||
|
||||
dataSource = module.get(getDataSourceToken());
|
||||
|
||||
authService = module.get(AuthService);
|
||||
|
||||
tenantService = module.get(TenantService);
|
||||
projectService = module.get(ProjectService);
|
||||
channelService = module.get(ChannelService);
|
||||
feedbackService = module.get(FeedbackService);
|
||||
configService = module.get(ConfigService);
|
||||
|
||||
tenantRepo = module.get(getRepositoryToken(TenantEntity));
|
||||
projectRepo = module.get(getRepositoryToken(ProjectEntity));
|
||||
channelRepo = module.get(getRepositoryToken(ChannelEntity));
|
||||
fieldRepo = module.get(getRepositoryToken(FieldEntity));
|
||||
osService = module.get<Client>('OPENSEARCH_CLIENT');
|
||||
opensearchRepository = module.get(OpensearchRepository);
|
||||
|
||||
await clearAllEntities(module);
|
||||
if (configService.get('opensearch.use')) {
|
||||
await opensearchRepository.deleteAllIndexes();
|
||||
}
|
||||
|
||||
await createTenant(tenantService);
|
||||
project = await createProject(projectService);
|
||||
const { id: channelId } = await createChannel(channelService, project);
|
||||
|
||||
channel = await channelService.findById({ channelId });
|
||||
|
||||
fields = await fieldRepo.find({
|
||||
where: { channel: { id: channel.id } },
|
||||
relations: { options: true },
|
||||
});
|
||||
|
||||
const { jwt } = await signInTestUser(dataSource, authService);
|
||||
accessToken = jwt.accessToken;
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/channels/:channelId/feedbacks (POST)', () => {
|
||||
it('should create random feedbacks', async () => {
|
||||
const dto: Record<string, string | number | string[] | number[]> = {};
|
||||
fields
|
||||
.filter(
|
||||
({ key }) =>
|
||||
key !== 'id' &&
|
||||
key !== 'issues' &&
|
||||
key !== 'createdAt' &&
|
||||
key !== 'updatedAt',
|
||||
)
|
||||
.forEach(({ key, format, options }) => {
|
||||
dto[key] = getRandomValue(format, options);
|
||||
});
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/channels/${channel.id}/feedbacks`)
|
||||
.set('x-api-key', `${process.env.MASTER_API_KEY}`)
|
||||
.send(dto)
|
||||
.expect(201)
|
||||
.then(
|
||||
async ({
|
||||
body,
|
||||
}: {
|
||||
body: Record<string, any> & { issueNames?: string[] };
|
||||
}) => {
|
||||
expect(body.id).toBeDefined();
|
||||
if (configService.get('opensearch.use')) {
|
||||
const esResult = await osService.get({
|
||||
id: body.id as string,
|
||||
index: channel.id.toString(),
|
||||
});
|
||||
|
||||
['id', 'createdAt', 'updatedAt'].forEach(
|
||||
(field) => delete esResult.body._source?.[field],
|
||||
);
|
||||
expect(dto).toMatchObject(esResult.body._source ?? {});
|
||||
} else {
|
||||
const feedback = await feedbackService.findById({
|
||||
channelId: channel.id,
|
||||
feedbackId: body.id as number,
|
||||
});
|
||||
|
||||
['id', 'createdAt', 'updatedAt', 'issues'].forEach(
|
||||
(field) => delete feedback[field],
|
||||
);
|
||||
expect(dto).toMatchObject(feedback);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/channels/:channelId/feedbacks/search (POST)', () => {
|
||||
it('should return all searched feedbacks', async () => {
|
||||
const dto: CreateFeedbackDto = {
|
||||
channelId: channel.id,
|
||||
data: {},
|
||||
};
|
||||
let availableFieldKey = '';
|
||||
fields
|
||||
.filter(
|
||||
({ key }) =>
|
||||
key !== 'id' &&
|
||||
key !== 'issues' &&
|
||||
key !== 'createdAt' &&
|
||||
key !== 'updatedAt',
|
||||
)
|
||||
.forEach(({ key, format, options }) => {
|
||||
dto.data[key] = getRandomValue(format, options);
|
||||
availableFieldKey = key;
|
||||
});
|
||||
|
||||
dto.data[availableFieldKey] = 'test';
|
||||
|
||||
await feedbackService.create(dto);
|
||||
|
||||
const keywordField = fields.find(
|
||||
({ format }) => format === FieldFormatEnum.keyword,
|
||||
);
|
||||
if (!keywordField) return;
|
||||
|
||||
const findFeedbackDto: FindFeedbacksByChannelIdRequestDtoV2 = {
|
||||
queries: [
|
||||
{
|
||||
key: availableFieldKey,
|
||||
value: 'test',
|
||||
condition: QueryV2ConditionsEnum.IS,
|
||||
},
|
||||
],
|
||||
operator: 'AND',
|
||||
limit: 10,
|
||||
page: 1,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(
|
||||
`/admin/projects/${project.id}/channels/${channel.id}/feedbacks/search`,
|
||||
)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(findFeedbackDto)
|
||||
.expect(201)
|
||||
.then(({ body }: { body: FindFeedbacksByChannelIdResponseDto }) => {
|
||||
expect(body.meta.itemCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/channels/:channelId/feedbacks/:feedbackId (PUT)', () => {
|
||||
it('should update a feedback', async () => {
|
||||
const dto: CreateFeedbackDto = {
|
||||
channelId: channel.id,
|
||||
data: {},
|
||||
};
|
||||
let availableFieldKey = '';
|
||||
fields
|
||||
.filter(
|
||||
({ key }) =>
|
||||
key !== 'id' &&
|
||||
key !== 'issues' &&
|
||||
key !== 'createdAt' &&
|
||||
key !== 'updatedAt',
|
||||
)
|
||||
.forEach(({ key, format, options }) => {
|
||||
dto.data[key] = getRandomValue(format, options);
|
||||
availableFieldKey = key;
|
||||
});
|
||||
|
||||
const feedback = await feedbackService.create(dto);
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.put(
|
||||
`/admin/projects/${project.id}/channels/${channel.id}/feedbacks/${feedback.id}`,
|
||||
)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({
|
||||
[availableFieldKey]: 'test',
|
||||
})
|
||||
.expect(200)
|
||||
.then(async () => {
|
||||
if (configService.get('opensearch.use')) {
|
||||
const esResult = await osService.get({
|
||||
id: feedback.id.toString(),
|
||||
index: channel.id.toString(),
|
||||
});
|
||||
|
||||
['id', 'createdAt', 'updatedAt'].forEach(
|
||||
(field) => delete esResult.body._source?.[field],
|
||||
);
|
||||
|
||||
dto.data[availableFieldKey] = 'test';
|
||||
expect(dto.data).toMatchObject(esResult.body._source ?? {});
|
||||
} else {
|
||||
const updatedFeedback = await feedbackService.findById({
|
||||
channelId: channel.id,
|
||||
feedbackId: feedback.id,
|
||||
});
|
||||
|
||||
['id', 'createdAt', 'updatedAt', 'issues'].forEach(
|
||||
(field) => delete updatedFeedback[field],
|
||||
);
|
||||
|
||||
dto.data[availableFieldKey] = 'test';
|
||||
expect(dto.data).toMatchObject(updatedFeedback);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should update a feedback with special character', async () => {
|
||||
const dto: CreateFeedbackDto = {
|
||||
channelId: channel.id,
|
||||
data: {},
|
||||
};
|
||||
let availableFieldKey = '';
|
||||
fields
|
||||
.filter(
|
||||
({ key }) =>
|
||||
key !== 'id' &&
|
||||
key !== 'issues' &&
|
||||
key !== 'createdAt' &&
|
||||
key !== 'updatedAt',
|
||||
)
|
||||
.forEach(({ key, format, options }) => {
|
||||
dto.data[key] = getRandomValue(format, options);
|
||||
availableFieldKey = key;
|
||||
});
|
||||
|
||||
const feedback = await feedbackService.create(dto);
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.put(
|
||||
`/admin/projects/${project.id}/channels/${channel.id}/feedbacks/${feedback.id}`,
|
||||
)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({
|
||||
[availableFieldKey]: '?',
|
||||
})
|
||||
.expect(200)
|
||||
.then(async () => {
|
||||
if (configService.get('opensearch.use')) {
|
||||
const esResult = await osService.get({
|
||||
id: feedback.id.toString(),
|
||||
index: channel.id.toString(),
|
||||
});
|
||||
|
||||
['id', 'createdAt', 'updatedAt'].forEach(
|
||||
(field) => delete esResult.body._source?.[field],
|
||||
);
|
||||
|
||||
dto.data[availableFieldKey] = '?';
|
||||
expect(dto.data).toMatchObject(esResult.body._source ?? {});
|
||||
} else {
|
||||
const updatedFeedback = await feedbackService.findById({
|
||||
channelId: channel.id,
|
||||
feedbackId: feedback.id,
|
||||
});
|
||||
|
||||
['id', 'createdAt', 'updatedAt', 'issues'].forEach(
|
||||
(field) => delete updatedFeedback[field],
|
||||
);
|
||||
|
||||
dto.data[availableFieldKey] = '?';
|
||||
expect(dto.data).toMatchObject(updatedFeedback);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('old feedback deletion test', () => {
|
||||
it('should create feedbacks and delete feedbacks within specific date range', async () => {
|
||||
const dto: Record<string, string | number | string[] | number[]> = {};
|
||||
fields
|
||||
.filter(
|
||||
({ key }) =>
|
||||
key !== 'id' &&
|
||||
key !== 'issues' &&
|
||||
key !== 'createdAt' &&
|
||||
key !== 'updatedAt',
|
||||
)
|
||||
.forEach(({ key, format, options }) => {
|
||||
dto[key] = getRandomValue(format, options);
|
||||
});
|
||||
|
||||
dto.createdAt = DateTime.now().minus({ month: 7 }).toFormat('yyyy-MM-dd');
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/channels/${channel.id}/feedbacks`)
|
||||
.set('x-api-key', `${process.env.MASTER_API_KEY}`)
|
||||
.send(dto)
|
||||
.expect(201);
|
||||
|
||||
dto.createdAt = DateTime.now().minus({ days: 1 }).toFormat('yyyy-MM-dd');
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/channels/${channel.id}/feedbacks`)
|
||||
.set('x-api-key', `${process.env.MASTER_API_KEY}`)
|
||||
.send(dto)
|
||||
.expect(201);
|
||||
|
||||
await tenantService.deleteOldFeedbacks();
|
||||
|
||||
const findFeedbackDto: FindFeedbacksByChannelIdRequestDtoV2 = {
|
||||
defaultQueries: [
|
||||
{
|
||||
key: 'createdAt',
|
||||
value: {
|
||||
gte: DateTime.now().minus({ years: 1 }).toFormat('yyyy-MM-dd'),
|
||||
lt: DateTime.now().toFormat('yyyy-MM-dd'),
|
||||
},
|
||||
condition: QueryV2ConditionsEnum.BETWEEN,
|
||||
},
|
||||
],
|
||||
operator: 'AND',
|
||||
limit: 10,
|
||||
page: 1,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(
|
||||
`/admin/projects/${project.id}/channels/${channel.id}/feedbacks/search`,
|
||||
)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(findFeedbackDto)
|
||||
.expect(201)
|
||||
.then(({ body }: { body: FindFeedbacksByChannelIdResponseDto }) => {
|
||||
expect(body.meta.itemCount).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await clearEntities([tenantRepo, projectRepo, channelRepo, fieldRepo]);
|
||||
const delay = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
await delay(500);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Server } from 'net';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
import request from 'supertest';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import { initializeTransactionalContext } from 'typeorm-transactional';
|
||||
|
||||
import { AppModule } from '@/app.module';
|
||||
import { IssueStatusEnum } from '@/common/enums';
|
||||
import { OpensearchRepository } from '@/common/repositories';
|
||||
import { AuthService } from '@/domains/admin/auth/auth.service';
|
||||
import { FindIssuesByProjectIdRequestDto } from '@/domains/admin/project/issue/dtos/requests';
|
||||
import type {
|
||||
FindIssueByIdResponseDto,
|
||||
FindIssuesByProjectIdResponseDto,
|
||||
} from '@/domains/admin/project/issue/dtos/responses';
|
||||
import type { CountIssuesByIdResponseDto } from '@/domains/admin/project/project/dtos/responses';
|
||||
import type { ProjectEntity } from '@/domains/admin/project/project/project.entity';
|
||||
import { ProjectService } from '@/domains/admin/project/project/project.service';
|
||||
import { SetupTenantRequestDto } from '@/domains/admin/tenant/dtos/requests';
|
||||
import { TenantService } from '@/domains/admin/tenant/tenant.service';
|
||||
import { clearAllEntities, signInTestUser } from '@/test-utils/util-functions';
|
||||
|
||||
describe('IssueController (integration)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let dataSource: DataSource;
|
||||
|
||||
let authService: AuthService;
|
||||
let tenantService: TenantService;
|
||||
let projectService: ProjectService;
|
||||
let configService: ConfigService;
|
||||
|
||||
let opensearchRepository: OpensearchRepository;
|
||||
|
||||
let project: ProjectEntity;
|
||||
|
||||
let accessToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
initializeTransactionalContext();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
await app.init();
|
||||
|
||||
dataSource = module.get(getDataSourceToken());
|
||||
|
||||
authService = module.get(AuthService);
|
||||
tenantService = module.get(TenantService);
|
||||
projectService = module.get(ProjectService);
|
||||
configService = module.get(ConfigService);
|
||||
|
||||
opensearchRepository = module.get(OpensearchRepository);
|
||||
|
||||
await clearAllEntities(module);
|
||||
if (configService.get('opensearch.use')) {
|
||||
await opensearchRepository.deleteAllIndexes();
|
||||
}
|
||||
|
||||
const dto = new SetupTenantRequestDto();
|
||||
dto.siteName = faker.string.sample();
|
||||
dto.password = '12345678';
|
||||
await tenantService.create(dto);
|
||||
|
||||
project = await projectService.create({
|
||||
name: faker.lorem.words(),
|
||||
description: faker.lorem.lines(1),
|
||||
timezone: {
|
||||
countryCode: 'KR',
|
||||
name: 'Asia/Seoul',
|
||||
offset: '+09:00',
|
||||
},
|
||||
});
|
||||
|
||||
const { jwt } = await signInTestUser(dataSource, authService);
|
||||
accessToken = jwt.accessToken;
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/issues (POST)', () => {
|
||||
it('should create an issue', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/issues`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({ name: 'TestIssue' })
|
||||
.expect(201);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/issues/:issueId (GET)', () => {
|
||||
it('should get an issue', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/issues/1`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: FindIssueByIdResponseDto }) => {
|
||||
expect(body.name).toBe('TestIssue');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/issue-count (GET)', () => {
|
||||
it('should return correct issue count', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/issue-count`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: CountIssuesByIdResponseDto }) => {
|
||||
expect(body.total).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/issues/search (POST)', () => {
|
||||
it('should return all searched issues', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/issues`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({ name: 'TestIssue2' })
|
||||
.expect(201);
|
||||
|
||||
const searchDto = new FindIssuesByProjectIdRequestDto();
|
||||
searchDto.query = {
|
||||
searchText: 'TestIssue',
|
||||
};
|
||||
searchDto.page = 1;
|
||||
searchDto.limit = 10;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/issues/search`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(searchDto)
|
||||
.expect(201)
|
||||
.then(({ body }: { body: FindIssuesByProjectIdResponseDto }) => {
|
||||
expect(body).toBeDefined();
|
||||
expect(body).toHaveProperty('items');
|
||||
expect(body.items.length).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/issues/:issueId (PUT)', () => {
|
||||
it('should update an issue', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/issues/1`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({
|
||||
name: 'TestIssue',
|
||||
description: 'TestIssueUpdated',
|
||||
status: IssueStatusEnum.IN_PROGRESS,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/issues/1`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: FindIssueByIdResponseDto }) => {
|
||||
expect(body.description).toBe('TestIssueUpdated');
|
||||
expect(body.status).toBe(IssueStatusEnum.IN_PROGRESS);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/issues/:issueId (DELETE)', () => {
|
||||
it('should delete an issue', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/issues/1`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const searchDto = new FindIssuesByProjectIdRequestDto();
|
||||
searchDto.query = {
|
||||
searchText: 'TestIssue',
|
||||
};
|
||||
searchDto.page = 1;
|
||||
searchDto.limit = 10;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/issues/search`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(searchDto)
|
||||
.expect(201)
|
||||
.then(({ body }: { body: FindIssuesByProjectIdResponseDto }) => {
|
||||
expect(body).toBeDefined();
|
||||
expect(body).toHaveProperty('items');
|
||||
expect(body.items.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/issues (DELETE)', () => {
|
||||
it('should delete many issues', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/issues`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({ issueIds: [2] })
|
||||
.expect(200);
|
||||
|
||||
const searchDto = new FindIssuesByProjectIdRequestDto();
|
||||
searchDto.query = {
|
||||
searchText: 'TestIssue',
|
||||
};
|
||||
searchDto.page = 1;
|
||||
searchDto.limit = 10;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/issues/search`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(searchDto)
|
||||
.expect(201)
|
||||
.then(({ body }: { body: FindIssuesByProjectIdResponseDto }) => {
|
||||
expect(body).toBeDefined();
|
||||
expect(body).toHaveProperty('items');
|
||||
expect(body.items.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 when deleting with invalid issueIds', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/issues`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({ issueIds: [] })
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/issues`)
|
||||
.send({ issueIds: [1] })
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Issue validation tests', () => {
|
||||
it('should return 400 when updating non-existent issue', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/issues/999`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({
|
||||
name: 'NonExistentIssue',
|
||||
description: 'This should fail',
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 400 when getting non-existent issue', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/issues/999`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const delay = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
await delay(500);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,427 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Server } from 'net';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
import request from 'supertest';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import { initializeTransactionalContext } from 'typeorm-transactional';
|
||||
|
||||
import { AppModule } from '@/app.module';
|
||||
import { OpensearchRepository } from '@/common/repositories';
|
||||
import { AuthService } from '@/domains/admin/auth/auth.service';
|
||||
import {
|
||||
CreateMemberRequestDto,
|
||||
UpdateMemberRequestDto,
|
||||
} from '@/domains/admin/project/member/dtos/requests';
|
||||
import type { GetAllMemberResponseDto } from '@/domains/admin/project/member/dtos/responses';
|
||||
import type { ProjectEntity } from '@/domains/admin/project/project/project.entity';
|
||||
import { ProjectService } from '@/domains/admin/project/project/project.service';
|
||||
import { PermissionEnum } from '@/domains/admin/project/role/permission.enum';
|
||||
import type { RoleEntity } from '@/domains/admin/project/role/role.entity';
|
||||
import { RoleService } from '@/domains/admin/project/role/role.service';
|
||||
import { SetupTenantRequestDto } from '@/domains/admin/tenant/dtos/requests';
|
||||
import { TenantService } from '@/domains/admin/tenant/tenant.service';
|
||||
import {
|
||||
UserStateEnum,
|
||||
UserTypeEnum,
|
||||
} from '@/domains/admin/user/entities/enums';
|
||||
import { UserEntity } from '@/domains/admin/user/entities/user.entity';
|
||||
import { clearAllEntities, signInTestUser } from '@/test-utils/util-functions';
|
||||
|
||||
describe('MemberController (integration)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let dataSource: DataSource;
|
||||
let authService: AuthService;
|
||||
let tenantService: TenantService;
|
||||
let projectService: ProjectService;
|
||||
let roleService: RoleService;
|
||||
|
||||
let configService: ConfigService;
|
||||
let opensearchRepository: OpensearchRepository;
|
||||
|
||||
let project: ProjectEntity;
|
||||
let role: RoleEntity;
|
||||
let user: UserEntity;
|
||||
let accessToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
initializeTransactionalContext();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
await app.init();
|
||||
|
||||
dataSource = module.get(getDataSourceToken());
|
||||
authService = module.get(AuthService);
|
||||
tenantService = module.get(TenantService);
|
||||
projectService = module.get(ProjectService);
|
||||
roleService = module.get(RoleService);
|
||||
configService = module.get(ConfigService);
|
||||
opensearchRepository = module.get(OpensearchRepository);
|
||||
|
||||
await clearAllEntities(module);
|
||||
if (configService.get('opensearch.use')) {
|
||||
await opensearchRepository.deleteAllIndexes();
|
||||
}
|
||||
|
||||
const dto = new SetupTenantRequestDto();
|
||||
dto.siteName = faker.string.sample();
|
||||
dto.password = '12345678';
|
||||
await tenantService.create(dto);
|
||||
|
||||
project = await projectService.create({
|
||||
name: faker.lorem.words(),
|
||||
description: faker.lorem.lines(1),
|
||||
timezone: {
|
||||
countryCode: 'KR',
|
||||
name: 'Asia/Seoul',
|
||||
offset: '+09:00',
|
||||
},
|
||||
});
|
||||
|
||||
role = await roleService.create({
|
||||
projectId: project.id,
|
||||
name: 'TestRole',
|
||||
permissions: [
|
||||
PermissionEnum.feedback_download_read,
|
||||
PermissionEnum.feedback_update,
|
||||
],
|
||||
});
|
||||
|
||||
const { jwt } = await signInTestUser(dataSource, authService);
|
||||
accessToken = jwt.accessToken;
|
||||
|
||||
const userRepo = dataSource.getRepository(UserEntity);
|
||||
user = await userRepo.save({
|
||||
email: faker.internet.email(),
|
||||
state: UserStateEnum.Active,
|
||||
hashPassword: faker.internet.password(),
|
||||
type: UserTypeEnum.GENERAL,
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/members (POST)', () => {
|
||||
afterEach(async () => {
|
||||
await dataSource.query('DELETE FROM members WHERE role_id = ?', [
|
||||
role.id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should create a member', async () => {
|
||||
const dto = new CreateMemberRequestDto();
|
||||
dto.userId = user.id;
|
||||
dto.roleId = role.id;
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/members`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(201);
|
||||
});
|
||||
|
||||
it('should return 400 for duplicate member', async () => {
|
||||
const dto = new CreateMemberRequestDto();
|
||||
dto.userId = user.id;
|
||||
dto.roleId = role.id;
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/members`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(201);
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/members`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 400 for non-existent user', async () => {
|
||||
const dto = new CreateMemberRequestDto();
|
||||
dto.userId = 999;
|
||||
dto.roleId = role.id;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/members`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent role', async () => {
|
||||
const dto = new CreateMemberRequestDto();
|
||||
dto.userId = user.id;
|
||||
dto.roleId = 999;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/members`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
const dto = new CreateMemberRequestDto();
|
||||
dto.userId = user.id;
|
||||
dto.roleId = role.id;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/members`)
|
||||
.send(dto)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/members/search (POST)', () => {
|
||||
afterEach(async () => {
|
||||
await dataSource.query('DELETE FROM members WHERE role_id = ?', [
|
||||
role.id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should find members by project id', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/members`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({
|
||||
userId: user.id,
|
||||
roleId: role.id,
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/members/search`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({
|
||||
queries: [
|
||||
{
|
||||
key: 'email',
|
||||
value: user.email,
|
||||
condition: 'LIKE',
|
||||
},
|
||||
],
|
||||
operator: 'AND',
|
||||
limit: 10,
|
||||
page: 1,
|
||||
})
|
||||
.expect(201)
|
||||
.then(({ body }: { body: GetAllMemberResponseDto }) => {
|
||||
const responseBody = body;
|
||||
expect(responseBody.items.length).toBeGreaterThan(0);
|
||||
expect(responseBody.items[0]).toHaveProperty('id');
|
||||
expect(responseBody.items[0]).toHaveProperty('user');
|
||||
expect(responseBody.items[0]).toHaveProperty('role');
|
||||
expect(responseBody.items[0].user).toHaveProperty('email');
|
||||
expect(responseBody.items[0].role).toHaveProperty('name');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty list when no members match search', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/members/search`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({
|
||||
queries: [
|
||||
{
|
||||
key: 'email',
|
||||
value: 'NonExistentUser',
|
||||
condition: 'LIKE',
|
||||
},
|
||||
],
|
||||
operator: 'AND',
|
||||
limit: 10,
|
||||
page: 1,
|
||||
})
|
||||
.expect(201)
|
||||
.then(({ body }: { body: GetAllMemberResponseDto }) => {
|
||||
const responseBody = body;
|
||||
expect(responseBody.items.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/members/search`)
|
||||
.send({
|
||||
queries: [],
|
||||
operator: 'AND',
|
||||
limit: 10,
|
||||
page: 1,
|
||||
})
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/members/:memberId (GET)', () => {
|
||||
afterEach(async () => {
|
||||
await dataSource.query('DELETE FROM members WHERE role_id = ?', [
|
||||
role.id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent member', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/members/999`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/members/:memberId (PUT)', () => {
|
||||
let memberId: number;
|
||||
let newRole: RoleEntity;
|
||||
|
||||
beforeEach(async () => {
|
||||
newRole = await roleService.create({
|
||||
projectId: project.id,
|
||||
name: `NewTestRole_${Date.now()}`,
|
||||
permissions: [PermissionEnum.feedback_download_read],
|
||||
});
|
||||
|
||||
const dto = new CreateMemberRequestDto();
|
||||
dto.userId = user.id;
|
||||
dto.roleId = role.id;
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/members`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(201);
|
||||
|
||||
const allMembers: { id: number }[] = await dataSource.query(
|
||||
'SELECT id FROM members ORDER BY id DESC LIMIT 1',
|
||||
);
|
||||
memberId = allMembers.length > 0 ? allMembers[0].id : 1;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await dataSource.query(
|
||||
'DELETE FROM members WHERE role_id = ? OR role_id = ?',
|
||||
[role.id, newRole.id],
|
||||
);
|
||||
await dataSource.query('DELETE FROM roles WHERE id = ?', [newRole.id]);
|
||||
});
|
||||
|
||||
it('should update member role', async () => {
|
||||
const dto = new UpdateMemberRequestDto();
|
||||
dto.roleId = newRole.id;
|
||||
|
||||
const response = await request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/members/${memberId}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent role', async () => {
|
||||
const dto = new UpdateMemberRequestDto();
|
||||
dto.roleId = 999;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/members/${memberId}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 400 for non-existent member', async () => {
|
||||
const dto = new UpdateMemberRequestDto();
|
||||
dto.roleId = newRole.id;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/members/999`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
const dto = new UpdateMemberRequestDto();
|
||||
dto.roleId = newRole.id;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/members/${memberId}`)
|
||||
.send(dto)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/members/:memberId (DELETE)', () => {
|
||||
let memberId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dto = new CreateMemberRequestDto();
|
||||
dto.userId = user.id;
|
||||
dto.roleId = role.id;
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/members`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(201);
|
||||
|
||||
const allMembers: { id: number }[] = await dataSource.query(
|
||||
'SELECT id FROM members ORDER BY id DESC LIMIT 1',
|
||||
);
|
||||
memberId = allMembers.length > 0 ? allMembers[0].id : 1;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await dataSource.query('DELETE FROM members WHERE role_id = ?', [
|
||||
role.id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should delete member', async () => {
|
||||
const response = await request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/members/${memberId}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
it('should return 200 when deleting non-existent member', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/members/999`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/members/${memberId}`)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const delay = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
await delay(500);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Server } from 'net';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm';
|
||||
import request from 'supertest';
|
||||
import type { DataSource, Repository } from 'typeorm';
|
||||
import { initializeTransactionalContext } from 'typeorm-transactional';
|
||||
|
||||
import { AppModule } from '@/app.module';
|
||||
import { OpensearchRepository } from '@/common/repositories';
|
||||
import { AuthService } from '@/domains/admin/auth/auth.service';
|
||||
import { ChannelService } from '@/domains/admin/channel/channel/channel.service';
|
||||
import { FieldEntity } from '@/domains/admin/channel/field/field.entity';
|
||||
import { FeedbackService } from '@/domains/admin/feedback/feedback.service';
|
||||
import {
|
||||
CreateProjectRequestDto,
|
||||
FindProjectsRequestDto,
|
||||
UpdateProjectRequestDto,
|
||||
} from '@/domains/admin/project/project/dtos/requests';
|
||||
import type {
|
||||
CountFeedbacksByIdResponseDto,
|
||||
FindProjectByIdResponseDto,
|
||||
FindProjectsResponseDto,
|
||||
} from '@/domains/admin/project/project/dtos/responses';
|
||||
import { ProjectService } from '@/domains/admin/project/project/project.service';
|
||||
import { SetupTenantRequestDto } from '@/domains/admin/tenant/dtos/requests';
|
||||
import { TenantService } from '@/domains/admin/tenant/tenant.service';
|
||||
import {
|
||||
clearAllEntities,
|
||||
createChannel,
|
||||
createFeedback,
|
||||
signInTestUser,
|
||||
} from '@/test-utils/util-functions';
|
||||
|
||||
describe('ProjectController (integration)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let dataSource: DataSource;
|
||||
let authService: AuthService;
|
||||
|
||||
let tenantService: TenantService;
|
||||
let projectService: ProjectService;
|
||||
let channelService: ChannelService;
|
||||
let feedbackService: FeedbackService;
|
||||
let configService: ConfigService;
|
||||
|
||||
let fieldRepo: Repository<FieldEntity>;
|
||||
|
||||
let opensearchRepository: OpensearchRepository;
|
||||
|
||||
let accessToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
initializeTransactionalContext();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
await app.init();
|
||||
|
||||
dataSource = module.get(getDataSourceToken());
|
||||
|
||||
authService = module.get(AuthService);
|
||||
tenantService = module.get(TenantService);
|
||||
projectService = module.get(ProjectService);
|
||||
channelService = module.get(ChannelService);
|
||||
feedbackService = module.get(FeedbackService);
|
||||
configService = module.get(ConfigService);
|
||||
|
||||
opensearchRepository = module.get(OpensearchRepository);
|
||||
|
||||
fieldRepo = module.get(getRepositoryToken(FieldEntity));
|
||||
|
||||
await clearAllEntities(module);
|
||||
if (configService.get('opensearch.use')) {
|
||||
await opensearchRepository.deleteAllIndexes();
|
||||
}
|
||||
|
||||
const dto = new SetupTenantRequestDto();
|
||||
dto.siteName = faker.string.sample();
|
||||
dto.password = '12345678';
|
||||
await tenantService.create(dto);
|
||||
|
||||
const { jwt } = await signInTestUser(dataSource, authService);
|
||||
accessToken = jwt.accessToken;
|
||||
});
|
||||
|
||||
describe('/admin/projects (POST)', () => {
|
||||
it('should create a project', async () => {
|
||||
const dto = new CreateProjectRequestDto();
|
||||
dto.name = 'TestProject';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(201);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects (GET)', () => {
|
||||
it('should find projects', async () => {
|
||||
const dto = new FindProjectsRequestDto();
|
||||
dto.limit = 10;
|
||||
dto.page = 1;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.query(dto)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: FindProjectsResponseDto }) => {
|
||||
expect(body.items.length).toEqual(1);
|
||||
expect(body.items[0].name).toEqual('TestProject');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId (GET)', () => {
|
||||
it('should find a project by id', async () => {
|
||||
const dto = new FindProjectsRequestDto();
|
||||
dto.limit = 10;
|
||||
dto.page = 1;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/1`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.query(dto)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: FindProjectByIdResponseDto }) => {
|
||||
expect(body.name).toEqual('TestProject');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/feedback-count (GET)', () => {
|
||||
it('should count feedbacks by project id', async () => {
|
||||
const project = await projectService.findById({ projectId: 1 });
|
||||
const channel = await createChannel(channelService, project);
|
||||
|
||||
const fields = await fieldRepo.find({
|
||||
where: { channel: { id: channel.id } },
|
||||
relations: { options: true },
|
||||
});
|
||||
|
||||
await createFeedback(fields, channel.id, feedbackService);
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/1/feedback-count`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: CountFeedbacksByIdResponseDto }) => {
|
||||
expect(body.total).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId (PUT)', () => {
|
||||
it('should update a project', async () => {
|
||||
const dto = new UpdateProjectRequestDto();
|
||||
dto.name = 'UpdatedTestProject';
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/1`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(200);
|
||||
|
||||
const findDto = new FindProjectsRequestDto();
|
||||
findDto.limit = 10;
|
||||
findDto.page = 1;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.query(findDto)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: FindProjectsResponseDto }) => {
|
||||
expect(body.items.length).toEqual(1);
|
||||
expect(body.items[0].name).toEqual('UpdatedTestProject');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId (DELETE)', () => {
|
||||
it('should delete a project', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/1`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const findDto = new FindProjectsRequestDto();
|
||||
findDto.limit = 10;
|
||||
findDto.page = 1;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.query(findDto)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: FindProjectsResponseDto }) => {
|
||||
expect(body.items.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const delay = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
await delay(500);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Server } from 'net';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
import request from 'supertest';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import { initializeTransactionalContext } from 'typeorm-transactional';
|
||||
|
||||
import { AppModule } from '@/app.module';
|
||||
import { OpensearchRepository } from '@/common/repositories';
|
||||
import { AuthService } from '@/domains/admin/auth/auth.service';
|
||||
import type { ProjectEntity } from '@/domains/admin/project/project/project.entity';
|
||||
import { ProjectService } from '@/domains/admin/project/project/project.service';
|
||||
import {
|
||||
CreateRoleRequestDto,
|
||||
UpdateRoleRequestDto,
|
||||
} from '@/domains/admin/project/role/dtos/requests';
|
||||
import type { GetAllRolesResponseDto } from '@/domains/admin/project/role/dtos/responses';
|
||||
import type { GetAllRolesResponseRoleDto } from '@/domains/admin/project/role/dtos/responses/get-all-roles-response.dto';
|
||||
import { PermissionEnum } from '@/domains/admin/project/role/permission.enum';
|
||||
import { RoleService } from '@/domains/admin/project/role/role.service';
|
||||
import { SetupTenantRequestDto } from '@/domains/admin/tenant/dtos/requests';
|
||||
import { TenantService } from '@/domains/admin/tenant/tenant.service';
|
||||
import { clearAllEntities, signInTestUser } from '@/test-utils/util-functions';
|
||||
|
||||
describe('RoleController (integration)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let dataSource: DataSource;
|
||||
let authService: AuthService;
|
||||
let tenantService: TenantService;
|
||||
let projectService: ProjectService;
|
||||
let _roleService: RoleService;
|
||||
let configService: ConfigService;
|
||||
let opensearchRepository: OpensearchRepository;
|
||||
|
||||
let project: ProjectEntity;
|
||||
let accessToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
initializeTransactionalContext();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
await app.init();
|
||||
|
||||
dataSource = module.get(getDataSourceToken());
|
||||
authService = module.get(AuthService);
|
||||
tenantService = module.get(TenantService);
|
||||
projectService = module.get(ProjectService);
|
||||
_roleService = module.get(RoleService);
|
||||
configService = module.get(ConfigService);
|
||||
opensearchRepository = module.get(OpensearchRepository);
|
||||
|
||||
await clearAllEntities(module);
|
||||
if (configService.get('opensearch.use')) {
|
||||
await opensearchRepository.deleteAllIndexes();
|
||||
}
|
||||
|
||||
const dto = new SetupTenantRequestDto();
|
||||
dto.siteName = faker.string.sample();
|
||||
dto.password = '12345678';
|
||||
await tenantService.create(dto);
|
||||
|
||||
project = await projectService.create({
|
||||
name: faker.lorem.words(),
|
||||
description: faker.lorem.lines(1),
|
||||
timezone: {
|
||||
countryCode: 'KR',
|
||||
name: 'Asia/Seoul',
|
||||
offset: '+09:00',
|
||||
},
|
||||
});
|
||||
|
||||
const { jwt } = await signInTestUser(dataSource, authService);
|
||||
accessToken = jwt.accessToken;
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/roles (POST)', () => {
|
||||
it('should create a role', async () => {
|
||||
const dto = new CreateRoleRequestDto();
|
||||
dto.name = 'TestRole';
|
||||
dto.permissions = [
|
||||
PermissionEnum.feedback_download_read,
|
||||
PermissionEnum.feedback_update,
|
||||
];
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/roles`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(201);
|
||||
|
||||
const listResponse = await request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/roles`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.query({
|
||||
searchText: 'TestRole',
|
||||
page: 1,
|
||||
limit: 10,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const roles = (listResponse.body as GetAllRolesResponseDto).roles;
|
||||
expect(roles.length).toBeGreaterThan(0);
|
||||
|
||||
const createdRole = roles.find(
|
||||
(role: GetAllRolesResponseRoleDto) => role.name === 'TestRole',
|
||||
);
|
||||
expect(createdRole).toBeDefined();
|
||||
expect(createdRole?.name).toBe('TestRole');
|
||||
expect(createdRole?.permissions).toEqual([
|
||||
'feedback_download_read',
|
||||
'feedback_update',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return 400 for empty role name', async () => {
|
||||
const dto = new CreateRoleRequestDto();
|
||||
dto.permissions = [PermissionEnum.feedback_download_read];
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/roles`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 400 for invalid permissions', async () => {
|
||||
const dto = new CreateRoleRequestDto();
|
||||
dto.name = 'TestRole';
|
||||
dto.permissions = [];
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/roles`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
const dto = new CreateRoleRequestDto();
|
||||
dto.name = 'TestRole';
|
||||
dto.permissions = [PermissionEnum.feedback_download_read];
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/roles`)
|
||||
.send(dto)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/roles (GET)', () => {
|
||||
beforeEach(async () => {
|
||||
const dto = new CreateRoleRequestDto();
|
||||
dto.name = 'TestRoleForList';
|
||||
dto.permissions = [PermissionEnum.feedback_download_read];
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/roles`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto);
|
||||
});
|
||||
|
||||
it('should find roles by project id', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/roles`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.query({
|
||||
searchText: 'TestRole',
|
||||
page: 1,
|
||||
limit: 10,
|
||||
})
|
||||
.expect(200)
|
||||
.then(({ body }: { body: GetAllRolesResponseDto }) => {
|
||||
const responseBody = body;
|
||||
expect(responseBody.roles.length).toBeGreaterThan(0);
|
||||
expect(responseBody.roles[0]).toHaveProperty('id');
|
||||
expect(responseBody.roles[0]).toHaveProperty('name');
|
||||
expect(responseBody.roles[0]).toHaveProperty('permissions');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/roles`)
|
||||
.query({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
})
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/roles/:roleId (PUT)', () => {
|
||||
let roleId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const dto = new CreateRoleRequestDto();
|
||||
dto.name = 'TestRoleForUpdate';
|
||||
dto.permissions = [PermissionEnum.feedback_download_read];
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/roles`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(201);
|
||||
|
||||
const response = await request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/roles`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const roles = (response.body as GetAllRolesResponseDto).roles;
|
||||
const createdRole = roles.find(
|
||||
(role: GetAllRolesResponseRoleDto) => role.name === 'TestRoleForUpdate',
|
||||
);
|
||||
if (!createdRole) {
|
||||
throw new Error('TestRoleForUpdate not found');
|
||||
}
|
||||
roleId = createdRole.id;
|
||||
});
|
||||
|
||||
it('should update role', async () => {
|
||||
const dto = new UpdateRoleRequestDto();
|
||||
dto.name = 'UpdatedTestRole';
|
||||
dto.permissions = [PermissionEnum.feedback_download_read];
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/roles/${roleId}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(204);
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/roles`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: GetAllRolesResponseDto }) => {
|
||||
const roles = body.roles;
|
||||
const updatedRole = roles.find(
|
||||
(role: GetAllRolesResponseRoleDto) =>
|
||||
role.name === 'UpdatedTestRole',
|
||||
);
|
||||
if (!updatedRole) {
|
||||
throw new Error('UpdatedTestRole not found');
|
||||
}
|
||||
expect(updatedRole.name).toBe('UpdatedTestRole');
|
||||
expect(updatedRole.permissions).toEqual(['feedback_download_read']);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 for empty role name', async () => {
|
||||
const dto = new UpdateRoleRequestDto();
|
||||
dto.permissions = [PermissionEnum.feedback_download_read];
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/roles/${roleId}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
const dto = new UpdateRoleRequestDto();
|
||||
dto.name = 'UpdatedRole';
|
||||
dto.permissions = [PermissionEnum.feedback_download_read];
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/roles/${roleId}`)
|
||||
.send(dto)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/roles/:roleId (DELETE)', () => {
|
||||
let roleId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const dto = new CreateRoleRequestDto();
|
||||
dto.name = 'TestRoleForDelete';
|
||||
dto.permissions = [PermissionEnum.feedback_download_read];
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/roles`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(201);
|
||||
|
||||
const response = await request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/roles`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const roles = (response.body as GetAllRolesResponseDto).roles;
|
||||
const createdRole = roles.find(
|
||||
(role: GetAllRolesResponseRoleDto) => role.name === 'TestRoleForDelete',
|
||||
);
|
||||
if (!createdRole) {
|
||||
throw new Error('TestRoleForDelete not found');
|
||||
}
|
||||
roleId = createdRole.id;
|
||||
});
|
||||
|
||||
it('should delete role', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/roles/${roleId}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const response = await request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/roles`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const roles = (response.body as GetAllRolesResponseDto).roles;
|
||||
const deletedRole = roles.find(
|
||||
(role: GetAllRolesResponseRoleDto) => role.name === 'TestRoleForDelete',
|
||||
);
|
||||
expect(deletedRole).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/roles/${roleId}`)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const delay = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
await delay(500);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Server } from 'net';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
import request from 'supertest';
|
||||
import type { DataSource, Repository } from 'typeorm';
|
||||
import { initializeTransactionalContext } from 'typeorm-transactional';
|
||||
|
||||
import { AppModule } from '@/app.module';
|
||||
import { AuthService } from '@/domains/admin/auth/auth.service';
|
||||
import {
|
||||
SetupTenantRequestDto,
|
||||
UpdateTenantRequestDto,
|
||||
} from '@/domains/admin/tenant/dtos/requests';
|
||||
import type { GetTenantResponseDto } from '@/domains/admin/tenant/dtos/responses';
|
||||
import { TenantEntity } from '@/domains/admin/tenant/tenant.entity';
|
||||
import { UserEntity } from '@/domains/admin/user/entities/user.entity';
|
||||
import {
|
||||
clearAllEntities,
|
||||
clearEntities,
|
||||
signInTestUser,
|
||||
} from '@/test-utils/util-functions';
|
||||
import { HttpStatusCode } from '@/types/http-status';
|
||||
|
||||
describe('TenantController (integration)', () => {
|
||||
let module: TestingModule;
|
||||
let app: INestApplication;
|
||||
|
||||
let dataSource: DataSource;
|
||||
let tenantRepo: Repository<TenantEntity>;
|
||||
let userRepo: Repository<UserEntity>;
|
||||
|
||||
let authService: AuthService;
|
||||
|
||||
beforeAll(async () => {
|
||||
initializeTransactionalContext();
|
||||
module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
dataSource = module.get(getDataSourceToken());
|
||||
tenantRepo = dataSource.getRepository(TenantEntity);
|
||||
userRepo = dataSource.getRepository(UserEntity);
|
||||
|
||||
authService = module.get(AuthService);
|
||||
|
||||
app = module.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await clearAllEntities(module);
|
||||
});
|
||||
|
||||
describe('/admin/tenants (POST)', () => {
|
||||
it('should create a tenant', async () => {
|
||||
const dto = new SetupTenantRequestDto();
|
||||
dto.siteName = faker.string.sample();
|
||||
dto.password = '12345678';
|
||||
|
||||
return await request(app.getHttpServer() as Server)
|
||||
.post('/admin/tenants')
|
||||
.send(dto)
|
||||
.expect(201)
|
||||
.then(async () => {
|
||||
const tenants = await tenantRepo.find();
|
||||
expect(tenants).toHaveLength(1);
|
||||
const [tenant] = tenants;
|
||||
for (const key in dto) {
|
||||
if (['email', 'password'].includes(key)) continue;
|
||||
const value = dto[key] as string;
|
||||
expect(tenant[key]).toEqual(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
it('should return bad request since tenant is already exists', async () => {
|
||||
await tenantRepo.save({
|
||||
siteName: faker.string.sample(),
|
||||
allowDomains: [],
|
||||
});
|
||||
const dto = new SetupTenantRequestDto();
|
||||
dto.siteName = faker.string.sample();
|
||||
dto.password = '12345678';
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post('/admin/tenants')
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await clearEntities([tenantRepo]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/tenants (PUT)', () => {
|
||||
let tenant: TenantEntity;
|
||||
let accessToken: string;
|
||||
beforeEach(async () => {
|
||||
tenant = await tenantRepo.save({
|
||||
siteName: faker.string.sample(),
|
||||
allowDomains: [],
|
||||
});
|
||||
const { jwt } = await signInTestUser(dataSource, authService);
|
||||
accessToken = jwt.accessToken;
|
||||
});
|
||||
it('should update a tenant', async () => {
|
||||
const dto = new UpdateTenantRequestDto();
|
||||
|
||||
dto.siteName = faker.string.sample();
|
||||
dto.allowDomains = [];
|
||||
|
||||
return await request(app.getHttpServer() as Server)
|
||||
.put('/admin/tenants')
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(204)
|
||||
.then(async () => {
|
||||
const updatedTenant = await tenantRepo.findOne({
|
||||
where: { id: tenant.id },
|
||||
});
|
||||
expect(updatedTenant?.siteName).toEqual(dto.siteName);
|
||||
expect(updatedTenant?.allowDomains).toEqual(dto.allowDomains);
|
||||
});
|
||||
});
|
||||
it('should fail to find a tenant', async () => {
|
||||
await clearEntities([tenantRepo]);
|
||||
|
||||
const dto = new UpdateTenantRequestDto();
|
||||
|
||||
dto.siteName = faker.string.sample();
|
||||
dto.allowDomains = [];
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.put('/admin/tenants')
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(404);
|
||||
});
|
||||
it('should reject the request when unauthorized', async () => {
|
||||
const dto = new UpdateTenantRequestDto();
|
||||
|
||||
dto.siteName = faker.string.sample();
|
||||
dto.allowDomains = [];
|
||||
|
||||
return await request(app.getHttpServer() as Server)
|
||||
.put('/admin/tenants')
|
||||
.send(dto)
|
||||
.expect(HttpStatusCode.UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/tenants (GET)', () => {
|
||||
const dto = new SetupTenantRequestDto();
|
||||
beforeEach(async () => {
|
||||
await clearEntities([tenantRepo, userRepo]);
|
||||
dto.siteName = faker.string.sample();
|
||||
dto.password = '12345678';
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post('/admin/tenants')
|
||||
.send(dto);
|
||||
});
|
||||
|
||||
it('should find a tenant', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.get('/admin/tenants')
|
||||
.expect(200)
|
||||
.expect(({ body }) => {
|
||||
expect(dto.siteName).toEqual((body as GetTenantResponseDto).siteName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const delay = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
await delay(500);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Server } from 'net';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
import { DateTime } from 'luxon';
|
||||
import request from 'supertest';
|
||||
import type { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { AppModule } from '@/app.module';
|
||||
import { AuthService } from '@/domains/admin/auth/auth.service';
|
||||
import { RoleEntity } from '@/domains/admin/project/role/role.entity';
|
||||
import { TenantEntity } from '@/domains/admin/tenant/tenant.entity';
|
||||
import { TenantService } from '@/domains/admin/tenant/tenant.service';
|
||||
import type { UserDto } from '@/domains/admin/user/dtos';
|
||||
import type { GetAllUserResponseDto } from '@/domains/admin/user/dtos/responses/get-all-user-response.dto';
|
||||
import { UserStateEnum } from '@/domains/admin/user/entities/enums';
|
||||
import { UserEntity } from '@/domains/admin/user/entities/user.entity';
|
||||
import {
|
||||
clearEntities,
|
||||
createTenant,
|
||||
getRandomEnumValue,
|
||||
signInTestUser,
|
||||
} from '@/test-utils/util-functions';
|
||||
import { HttpStatusCode } from '@/types/http-status';
|
||||
|
||||
describe('UserController (integration)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let dataSource: DataSource;
|
||||
let userRepo: Repository<UserEntity>;
|
||||
let roleRepo: Repository<RoleEntity>;
|
||||
let tenantRepo: Repository<TenantEntity>;
|
||||
|
||||
let tenantService: TenantService;
|
||||
|
||||
let authService: AuthService;
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({ transform: true, whitelist: true }),
|
||||
);
|
||||
await app.init();
|
||||
|
||||
dataSource = module.get(getDataSourceToken());
|
||||
userRepo = dataSource.getRepository(UserEntity);
|
||||
roleRepo = dataSource.getRepository(RoleEntity);
|
||||
tenantRepo = dataSource.getRepository(TenantEntity);
|
||||
authService = module.get(AuthService);
|
||||
tenantService = module.get(TenantService);
|
||||
|
||||
await clearEntities([tenantRepo, userRepo, roleRepo]);
|
||||
|
||||
await createTenant(tenantService);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await dataSource.destroy();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
let total: number;
|
||||
let userEntities: UserEntity[];
|
||||
let accessToken: string;
|
||||
let ownerUser: UserEntity;
|
||||
|
||||
beforeEach(async () => {
|
||||
await clearEntities([userRepo, roleRepo]);
|
||||
|
||||
const length = faker.number.int({ min: 3, max: 8 });
|
||||
|
||||
userEntities = (
|
||||
await userRepo.save(
|
||||
Array.from({ length: length }).map(() => ({
|
||||
email: faker.internet.email(),
|
||||
state: getRandomEnumValue(UserStateEnum),
|
||||
hashPassword: faker.internet.password(),
|
||||
})),
|
||||
)
|
||||
).sort((a, b) =>
|
||||
DateTime.fromJSDate(b.createdAt)
|
||||
.diff(DateTime.fromJSDate(a.createdAt))
|
||||
.as('milliseconds'),
|
||||
);
|
||||
|
||||
const { jwt, user } = await signInTestUser(dataSource, authService);
|
||||
accessToken = jwt.accessToken;
|
||||
ownerUser = user;
|
||||
|
||||
total = length + 1;
|
||||
});
|
||||
|
||||
describe('/admin/users (GET)', () => {
|
||||
it('should return all users', async () => {
|
||||
const expectUsers = userEntities
|
||||
.concat(ownerUser)
|
||||
.sort((a, b) =>
|
||||
DateTime.fromJSDate(a.createdAt)
|
||||
.diff(DateTime.fromJSDate(b.createdAt))
|
||||
.as('milliseconds'),
|
||||
)
|
||||
.map(({ id, email }) => ({
|
||||
id,
|
||||
email,
|
||||
}))
|
||||
.slice(0, 10);
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get('/admin/users')
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(HttpStatusCode.OK)
|
||||
.expect(({ body }) => {
|
||||
expect(body).toHaveProperty('items');
|
||||
expect(body).toHaveProperty('meta');
|
||||
|
||||
const { items, meta } = body as GetAllUserResponseDto;
|
||||
[
|
||||
'name',
|
||||
'department',
|
||||
'type',
|
||||
'members',
|
||||
'createdAt',
|
||||
'signUpMethod',
|
||||
].forEach((field) => items.forEach((item) => delete item[field]));
|
||||
expect(items).toEqual(expectUsers);
|
||||
expect(meta.totalItems).toEqual(total);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return unauthorized status code', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get('/admin/users')
|
||||
.expect(HttpStatusCode.UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/users (DELETE)', () => {
|
||||
it('should return empty result', async () => {
|
||||
const ids = faker.helpers.arrayElements(userEntities).map((v) => v.id);
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/users`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({ ids })
|
||||
.expect(HttpStatusCode.OK)
|
||||
.then(async () => {
|
||||
for (const id of ids) {
|
||||
const result = await userRepo.findOneBy({ id });
|
||||
expect(result).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should return unauthorized status code', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/users`)
|
||||
.expect(HttpStatusCode.UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/users/:id (GET)', () => {
|
||||
it('check signed-in user', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.get(`/admin/users/${ownerUser.id}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200)
|
||||
.expect(({ body }) => {
|
||||
expect((body as UserDto).id).toEqual(ownerUser.id);
|
||||
expect((body as UserDto).email).toEqual(ownerUser.email);
|
||||
});
|
||||
});
|
||||
it('should return unauthorized status code', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.get(`/admin/users/${ownerUser.id}`)
|
||||
.expect(HttpStatusCode.UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/users/:id (DELETE)', () => {
|
||||
it('should return empty result', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/users/${ownerUser.id}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(HttpStatusCode.OK)
|
||||
.then(async () => {
|
||||
const result = await userRepo.findOneBy({ id: ownerUser.id });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
it('should return unauthorized status code', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/users/${faker.number.int()}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(HttpStatusCode.UNAUTHORIZED);
|
||||
});
|
||||
it('should return unauthorized status code', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/users/${ownerUser.id}`)
|
||||
.expect(HttpStatusCode.UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/users/:id/roles (GET)', () => {
|
||||
it('should return OK', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.get(`/admin/users/${ownerUser.id}/roles`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(HttpStatusCode.OK);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/users/:id/roles (PUT)', () => {
|
||||
it('should return unauthorized status code', async () => {
|
||||
const role = await roleRepo.save({
|
||||
name: faker.string.sample(),
|
||||
permissions: [],
|
||||
});
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.put(`/admin/users/${ownerUser.id}`)
|
||||
.send({ roleId: role.id })
|
||||
.expect(HttpStatusCode.UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,486 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Server } from 'net';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
import request from 'supertest';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import { initializeTransactionalContext } from 'typeorm-transactional';
|
||||
|
||||
import { AppModule } from '@/app.module';
|
||||
import {
|
||||
EventStatusEnum,
|
||||
EventTypeEnum,
|
||||
WebhookStatusEnum,
|
||||
} from '@/common/enums';
|
||||
import { OpensearchRepository } from '@/common/repositories';
|
||||
import { AuthService } from '@/domains/admin/auth/auth.service';
|
||||
import type { ProjectEntity } from '@/domains/admin/project/project/project.entity';
|
||||
import { ProjectService } from '@/domains/admin/project/project/project.service';
|
||||
import {
|
||||
CreateWebhookRequestDto,
|
||||
UpdateWebhookRequestDto,
|
||||
} from '@/domains/admin/project/webhook/dtos/requests';
|
||||
import type {
|
||||
GetWebhookByIdResponseDto,
|
||||
GetWebhooksByProjectIdResponseDto,
|
||||
} from '@/domains/admin/project/webhook/dtos/responses';
|
||||
import { SetupTenantRequestDto } from '@/domains/admin/tenant/dtos/requests';
|
||||
import { TenantService } from '@/domains/admin/tenant/tenant.service';
|
||||
import { clearAllEntities, signInTestUser } from '@/test-utils/util-functions';
|
||||
|
||||
describe('WebhookController (integration)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let dataSource: DataSource;
|
||||
let authService: AuthService;
|
||||
let tenantService: TenantService;
|
||||
let projectService: ProjectService;
|
||||
let configService: ConfigService;
|
||||
let opensearchRepository: OpensearchRepository;
|
||||
|
||||
let project: ProjectEntity;
|
||||
let accessToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
initializeTransactionalContext();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
await app.init();
|
||||
|
||||
dataSource = module.get(getDataSourceToken());
|
||||
authService = module.get(AuthService);
|
||||
tenantService = module.get(TenantService);
|
||||
projectService = module.get(ProjectService);
|
||||
configService = module.get(ConfigService);
|
||||
opensearchRepository = module.get(OpensearchRepository);
|
||||
|
||||
await clearAllEntities(module);
|
||||
if (configService.get('opensearch.use')) {
|
||||
await opensearchRepository.deleteAllIndexes();
|
||||
}
|
||||
|
||||
const dto = new SetupTenantRequestDto();
|
||||
dto.siteName = faker.string.sample();
|
||||
dto.password = '12345678';
|
||||
await tenantService.create(dto);
|
||||
|
||||
project = await projectService.create({
|
||||
name: faker.lorem.words(),
|
||||
description: faker.lorem.lines(1),
|
||||
timezone: {
|
||||
countryCode: 'KR',
|
||||
name: 'Asia/Seoul',
|
||||
offset: '+09:00',
|
||||
},
|
||||
});
|
||||
|
||||
const { jwt } = await signInTestUser(dataSource, authService);
|
||||
accessToken = jwt.accessToken;
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/webhooks (POST)', () => {
|
||||
it('should create a webhook', async () => {
|
||||
const dto = new CreateWebhookRequestDto();
|
||||
dto.name = 'TestWebhook';
|
||||
dto.url = 'https://example.com/webhook';
|
||||
dto.events = [
|
||||
{
|
||||
type: EventTypeEnum.FEEDBACK_CREATION,
|
||||
status: EventStatusEnum.ACTIVE,
|
||||
channelIds: [],
|
||||
},
|
||||
];
|
||||
dto.status = WebhookStatusEnum.ACTIVE;
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/webhooks`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(201);
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/webhooks`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(200)
|
||||
.then(({ body }: { body: GetWebhooksByProjectIdResponseDto }) => {
|
||||
expect(body.items[0].name).toBe('TestWebhook');
|
||||
expect(body.items[0].url).toBe('https://example.com/webhook');
|
||||
expect(body.items[0].events).toHaveLength(1);
|
||||
expect(body.items[0].status).toBe(WebhookStatusEnum.ACTIVE);
|
||||
expect(body.items[0].createdAt).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 for empty webhook name', async () => {
|
||||
const dto = new CreateWebhookRequestDto();
|
||||
dto.url = 'https://example.com/webhook';
|
||||
dto.events = [
|
||||
{
|
||||
type: EventTypeEnum.FEEDBACK_CREATION,
|
||||
status: EventStatusEnum.ACTIVE,
|
||||
channelIds: [],
|
||||
},
|
||||
];
|
||||
dto.status = WebhookStatusEnum.ACTIVE;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/webhooks`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 400 for invalid URL format', async () => {
|
||||
const dto = new CreateWebhookRequestDto();
|
||||
dto.name = 'TestWebhook';
|
||||
dto.url = 'invalid-url';
|
||||
dto.events = [
|
||||
{
|
||||
type: EventTypeEnum.FEEDBACK_CREATION,
|
||||
status: EventStatusEnum.ACTIVE,
|
||||
channelIds: [],
|
||||
},
|
||||
];
|
||||
dto.status = WebhookStatusEnum.ACTIVE;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/webhooks`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 400 for empty events array', async () => {
|
||||
const dto = new CreateWebhookRequestDto();
|
||||
dto.name = 'TestWebhook';
|
||||
dto.url = 'https://example.com/webhook';
|
||||
dto.events = [];
|
||||
dto.status = WebhookStatusEnum.ACTIVE;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/webhooks`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
const dto = new CreateWebhookRequestDto();
|
||||
dto.name = 'TestWebhook';
|
||||
dto.url = 'https://example.com/webhook';
|
||||
dto.events = [
|
||||
{
|
||||
type: EventTypeEnum.FEEDBACK_CREATION,
|
||||
status: EventStatusEnum.ACTIVE,
|
||||
channelIds: [],
|
||||
},
|
||||
];
|
||||
dto.status = WebhookStatusEnum.ACTIVE;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/webhooks`)
|
||||
.send(dto)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/webhooks (GET)', () => {
|
||||
beforeEach(async () => {
|
||||
const dto = new CreateWebhookRequestDto();
|
||||
dto.name = 'TestWebhookForList';
|
||||
dto.url = 'https://example.com/webhook';
|
||||
dto.events = [
|
||||
{
|
||||
type: EventTypeEnum.FEEDBACK_CREATION,
|
||||
status: EventStatusEnum.ACTIVE,
|
||||
channelIds: [],
|
||||
},
|
||||
];
|
||||
dto.status = WebhookStatusEnum.ACTIVE;
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/webhooks`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto);
|
||||
});
|
||||
|
||||
it('should find webhooks by project id', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/webhooks`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.query({
|
||||
searchText: 'TestWebhook',
|
||||
page: 1,
|
||||
limit: 10,
|
||||
})
|
||||
.expect(200)
|
||||
.then(({ body }: { body: GetWebhooksByProjectIdResponseDto }) => {
|
||||
const responseBody = body;
|
||||
expect(responseBody.items.length).toBeGreaterThan(0);
|
||||
expect(responseBody.items[0]).toHaveProperty('id');
|
||||
expect(responseBody.items[0]).toHaveProperty('name');
|
||||
expect(responseBody.items[0]).toHaveProperty('url');
|
||||
expect(responseBody.items[0]).toHaveProperty('events');
|
||||
expect(responseBody.items[0]).toHaveProperty('status');
|
||||
expect(responseBody.items[0]).toHaveProperty('createdAt');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/webhooks`)
|
||||
.query({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
})
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/webhooks/:webhookId (GET)', () => {
|
||||
let webhookId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dto = new CreateWebhookRequestDto();
|
||||
dto.name = 'TestWebhookForGet';
|
||||
dto.url = 'https://example.com/webhook';
|
||||
dto.events = [
|
||||
{
|
||||
type: EventTypeEnum.FEEDBACK_CREATION,
|
||||
status: EventStatusEnum.ACTIVE,
|
||||
channelIds: [],
|
||||
},
|
||||
];
|
||||
dto.status = WebhookStatusEnum.ACTIVE;
|
||||
|
||||
const response = await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/webhooks`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto);
|
||||
|
||||
webhookId = (response.body as { id: number }).id;
|
||||
});
|
||||
|
||||
it('should find webhook by id', async () => {
|
||||
const response = await request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/webhooks/${webhookId}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const body = response.body as GetWebhookByIdResponseDto[];
|
||||
expect(response.body).toBeDefined();
|
||||
expect(body[0].id).toBe(webhookId);
|
||||
expect(body[0].name).toBe('TestWebhookForGet');
|
||||
expect(body[0].url).toBe('https://example.com/webhook');
|
||||
expect(body[0].events).toHaveLength(1);
|
||||
expect(body[0].status).toBe(WebhookStatusEnum.ACTIVE);
|
||||
expect(body[0].createdAt).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/webhooks/${webhookId}`)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/webhooks/:webhookId (PUT)', () => {
|
||||
let webhookId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dto = new CreateWebhookRequestDto();
|
||||
dto.name = 'TestWebhookForUpdate';
|
||||
dto.url = 'https://example.com/webhook';
|
||||
dto.events = [
|
||||
{
|
||||
type: EventTypeEnum.FEEDBACK_CREATION,
|
||||
status: EventStatusEnum.ACTIVE,
|
||||
channelIds: [],
|
||||
},
|
||||
];
|
||||
dto.status = WebhookStatusEnum.ACTIVE;
|
||||
|
||||
const response = await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/webhooks`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto);
|
||||
|
||||
webhookId = (response.body as { id: number }).id;
|
||||
});
|
||||
|
||||
it('should update webhook', async () => {
|
||||
const dto = new UpdateWebhookRequestDto();
|
||||
dto.name = 'UpdatedTestWebhook';
|
||||
dto.url = 'https://updated-example.com/webhook';
|
||||
dto.events = [
|
||||
{
|
||||
type: EventTypeEnum.FEEDBACK_CREATION,
|
||||
status: EventStatusEnum.ACTIVE,
|
||||
channelIds: [],
|
||||
},
|
||||
];
|
||||
dto.status = WebhookStatusEnum.ACTIVE;
|
||||
dto.token = null;
|
||||
|
||||
await request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/webhooks/${webhookId}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(200);
|
||||
|
||||
const response = await request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/webhooks/${webhookId}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toBeDefined();
|
||||
const body = response.body as GetWebhookByIdResponseDto[];
|
||||
expect(body[0].name).toBe('UpdatedTestWebhook');
|
||||
expect(body[0].url).toBe('https://updated-example.com/webhook');
|
||||
expect(body[0].events).toHaveLength(1);
|
||||
expect(body[0].events[0].type).toBe(EventTypeEnum.FEEDBACK_CREATION);
|
||||
expect(body[0].status).toBe(WebhookStatusEnum.ACTIVE);
|
||||
});
|
||||
|
||||
it('should update webhook with empty name', async () => {
|
||||
const dto = new UpdateWebhookRequestDto();
|
||||
dto.name = '';
|
||||
dto.url = 'https://updated-example.com/webhook';
|
||||
dto.events = [
|
||||
{
|
||||
type: EventTypeEnum.FEEDBACK_CREATION,
|
||||
status: EventStatusEnum.ACTIVE,
|
||||
channelIds: [],
|
||||
},
|
||||
];
|
||||
dto.status = WebhookStatusEnum.ACTIVE;
|
||||
dto.token = null;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/webhooks/${webhookId}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent webhook', async () => {
|
||||
const dto = new UpdateWebhookRequestDto();
|
||||
dto.name = 'UpdatedWebhook';
|
||||
dto.url = 'https://updated-example.com/webhook';
|
||||
dto.events = [
|
||||
{
|
||||
type: EventTypeEnum.FEEDBACK_CREATION,
|
||||
status: EventStatusEnum.ACTIVE,
|
||||
channelIds: [],
|
||||
},
|
||||
];
|
||||
dto.status = WebhookStatusEnum.ACTIVE;
|
||||
dto.token = null;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/webhooks/999`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
const dto = new UpdateWebhookRequestDto();
|
||||
dto.name = 'UpdatedWebhook';
|
||||
dto.url = 'https://updated-example.com/webhook';
|
||||
dto.events = [
|
||||
{
|
||||
type: EventTypeEnum.FEEDBACK_CREATION,
|
||||
status: EventStatusEnum.ACTIVE,
|
||||
channelIds: [],
|
||||
},
|
||||
];
|
||||
dto.status = WebhookStatusEnum.ACTIVE;
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.put(`/admin/projects/${project.id}/webhooks/${webhookId}`)
|
||||
.send(dto)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/admin/projects/:projectId/webhooks/:webhookId (DELETE)', () => {
|
||||
let webhookId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dto = new CreateWebhookRequestDto();
|
||||
dto.name = 'TestWebhookForDelete';
|
||||
dto.url = 'https://example.com/webhook';
|
||||
dto.events = [
|
||||
{
|
||||
type: EventTypeEnum.FEEDBACK_CREATION,
|
||||
status: EventStatusEnum.ACTIVE,
|
||||
channelIds: [],
|
||||
},
|
||||
];
|
||||
dto.status = WebhookStatusEnum.ACTIVE;
|
||||
|
||||
const response = await request(app.getHttpServer() as Server)
|
||||
.post(`/admin/projects/${project.id}/webhooks`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto);
|
||||
|
||||
webhookId = (response.body as { id: number }).id;
|
||||
});
|
||||
|
||||
it('should delete webhook', async () => {
|
||||
await request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/webhooks/${webhookId}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
return request(app.getHttpServer() as Server)
|
||||
.get(`/admin/projects/${project.id}/webhooks/${webhookId}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should return 404 when deleting non-existent webhook', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/webhooks/999`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 401 when unauthorized', async () => {
|
||||
return request(app.getHttpServer() as Server)
|
||||
.delete(`/admin/projects/${project.id}/webhooks/${webhookId}`)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const delay = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
await delay(500);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user