Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions WebCA Server API.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,56 @@
{
"name": "Database Users",
"item": [
{
"name": "Get User Info (userinfo)",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/{{hostUid}}/database/users/info/{{dbname}}",
"host": ["{{base_url}}"],
"path": ["{{hostUid}}", "database", "users", "info", "{{dbname}}"]
},
"description": "Get user info (list of users) for a database. CMS task: userinfo. GET /:hostUid/database/users/info/:dbname. Auth: Bearer JWT."
},
"response": []
},
{
"name": "User Verify (userverify)",
"request": {
"method": "POST",
"header": [{"key": "Content-Type", "value": "application/json"}],
"body": {
"mode": "raw",
"raw": "{\n \"dbname\": \"demodb\",\n \"dbuser\": \"dba\",\n \"dbpasswd\": \"\"\n}"
},
"url": {
"raw": "{{base_url}}/{{hostUid}}/database/users/verify",
"host": ["{{base_url}}"],
"path": ["{{hostUid}}", "database", "users", "verify"]
},
"description": "Verify database user credentials. CMS task: userverify. POST /:hostUid/database/users/verify. Body: dbname, dbuser, dbpasswd. Auth: Bearer JWT."
},
"response": []
},
{
"name": "Create Database User (createuser)",
"request": {
"method": "POST",
"header": [{"key": "Content-Type", "value": "application/json"}],
"body": {
"mode": "raw",
"raw": "{\n \"dbname\": \"demodb\",\n \"username\": \"yifan\",\n \"userpass\": \"1111\",\n \"groups\": { \"group\": [\"public\"] },\n \"authorization\": []\n}"
},
"url": {
"raw": "{{base_url}}/{{hostUid}}/database/users",
"host": ["{{base_url}}"],
"path": ["{{hostUid}}", "database", "users"]
},
"description": "Create a database user. CMS task: createuser. POST /:hostUid/database/users. Body: dbname, username, userpass, groups, authorization. Auth: Bearer JWT."
},
"response": []
},
{
"name": "Get Database Users",
"request": {
Expand Down Expand Up @@ -1218,6 +1268,20 @@
"description": "Update a database user. Returns empty object on success.\n\n**URI:**\n```\nPUT /:hostUid/database/users/:dbname/:username\n```\n**Example:**\n```\nPUT /host-uid-1/database/users/demodb/yifan\n```\n\n**Authentication:** Required\n**Headers:**\n```\nAuthorization: Bearer <JWT_TOKEN>\nContent-Type: application/json\n```\n\n**Path Parameters:**\n- `hostUid` - Host unique identifier\n- `dbname` - Database name\n- `username` - Username to update\n\n**Request Body:**\n```json\n{\n \"userpass\": \"1111\",\n \"groups\": {\n \"group\": [\"public\"]\n },\n \"authorization\": []\n}\n```\n\n**Success Response (200 OK):**\n```json\n{}\n```\n\n**Error Response (400 Bad Request):**\n```json\n{\n \"statusCode\": 400,\n \"message\": \"Missing required fields: userpass, groups, authorization\",\n \"error\": \"Bad Request\"\n}\n```\n\n**Error Response (404 Not Found):**\n```json\n{\n \"statusCode\": 404,\n \"message\": \"Database or user not found\",\n \"error\": \"Not Found\"\n}\n```"
},
"response": []
},
{
"name": "Delete Database User (deleteuser)",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "{{base_url}}/{{hostUid}}/database/users/{{dbname}}/{{username}}",
"host": ["{{base_url}}"],
"path": ["{{hostUid}}", "database", "users", "{{dbname}}", "{{username}}"]
},
"description": "Delete a database user. CMS task: deleteuser. DELETE /:hostUid/database/users/:dbname/:username. Auth: Bearer JWT."
},
"response": []
}
],
"description": "Database user management endpoints. All endpoints require JWT authentication."
Expand Down
49 changes: 49 additions & 0 deletions apps/api-server/src/broker/broker.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,67 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BrokerController } from './broker.controller';
import { BrokerService } from './broker.service';

