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
39 changes: 39 additions & 0 deletions .github/CI-CD-PIPELINE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
### Understanding the Unit Test Workflow

This document explains the workflow for running unit tests and checking code coverage using GitHub Actions. It is based on the ci.yml file you have. This workflow automates the process of testing your code every time a change is pushed or a pull request is made, helping to ensure the stability and quality of your application.

### Workflow Components

The workflow is broken down into several key steps:

#### 1\. Triggers

The on section defines when the workflow should run. In this case, it's triggered by two events:

- A push to the main branch.
- A pull_request targeting the main branch.

This ensures that all new code is automatically tested before it can be merged into the main codebase.

#### 2\. Jobs

The jobs section contains one job named test-and-coverage. A job is a set of steps that are executed on a single virtual machine.

- runs-on: ubuntu-latest: This specifies that the job will run on the latest version of an Ubuntu Linux virtual machine provided by GitHub.

#### 3\. Steps

The steps are the individual tasks that the job performs in sequence:

1. **Checkout repository**: The actions/checkout@v4 action downloads your repository's code into the virtual machine, so the workflow can access your project files.
2. **Setup Node.js**: This step uses the actions/setup-node@v4 action to install a specific version of Node.js (20 in this case), which is required to run your npm scripts.
3. **Install dependencies**: The npm ci command installs your project's dependencies. It's similar to npm install but is optimized for clean, automated environments like a CI server.
4. **Run unit tests with coverage**: The npm run test:cov command executes the test:cov script defined in your package.json. This script, in turn, runs Jest with the --coverage flag, generating a code coverage report.
5. **Check code coverage**: This crucial step uses a third-party action, artiomtr/jest-coverage-report-action@v2. It reads the coverage report generated in the previous step and compares the total coverage percentage against the specified threshold (in this case, 5). If the coverage falls below this threshold, the workflow will fail, providing a clear signal that the code quality has dropped.

### Benefits of this Workflow

- **Automation**: It eliminates the need for manual testing and reporting.
- **Quality Gates**: By enforcing a minimum coverage threshold, it acts as a quality gate, preventing code that is not adequately tested from being merged.
- **Rapid Feedback**: Developers receive immediate feedback on their code, allowing them to catch bugs and issues early in the development cycle.
- **Consistency**: The same testing process is applied to all code, ensuring a consistent level of quality across the entire codebase.
44 changes: 44 additions & 0 deletions .github/workflows/unit-test-ci-cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# GitHub Actions workflow to run tests and check code coverage
name: 🧪 CI with Jest Coverage Check

# Triggers the workflow on push or pull request events to the main branch
on:
push:
branches: [main]
pull_request:
branches: [main]

# A job is a set of steps that execute on the same runner.
jobs:
test-and-coverage:
# Specifies the runner environment to use (e.g., Ubuntu Linux)
runs-on: ubuntu-latest

# Defines the steps for this job
steps:
- name: Checkout repository
# Action to checkout your repository code
uses: actions/checkout@v4

- name: Setup Node.js
# Action to set up Node.js with a specific version
uses: actions/setup-node@v4
with:
node-version: '20' # Or your desired Node.js version

- name: Install dependencies
# Runs the npm install command to install project dependencies
run: npm ci

- name: Run unit tests with coverage
# Runs the Jest test command with the --coverage flag
run: npm run test:cov

# - name: Check code coverage
# # Uses the Jest Coverage Check action to enforce a minimum coverage threshold
# # This action will fail the build if the coverage percentage is below the threshold
# uses: artiomtr/jest-coverage-report-action@v2
# with:
# # Sets the minimum coverage threshold to 5%
# # You can adjust this value as your project grows
# threshold: 5
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Controller, Get } from '@nestjs/common';
import { PrismaService } from 'src/shared/database/prisma.service';
import { AppLoggerService } from 'src/shared/logger/app-logger.service';
import { ApiRoutes } from 'src/shared/router/routes';
import { PrismaService } from '../../../../shared/database/prisma.service';
import { AppLoggerService } from '../../../../shared/logger/app-logger.service';
import { ApiRoutes } from '../../../../shared/router/routes';

