Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"core": "npx -w @filecoin-plus/core",
"format:check": "prettier --check 'packages/**/*.{js,ts,json}'",
"format:fix": "prettier --write 'packages/**/*.{js,ts,json}'",
"test:unit": "cd packages/application && npm run test:unit run"
"test:unit": "cd packages/application && npm run test:unit run",
"test:e2e": "cd packages/application && npm run test:e2e run"
},
"devDependencies": {
"prettier": "^3.3.3"
Expand Down
39 changes: 27 additions & 12 deletions packages/application/src/api/http/controllers/refresh.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
httpGet,
httpPost,
httpPut,
PARAMETER_TYPE,
params,
request,
requestBody,
requestParam,
Expand All @@ -30,6 +32,8 @@ import { SignatureType } from '@src/patterns/decorators/signature-guard.decorato
import { SignatureGuard } from '@src/patterns/decorators/signature-guard.decorator';
import { RejectRefreshCommand } from '@src/application/use-cases/refresh-issues/reject-refesh.command';
import { ApproveRefreshCommand } from '@src/application/use-cases/refresh-issues/approve-refresh.command';
import { GetRefreshesQueryDto } from '@src/application/dtos/GetRefreshesQueryDto';
import { SyncIssueCommand } from '@src/application/use-cases/refresh-issues/sync-issue.command';

const RES = RESPONSE_MESSAGES.REFRESH_CONTROLLER;

Expand All @@ -42,18 +46,16 @@ export class RefreshController {
@inject(TYPES.RoleService) private readonly _roleService: RoleService,
) {}

@httpGet('', ...validateRefreshesQuery)
async getAllRefreshes(@request() req: Request, @response() res: Response) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json(badRequest(RES.INVALID_QUERY, errors.array()));
}

const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 20;
const search = req.query.search as string | undefined;
@httpGet('', validateRequest(validateRefreshesQuery, RES.INVALID_QUERY))
async getAllRefreshes(
@params(PARAMETER_TYPE.QUERY) getRefreshesQueryDto: GetRefreshesQueryDto,
@response() res: Response,
) {
const { page, limit, search, status } = getRefreshesQueryDto;

const result = await this._queryBus.execute(new GetRefreshesQuery(page, limit, search));
const result = await this._queryBus.execute(
new GetRefreshesQuery(parseInt(page), parseInt(limit), search, status),
);

return res.json(ok(RES.GET_ALL, result));
}
Expand Down Expand Up @@ -83,6 +85,19 @@ export class RefreshController {
return res.json(ok(RES.UPSERTED_ISSUE));
}

@httpPost('/sync/issue/:githubIssueNumber')
async syncIssue(
@requestParam('githubIssueNumber') githubIssueNumber: string,
@response() res: Response,
) {
const result = await this._commandBus.send(new SyncIssueCommand(parseInt(githubIssueNumber)));

if (!result?.success) {
return res.status(400).json(badRequest(RES.FAILED_TO_UPSERT_ISSUE, [result.error.message]));
}
return res.json(ok(RES.UPSERTED_ISSUE));
}