describe('BrokerController', () => {
let controller: BrokerController;
let brokerService: jest.Mocked<BrokerService>;

beforeEach(async () => {
const mockBrokerService = {
getBrokers: jest.fn(),
stopBroker: jest.fn(),
startBroker: jest.fn(),
restartBroker: jest.fn(),
getBrokerStatus: jest.fn(),
startAllBrokers: jest.fn(),
stopAllBrokers: jest.fn(),
};

const module: TestingModule = await Test.createTestingModule({
controllers: [BrokerController],
providers: [
{
provide: BrokerService,
useValue: mockBrokerService,
},
],
}).compile();

controller = module.get<BrokerController>(BrokerController);
brokerService = module.get(BrokerService);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});

describe('startAllBrokers', () => {
it('should call brokerService.startAllBrokers and return { success: true }', async () => {
const req = { user: { sub: 'user-123' } };
brokerService.startAllBrokers.mockResolvedValue({ success: true });

const result = await controller.startAllBrokers(req, 'host-uid-1');

expect(brokerService.startAllBrokers).toHaveBeenCalledWith(
'user-123',
'host-uid-1'
);
expect(result).toEqual({ success: true });
});
});

describe('stopAllBrokers', () => {
it('should call brokerService.stopAllBrokers and return { success: true }', async () => {
const req = { user: { sub: 'user-123' } };
brokerService.stopAllBrokers.mockResolvedValue({ success: true });

const result = await controller.stopAllBrokers(req, 'host-uid-1');

expect(brokerService.stopAllBrokers).toHaveBeenCalledWith(
'user-123',
'host-uid-1'
);
expect(result).toEqual({ success: true });
});
});
});
57 changes: 51 additions & 6 deletions apps/api-server/src/broker/broker.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Controller, Get, Logger, Param, Post, Request } from '@nestjs/common';
import { BrokerListClientResponse, GetBrokerStatusClientResponse } from '@api-interfaces';
import {
BrokerListClientResponse,
GetBrokerStatusClientResponse,
StartAllBrokersClientResponse,
StopAllBrokersClientResponse,
} from '@api-interfaces';
import { BaseCmsResponse } from '@type';
import { BrokerService } from './broker.service';