@Controller(ApiRoutes.HEALTH)
export class HealthController {
Expand Down
14 changes: 13 additions & 1 deletion src/modules/transaction/api/transaction.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
HttpStatus,
Param,
ParseIntPipe,
Patch,
Post,
ValidationPipe,
} from '@nestjs/common';
Expand All @@ -14,6 +15,7 @@ import { CreateTransactionDto } from '../../../shared/dto/create-transaction.dto
import { AppLoggerService } from '../../../shared/logger/app-logger.service';
import { ApiRoutes, TransactionRoutes } from '../../../shared/router/routes';
import { jsonStringifyReplacer } from '../../../shared/utils/json.utils';
import { CancelTransactionUseCase } from '../app/cancel-transaction-use-case/cancel-transaction.use-case';
import { CompleteTransactionUseCase } from '../app/complete-transaction-use-case/complete-transaction.use-case';
import { CreateTransactionUseCase } from '../app/create-transaction-use-case/create-transaction.use-case';
import { CreateTransactionInput } from '../app/input';
Expand All @@ -36,6 +38,7 @@ export class TransactionController {
private readonly createTransactionUseCase: CreateTransactionUseCase,
private readonly completeTransactionUseCase: CompleteTransactionUseCase,
private readonly transactionRepresentationMapper: TransactionRepresentationMapper,
private readonly cancelTransactionUseCase: CancelTransactionUseCase,
) {}

@Post()
Expand Down Expand Up @@ -87,7 +90,7 @@ export class TransactionController {
};
}