@httpPost('/sync/issues')
async syncIssues(@response() res: Response) {
const result = await this._commandBus.send(new RefreshIssuesCommand());
Expand Down Expand Up @@ -113,7 +128,7 @@ export class RefreshController {

const command =
result === 'approve'
? new ApproveRefreshCommand(id, approveRefreshDto.details.finalDataCap)
? new ApproveRefreshCommand(id, parseInt(approveRefreshDto.details.finalDataCap))
: new RejectRefreshCommand(id);

const refreshResult = await this._commandBus.send(command);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { Request, Response, NextFunction } from 'express';
import { validationResult, ValidationChain } from 'express-validator';
import { validationResult, ValidationChain, ValidationError } from 'express-validator';
import { badRequest } from '@src/api/http/processors/response';

export function validateRequest(validators: ValidationChain[]) {
export function validateRequest(validators: ValidationChain[], responseMessage?: string) {
return async (req: Request, res: Response, next: NextFunction) => {
await Promise.all(validators.map(validator => validator.run(req)));

const errors = validationResult(req);

if (!errors.isEmpty()) {
const errorMessages = errors.array().map(error => error.msg);
return res.status(400).json(badRequest('Validation failed', errorMessages));
return res
.status(400)
.json(badRequest(responseMessage ?? 'Validation failed', errorMessages));
}

next();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { body, query } from 'express-validator';
import { VALIDATION_MESSAGES } from '@src/constants/validation-messages';
import { RefreshStatus } from '@src/infrastructure/repositories/issue-details';

export const validateIssueUpsert = [
body('issue')
Expand Down Expand Up @@ -45,7 +46,15 @@ export const validateIssueUpsert = [
];

export const validateRefreshesQuery = [
query('page').optional().isInt({ min: 1 }),
query('limit').optional().isInt({ min: 1, max: 100 }),
query('search').optional().isString(),
query('page').optional().isInt({ min: 1 }).withMessage(VALIDATION_MESSAGES.QUERY.PAGE.INVALID),
query('limit')
.optional()
.isInt({ min: 1, max: 100 })
.withMessage(VALIDATION_MESSAGES.QUERY.LIMIT.INVALID),
query('search').optional().isString().withMessage(VALIDATION_MESSAGES.QUERY.SEARCH.INVALID),
query('status').optional().isArray().withMessage(VALIDATION_MESSAGES.QUERY.STATUS.INVALID),
query('status.*')
.optional()
.isIn(Object.values(RefreshStatus))
.withMessage(VALIDATION_MESSAGES.QUERY.STATUS.INVALID_VALUE),
];
3 changes: 2 additions & 1 deletion packages/application/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ async function main() {
});

if (!lotusResponse.ok) {
logger.error('Lotus RPC request failed in batch', {
logger.error('Lotus RPC request failed in batch');
logger.error({
status: lotusResponse.status,
statusText: lotusResponse.statusText,
method,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { RefreshStatus } from '@src/infrastructure/repositories/issue-details';

export interface GetRefreshesQueryDto {
page: string;
limit: string;
search?: string;
status?: RefreshStatus[];
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export interface GovernanceReviewDetailsDto {
reviewerAddress: string;
reviewerPublicKey: string;
finalDataCap: number;
finalDataCap: string;
allocatorType: string;
}

Expand Down
4 changes: 4 additions & 0 deletions packages/application/src/application/dtos/SyncIssueDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface SyncIssueDto {
githubIssueId: string;
jsonNumber: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('RefreshAuditPublisher', () => {
datacap_amount: a.datacapAmount as number,
})),
};
const loggerMock = { info: vi.fn(), error: vi.fn() };
const loggerMock = { info: vi.fn(), error: vi.fn(), debug: vi.fn() };

const baseAllocator = {
application_number: 123,
Expand Down Expand Up @@ -115,13 +115,36 @@ describe('RefreshAuditPublisher', () => {
);

it('updateAudit applies partial changes to latest audit and publishes PR', async () => {
const change = { ended: '2024-01-02T00:00:00.000Z', outcome: AuditOutcome.APPROVED } as any;
const change = { ended: '2024-01-02T00:00:00.000Z', outcome: AuditOutcome.APPROVED };

const result = await publisher.updateAudit('hash123', change);

expect(auditMapperMock.partialFromAuditDataToDomain).toHaveBeenCalledWith(change);
expect(githubMock.createPullRequest).toHaveBeenCalled();
expect(result.prNumber).toBe(10);
expect(result.auditChange).toEqual(change);
expect(loggerMock.info).toHaveBeenCalledWith(
`[RefreshAuditPublisher]: Audit updated jsonHash: hash123 outcome: APPROVED`,
);
expect(loggerMock.debug).toHaveBeenCalledWith(`Allocator:`);
expect(loggerMock.debug).toHaveBeenCalledWith(baseAllocator);
expect(loggerMock.debug).toHaveBeenCalledWith(`Current audit:`);
expect(loggerMock.debug).toHaveBeenCalledWith(
baseAllocator.audits[baseAllocator.audits.length - 1],
);
expect(loggerMock.debug).toHaveBeenCalledWith(`Incoming audit update:`);
expect(loggerMock.debug).toHaveBeenCalledWith(change);
expect(loggerMock.debug).toHaveBeenCalledWith(`Pull request:`);
expect(loggerMock.debug).toHaveBeenCalledWith(result.prNumber);
expect(loggerMock.debug).toHaveBeenCalledWith(`Branch deleted:`);
expect(loggerMock.debug).toHaveBeenCalledWith(result.branchName);
});

it('should preserve fields with empty string values', async () => {
const change = { ended: '2024-01-02T00:00:00.000Z', outcome: AuditOutcome.APPROVED } as any;

const result = await publisher.updateAudit('hash123', change);

expect(result.auditChange).toEqual(change);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ export class RefreshAuditPublisher implements IRefreshAuditPublisher {
}> {
const { allocator } = await this.getAllocatorJsonDetails(jsonHash);

this._logger.debug(`Allocator:`);
this._logger.debug(allocator);
this._logger.debug(`Current audit:`);
this._logger.debug(allocator.audits[allocator.audits.length - 1]);

if (
expectedPreviousAuditOutcome &&
!expectedPreviousAuditOutcome.includes(allocator.audits.at(-1)?.outcome as AuditOutcome)
Expand All @@ -99,6 +104,8 @@ export class RefreshAuditPublisher implements IRefreshAuditPublisher {
);

const auditDataToUpdate = typeof auditData === 'function' ? auditData(allocator!) : auditData;
this._logger.debug(`Incoming audit update:`);
this._logger.debug(auditDataToUpdate);

Object.entries(this._auditMapper.partialFromAuditDataToDomain(auditDataToUpdate)).forEach(
([key, value]) => {
Expand Down Expand Up @@ -162,19 +169,28 @@ export class RefreshAuditPublisher implements IRefreshAuditPublisher {
files,
);

this._logger.debug(`Pull request:`);
this._logger.debug(pr);

await this._github.mergePullRequest(
this._allocatorRegistryConfig.owner,
this._allocatorRegistryConfig.repo,
pr.number,
`Refresh Audit ${jsonHash} ${allocator.audits.length} - ${allocator.application_number}`,
);

this._logger.debug(`Pull request merged:`);
this._logger.debug(pr.number);

await this._github.deleteBranch(
this._allocatorRegistryConfig.owner,
this._allocatorRegistryConfig.repo,
branchName,
);

this._logger.debug(`Branch deleted:`);
this._logger.debug(branchName);

return {
branchName,
commitSha: pr.head.sha,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Container } from 'inversify';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { GetRefreshesQuery, GetRefreshesQueryHandler } from './get-refreshes.query';
import { RefreshStatus } from '@src/infrastructure/repositories/issue-details';
import { TYPES } from '@src/types';
import { IIssueDetailsRepository } from '@src/infrastructure/repositories/issue-details.repository';

describe('GetRefreshesQuery', () => {
let container: Container;
let handler: GetRefreshesQueryHandler;

const repositoryMock = {
getPaginated: vi.fn(),
};

beforeEach(() => {
container = new Container();
container.bind<GetRefreshesQueryHandler>(GetRefreshesQueryHandler).toSelf();
container
.bind<IIssueDetailsRepository>(TYPES.IssueDetailsRepository)
.toConstantValue(repositoryMock as unknown as IIssueDetailsRepository);
handler = container.get(GetRefreshesQueryHandler);

repositoryMock.getPaginated.mockResolvedValue('fixtureRepositoryResponse');
});

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

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

it('should execute the query', async () => {
const query = new GetRefreshesQuery(1, 10, 'test', [RefreshStatus.PENDING]);
const result = await handler.execute(query);

expect(result).toBe('fixtureRepositoryResponse');
expect(repositoryMock.getPaginated).toHaveBeenCalledWith({
page: 1,
limit: 10,
search: 'test',
filters: {
refreshStatus: [RefreshStatus.PENDING],
},
});
});

it('should throw an error when the repository throws an error', async () => {
const error = new Error('test error');
repositoryMock.getPaginated.mockRejectedValue(error);

const query = new GetRefreshesQuery(1, 10, 'test', [RefreshStatus.PENDING]);
await expect(handler.execute(query)).rejects.toThrow(error);

expect(repositoryMock.getPaginated).toHaveBeenCalledWith({
page: 1,
limit: 10,
search: 'test',
filters: {
refreshStatus: [RefreshStatus.PENDING],
},
});
});

it('should be called without optional parameters', async () => {
const query = new GetRefreshesQuery();
const result = await handler.execute(query);

expect(result).toBe('fixtureRepositoryResponse');
expect(repositoryMock.getPaginated).toHaveBeenCalledWith({
page: 1,
limit: 10,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { inject, injectable } from 'inversify';

import { TYPES } from '@src/types';
import { IIssueDetailsRepository } from '@src/infrastructure/repositories/issue-details.repository';
import { RefreshStatus } from '@src/infrastructure/repositories/issue-details';

export class GetRefreshesQuery implements IQuery {
constructor(
public readonly page: number = 1,
public readonly limit: number = 10,
public readonly search?: string,
public readonly status?: RefreshStatus[],
) {}
}

Expand All @@ -21,6 +23,15 @@ export class GetRefreshesQueryHandler implements IQueryHandler<GetRefreshesQuery
) {}

async execute(query: GetRefreshesQuery) {
return this._repository.getPaginated(query.page, query.limit, query.search);
return this._repository.getPaginated({
page: query.page,
limit: query.limit,
search: query.search,
...(query.status && {
filters: {
refreshStatus: query.status,
},
}),
});
}
}
Loading