Expand All @@ -23,6 +28,46 @@ export class BrokerController {

constructor(private readonly brokerService: BrokerService) {}

/**
* Start all brokers on a host (CMS task: startbroker).
*
* @route POST /:hostUid/broker/start-all
* @param req - Request object containing user information
* @param hostUid - Host unique identifier from path parameter
* @returns StartAllBrokersClientResponse { success: true } on success
* @example
* // POST /host-uid/broker/start-all
*/
@Post('start-all')
async startAllBrokers(
@Request() req,
@Param('hostUid') hostUid: string
): Promise<StartAllBrokersClientResponse> {
const userId = req.user.sub;
this.logger.log(`Starting all brokers on host: ${hostUid}`);
return await this.brokerService.startAllBrokers(userId, hostUid);
}

/**
* Stop all brokers on a host (CMS task: stopbroker).
*
* @route POST /:hostUid/broker/stop-all
* @param req - Request object containing user information
* @param hostUid - Host unique identifier from path parameter
* @returns StopAllBrokersClientResponse { success: true } on success
* @example
* // POST /host-uid/broker/stop-all
*/
@Post('stop-all')
async stopAllBrokers(
@Request() req,
@Param('hostUid') hostUid: string
): Promise<StopAllBrokersClientResponse> {
const userId = req.user.sub;
this.logger.log(`Stopping all brokers on host: ${hostUid}`);
return await this.brokerService.stopAllBrokers(userId, hostUid);
}

/**
* Get list of brokers for a specific host.
*
Expand All @@ -31,7 +76,7 @@ export class BrokerController {
* @param hostUid - Host unique identifier from path parameter
* @returns List of brokers
* @example
* // POST /host-uid/broker/list
* // GET /host-uid/broker/list
*/
@Get('list')
async getBrokers(
Expand Down Expand Up @@ -63,7 +108,7 @@ export class BrokerController {
): Promise<BaseCmsResponse> {
const userId = req.user.sub;

Logger.log(`Stopping broker: ${bname} on host: ${hostUid}`, 'BrokerController');
this.logger.log(`Stopping broker: ${bname} on host: ${hostUid}`);
const response = await this.brokerService.stopBroker(userId, hostUid, bname);
return response;
}
Expand All @@ -87,7 +132,7 @@ export class BrokerController {
): Promise<BaseCmsResponse> {
const userId = req.user.sub;

Logger.log(`Starting broker: ${bname} on host: ${hostUid}`, 'BrokerController');
this.logger.log(`Starting broker: ${bname} on host: ${hostUid}`);
const response = await this.brokerService.startBroker(userId, hostUid, bname);
return response;
}
Expand All @@ -111,7 +156,7 @@ export class BrokerController {
): Promise<boolean> {
const userId = req.user.sub;

Logger.log(`Restarting broker: ${bname} on host: ${hostUid}`, 'BrokerController');
this.logger.log(`Restarting broker: ${bname} on host: ${hostUid}`);
const response: boolean = await this.brokerService.restartBroker(userId, hostUid, bname);
return response;
}
Expand All @@ -135,7 +180,7 @@ export class BrokerController {
): Promise<GetBrokerStatusClientResponse> {
const userId = req.user.sub;

Logger.log(`Getting broker status: ${bname} on host: ${hostUid}`, 'BrokerController');
this.logger.log(`Getting broker status: ${bname} on host: ${hostUid}`);
const response = await this.brokerService.getBrokerStatus(userId, hostUid, bname);
return response;
}
Expand Down
122 changes: 120 additions & 2 deletions apps/api-server/src/broker/broker.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,136 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BrokerService } from './broker.service';
import { HostService } from '@host';
import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.service';
import { BrokerError } from '@error/broker/broker-error';
import * as common from '@common';

jest.mock('@common', () => ({
...jest.requireActual('@common'),
checkCmsTokenError: jest.fn(),
checkCmsStatusError: jest.fn(),
}));

describe('BrokerService', () => {
let service: BrokerService;
let hostService: jest.Mocked<HostService>;
let cmsClient: jest.Mocked<CmsHttpsClientService>;

const mockHost = {
uid: 'host-uid-1',
id: 'host-1',
address: 'localhost',
port: 8001,
password: 'host-password',
token: 'test-token',
dbProfiles: {},
};

const mockUserId = 'user-123';
const mockHostUid = 'host-uid-1';

beforeEach(async () => {
const mockHostService = { findHostInternal: jest.fn() };
const mockCmsClient = { postAuthenticated: jest.fn() };

const module: TestingModule = await Test.createTestingModule({
providers: [BrokerService],
providers: [
BrokerService,
{ provide: HostService, useValue: mockHostService },
{ provide: CmsHttpsClientService, useValue: mockCmsClient },
],
}).compile();

service = module.get<BrokerService>(BrokerService);
service = module.get(BrokerService);
hostService = module.get(HostService);
cmsClient = module.get(CmsHttpsClientService);

hostService.findHostInternal.mockResolvedValue(mockHost);
(common.checkCmsTokenError as jest.Mock).mockImplementation(() => {});
(common.checkCmsStatusError as jest.Mock).mockImplementation(() => {});
});

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

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('startAllBrokers', () => {
it('should send startbroker task and return { success: true }', async () => {
const mockResponse = {
__EXEC_TIME: '72 ms',
note: 'none',
status: 'success',
task: 'startbroker',
};
cmsClient.postAuthenticated.mockResolvedValue(mockResponse);

const result = await service.startAllBrokers(mockUserId, mockHostUid);

expect(hostService.findHostInternal).toHaveBeenCalledWith(
mockUserId,
mockHostUid
);
expect(cmsClient.postAuthenticated).toHaveBeenCalledWith(
`https://${mockHost.address}:${mockHost.port}/cm_api`,
expect.objectContaining({
task: 'startbroker',
token: mockHost.token,
})
);
expect(result).toEqual({ success: true });
});

it('should throw BrokerError when CMS status is not success', async () => {
cmsClient.postAuthenticated.mockResolvedValue({
__EXEC_TIME: '0 ms',
note: 'failed',
status: 'fail',
task: 'startbroker',
});

await expect(
service.startAllBrokers(mockUserId, mockHostUid)
).rejects.toThrow(BrokerError);
});
});

describe('stopAllBrokers', () => {
it('should send stopbroker task and return { success: true }', async () => {
const mockResponse = {
__EXEC_TIME: '72 ms',
note: 'none',
status: 'success',
task: 'stopbroker',
};
cmsClient.postAuthenticated.mockResolvedValue(mockResponse);

const result = await service.stopAllBrokers(mockUserId, mockHostUid);

expect(cmsClient.postAuthenticated).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
task: 'stopbroker',
token: mockHost.token,
})
);
expect(result).toEqual({ success: true });
});

it('should throw BrokerError when CMS status is not success', async () => {
cmsClient.postAuthenticated.mockResolvedValue({
__EXEC_TIME: '0 ms',
note: 'failed',
status: 'fail',
task: 'stopbroker',
});

await expect(
service.stopAllBrokers(mockUserId, mockHostUid)
).rejects.toThrow(BrokerError);
});
});
});
Loading
Loading