@Post(TransactionRoutes.COMPLETE)
@Patch(TransactionRoutes.COMPLETE)
@HttpCode(HttpStatus.OK)
async completeTransactions(
@Param('walletId', ParseIntPipe) walletId: number,
Expand All @@ -101,4 +104,13 @@ export class TransactionController {
this.transactionRepresentationMapper.getOutput(results);
return { elements: representation.length, data: representation };
}

@Patch(TransactionRoutes.CANCEL)
@HttpCode(HttpStatus.OK)
async cancel(
@Param('transactionId', ParseIntPipe) transactionId: number,
): Promise<TransactionRepresentation> {
const result = await this.cancelTransactionUseCase.run(transactionId);
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { AppLoggerService } from '../../../../shared/logger/app-logger.service';
import { TransactionStatusEnum } from '../../../../shared/validations/transaction/status';
import { TransactionRepresentation } from '../../api/representation';
import { TransactionRepresentationMapper } from '../../api/representationMapper';
import { TransactionService } from '../services/transaction.service';

@Injectable()
export class CancelTransactionUseCase {
private get logPrefix(): string {
return `[${this.constructor.name}] - `;
}

constructor(
private readonly logger: AppLoggerService,
private readonly transactionService: TransactionService,
private readonly mapper: TransactionRepresentationMapper,
) {}

public async run(transactionId: number): Promise<TransactionRepresentation> {
const transaction = await this.transactionService.getById(transactionId);
if (transaction.status === TransactionStatusEnum.CANCELLED) {
const msg = `Transaction: ${transactionId} already with status: ${transaction.status}. Cannot update.`;
this.logger.warn(this.logPrefix, msg);
throw new BadRequestException(msg);
}
const cancelledTransaction = await this.transactionService.update({
transactionId,
status: TransactionStatusEnum.CANCELLED,
});
this.logger.log(
this.logPrefix,
`Transaction: ${cancelledTransaction.id} updated to status: ${cancelledTransaction.status} successfully.`,
);
const result = this.mapper.getTransaction(cancelledTransaction);
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ export class CreateTransactionUseCase {
const transactionInput = this.buildTransactionInput(wallet, input);
const transaction = await this.transactionService.create(transactionInput);

if (!wallet) {
this.throwError(input);
}
if (
transaction.status === TransactionStatusEnum.COMPLETED &&
transaction.type === TransactionTypeEnum.EXCHANGE
Expand Down Expand Up @@ -89,6 +86,14 @@ export class CreateTransactionUseCase {
wallet: Wallet,
input: CreateTransactionInput,
): TransactionInput {
if (
wallet.currency === input.targetCurrency &&
Number(input.amount) === 0
) {
const err = `Requested to change created transaction for: ${Number(input.amount)} for currency: ${input.targetCurrency} which is the same as ${wallet.currency}. No action needed.`;
this.logger.error(this.logPrefix, err);
throw new BadRequestException(err);
}
const type = this.getTransactionType(
wallet.currency,
input.amount,
Expand All @@ -100,7 +105,6 @@ export class CreateTransactionUseCase {
this.logger.error(this.logPrefix, err);
throw new BadRequestException(err);
}

const transactionInput: TransactionInput = {
walletId: wallet.id,
type: type,
Expand Down Expand Up @@ -135,7 +139,7 @@ export class CreateTransactionUseCase {
}

private throwError(input: CreateTransactionInput): void {
throw new Error(
throw new BadRequestException(
`No wallet found for input: ${JSON.stringify(input, jsonStringifyReplacer)}`,
);
}
Expand Down
18 changes: 17 additions & 1 deletion src/modules/transaction/app/services/transaction.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,25 @@ export class TransactionService {
walletId: number,
transactionStatus?: TransactionStatusEnum,
): Promise<Transaction[]> {
return await this.transactionRepo.getByWalletId(
const results = await this.transactionRepo.getByWalletId(
walletId,
transactionStatus,
);
if (results.length === 0) {
const err = `No transaction found for wallet: ${walletId}`;
this.logger.log(this.logPrefix, err);
throw new BadRequestException(err);
}
return results;
}

public async getById(id: number): Promise<Transaction> {
const transaction = await this.transactionRepo.getById(id);
if (!transaction) {
const err = `No transaction found for ID: ${id}`;
this.logger.error(this.logPrefix, err);
throw new BadRequestException(err);
}
return transaction;
}
}
3 changes: 3 additions & 0 deletions src/modules/transaction/transaction.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { LoggerModule } from '../../shared/logger/logger.module';
import { WalletModule } from '../wallet/wallet.module';
import { TransactionRepresentationMapper } from './api/representationMapper';
import { TransactionController } from './api/transaction.controller';
import { CancelTransactionUseCase } from './app/cancel-transaction-use-case/cancel-transaction.use-case';
import { CompleteTransactionUseCase } from './app/complete-transaction-use-case/complete-transaction.use-case';
import { CreateTransactionUseCase } from './app/create-transaction-use-case/create-transaction.use-case';
import { TransactionService } from './app/services/transaction.service';
Expand All @@ -16,6 +17,7 @@ import { TransactionRepoImpl } from './infra/repo/transaction.postgres.repo.impl
TransactionService,
CreateTransactionUseCase,
CompleteTransactionUseCase,
CancelTransactionUseCase,
TransactionRepresentationMapper,
{
provide: TRANSACTION_REPOSITORY_TOKEN,
Expand All @@ -27,6 +29,7 @@ import { TransactionRepoImpl } from './infra/repo/transaction.postgres.repo.impl
CreateTransactionUseCase,
CompleteTransactionUseCase,
TransactionRepresentationMapper,
CancelTransactionUseCase,
],
})
export class TransactionModule {}
4 changes: 2 additions & 2 deletions src/modules/wallet/app/services/tokenization.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as crypto from 'crypto';
import { EnvVariables } from 'src/shared/config/envEnums';
import { AppLoggerService } from 'src/shared/logger/app-logger.service';
import { EnvVariables } from '../../../../shared/config/envEnums';
import { AppLoggerService } from '../../../../shared/logger/app-logger.service';

@Injectable()
export class TokenizationService {
Expand Down
1 change: 1 addition & 0 deletions src/shared/router/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export enum WalletRoutes {

export enum TransactionRoutes {
COMPLETE = 'complete/:walletId',
CANCEL = 'cancel/:transactionId',
}
27 changes: 0 additions & 27 deletions test/app.e2e-spec.ts

This file was deleted.

22 changes: 0 additions & 22 deletions test/unit/app.controller.spec.ts

This file was deleted.

Loading