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
11 changes: 7 additions & 4 deletions apps/api/src/certificate/controllers/certificate.controller.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { singleton } from "tsyringe";

import { Protected } from "@src/auth/services/auth.service";
import { AuthService, Protected } from "@src/auth/services/auth.service";
import { CreateCertificateResponse } from "../http-schemas/create-certificate.schema";
import { CertificateService } from "../services/certificate.service";
import { CertificateService } from "../services/certificate/certificate.service";

@singleton()
export class CertificateController {
constructor(private readonly certificateService: CertificateService) {}
constructor(
private readonly certificateService: CertificateService,
private readonly authService: AuthService
) {}

@Protected([{ action: "sign", subject: "UserWallet" }])
async create(): Promise<CreateCertificateResponse> {
const cert = await this.certificateService.create();
const cert = await this.certificateService.create({ userId: this.authService.currentUser.id });
return { data: cert };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { CertificateManager, certificateManager } from "@akashnetwork/chain-sdk";
import { container } from "tsyringe";

container.register(CertificateManager, {
useValue: certificateManager
});

export { CertificateManager };
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type { CertificateManager, certificateManager } from "@akashnetwork/chain-sdk";
import type { MongoAbility } from "@casl/ability";
import { createMongoAbility } from "@casl/ability";
import { faker } from "@faker-js/faker";
import { mock } from "jest-mock-extended";

import type { AuthService } from "@src/auth/services/auth.service";
import type { UserWalletRepository } from "@src/billing/repositories";
import type { RpcMessageService } from "@src/billing/services";
import type { ManagedSignerService } from "@src/billing/services/managed-signer/managed-signer.service";
import { CertificateService } from "./certificate.service";

import { UserWalletSeeder } from "@test/seeders/user-wallet.seeder";

describe(CertificateService.name, () => {
describe("create", () => {
it("creates certificate successfully when wallet exists", async () => {
const userWallet = UserWalletSeeder.create();
const certificateData = {
cert: "-----BEGIN CERTIFICATE-----\nMOCK_CERT\n-----END CERTIFICATE-----",
publicKey: "-----BEGIN PUBLIC KEY-----\nMOCK_PUBLIC_KEY\n-----END PUBLIC KEY-----",
privateKey: "encrypted-private-key"
};

const createCertificateMsg = {
typeUrl: "/akash.cert.v1beta3.MsgCreateCertificate",
value: {}
};

const { service, userWalletRepository, authService, rpcMessageService, managedSignerService, certificateManager } = setup({
findWallet: jest.fn().mockResolvedValue(userWallet),
getCreateCertificateMsg: jest.fn().mockReturnValue(createCertificateMsg),
executeDecodedTxByUserId: jest.fn().mockResolvedValue({ code: 0, hash: "tx-hash", transactionHash: "tx-hash" })
});

const result = await service.create({ userId: userWallet.userId });

expect(userWalletRepository.accessibleBy).toHaveBeenCalledWith(authService.ability, "sign");
expect(userWalletRepository.findOneByUserId).toHaveBeenCalledWith(userWallet.userId);
expect(certificateManager.generatePEM).toHaveBeenCalledWith(userWallet.address);
expect(rpcMessageService.getCreateCertificateMsg).toHaveBeenCalledWith(userWallet.address, certificateData.cert, certificateData.publicKey);
expect(managedSignerService.executeDecodedTxByUserId).toHaveBeenCalledWith(userWallet.userId, [createCertificateMsg]);
expect(result).toEqual({
certPem: certificateData.cert,
pubkeyPem: certificateData.publicKey,
encryptedKey: certificateData.privateKey
});
});

it("throws 404 error when user wallet is not found", async () => {
const userId = faker.string.uuid();
const { service, userWalletRepository, authService } = setup({
findWallet: jest.fn().mockResolvedValue(null)
});

await expect(service.create({ userId })).rejects.toThrow("UserWallet not found");

expect(userWalletRepository.accessibleBy).toHaveBeenCalledWith(authService.ability, "sign");
expect(userWalletRepository.findOneByUserId).toHaveBeenCalledWith(userId);
});

it("throws 404 error when user wallet has no address", async () => {
const userWallet = UserWalletSeeder.create();
userWallet.address = null;

const { service, userWalletRepository, authService } = setup({
findWallet: jest.fn().mockResolvedValue(userWallet)
});

await expect(service.create({ userId: userWallet.userId })).rejects.toThrow("UserWallet not found");

expect(userWalletRepository.accessibleBy).toHaveBeenCalledWith(authService.ability, "sign");
expect(userWalletRepository.findOneByUserId).toHaveBeenCalledWith(userWallet.userId);
});
});

function setup(input?: {
findWallet?: UserWalletRepository["findOneByUserId"];
generateCert?: typeof certificateManager.generatePEM;
getCreateCertificateMsg?: RpcMessageService["getCreateCertificateMsg"];
executeDecodedTxByUserId?: ManagedSignerService["executeDecodedTxByUserId"];
}) {
const mocks = {
userWalletRepository: mock<UserWalletRepository>({
accessibleBy: jest.fn().mockReturnThis(),
findOneByUserId: input?.findWallet ?? jest.fn()
}),
authService: mock<AuthService>({
ability: createMongoAbility<MongoAbility>()
}),
rpcMessageService: mock<RpcMessageService>({
getCreateCertificateMsg: input?.getCreateCertificateMsg ?? jest.fn()
}),
managedSignerService: mock<ManagedSignerService>({
executeDecodedTxByUserId: input?.executeDecodedTxByUserId ?? jest.fn()
}),
certificateManager: mock<CertificateManager>({
generatePEM:
input?.generateCert ??
jest.fn(async () => ({
cert: "-----BEGIN CERTIFICATE-----\nMOCK_CERT\n-----END CERTIFICATE-----",
publicKey: "-----BEGIN PUBLIC KEY-----\nMOCK_PUBLIC_KEY\n-----END PUBLIC KEY-----",
privateKey: "encrypted-private-key"
}))
})
};

const service = new CertificateService(
mocks.userWalletRepository,
mocks.authService,
mocks.certificateManager,
mocks.rpcMessageService,
mocks.managedSignerService
);

return { service, ...mocks };
}
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { certificateManager } from "@akashnetwork/chain-sdk";
import assert from "http-assert";
import { singleton } from "tsyringe";

import { AuthService } from "@src/auth/services/auth.service";
import { UserWalletRepository } from "@src/billing/repositories";
import { UserWalletOutput, UserWalletRepository } from "@src/billing/repositories";
import { RpcMessageService } from "@src/billing/services";
import { ManagedSignerService } from "@src/billing/services/managed-signer/managed-signer.service";
import { CertificateManager } from "../../providers/certificate-manager.provider";

interface CertificateOutput {
certPem: string;
Expand All @@ -18,16 +18,16 @@ export class CertificateService {
constructor(
private readonly userWalletRepository: UserWalletRepository,
private readonly authService: AuthService,
private readonly certificateManager: CertificateManager,
private readonly rpcMessageService: RpcMessageService,
private readonly managedSignerService: ManagedSignerService
) {}

async create(): Promise<CertificateOutput> {
const userWallet = await this.userWalletRepository.accessibleBy(this.authService.ability, "sign").findOneByUserId(this.authService.currentUser.id);

async create(input: { userId: UserWalletOutput["userId"] }): Promise<CertificateOutput> {
const userWallet = await this.userWalletRepository.accessibleBy(this.authService.ability, "sign").findOneByUserId(input.userId);
assert(userWallet?.address, 404, "UserWallet not found");

const { cert: crtpem, publicKey: pubpem, privateKey: encryptedKey } = await certificateManager.generatePEM(userWallet.address);
const { cert: crtpem, publicKey: pubpem, privateKey: encryptedKey } = await this.certificateManager.generatePEM(userWallet.address);
const createCertificateMsg = this.rpcMessageService.getCreateCertificateMsg(userWallet.address, crtpem, pubpem);
const messages = [createCertificateMsg];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import "@test/setup-functional-tests";

import { container } from "tsyringe";

import { TransactionRepository } from "./transaction.repository";

import { createAkashAddress, createAkashBlock, createAkashMessage, createTransaction } from "@test/seeders";
import { createAddressReferenceInDatabase } from "@test/seeders/address-reference.seeder";

describe(TransactionRepository.name, () => {
describe("getTransactions", () => {
it("returns a list of transactions", async () => {
const { transactions, repository } = await setup();
const transactionsFound = await repository.getTransactions(10);

expect(transactions).toEqual(expect.arrayContaining(transactionsFound.map(tx => expect.objectContaining({ hash: tx.hash }))));
});

it("does not return more than 100 transactions", async () => {
const { transactions, repository } = await setup();
const transactionsFound = await repository.getTransactions(101);
expect(transactions.length).toBeGreaterThan(100);
expect(transactionsFound.length).toBe(100);
});
});

describe("getTransactionByHash", () => {
it("returns a transaction by hash", async () => {
const { transactions, repository } = await setup();
const txToFind = transactions[20];
const transactionFound = await repository.getTransactionByHash(txToFind.hash);
expect(transactionFound).toEqual(
expect.objectContaining({
height: txToFind.height,
hash: txToFind.hash,
isSuccess: !txToFind.hasProcessingError,
error: txToFind.hasProcessingError && txToFind.log ? txToFind.log : null,
gasUsed: txToFind.gasUsed,
gasWanted: txToFind.gasWanted,
fee: parseInt(txToFind.fee),
memo: txToFind.memo
})
);
});

it("returns null if the transaction is not found", async () => {
const { repository } = await setup();
const transactionFound = await repository.getTransactionByHash("unknown-hash");
expect(transactionFound).toBeNull();
});
});

describe("getTransactionsByAddress", () => {
it("returns a list of transactions by address", async () => {
const { transactions, repository } = await setup();
const address = createAkashAddress();
const transactionsWithAddressRef = transactions.slice(0, 10);
await Promise.all(
transactionsWithAddressRef.map(async trx => {
const message = await createAkashMessage({
txId: trx.id,
height: trx.height
});
return createAddressReferenceInDatabase({
transactionId: trx.id,
address,
type: "sender",
messageId: message.id
});
})
);
const transactionsFound = await repository.getTransactionsByAddress({ address, skip: 0, limit: 10 });
expect(transactionsFound.count).toBe(transactionsWithAddressRef.length);
expect(transactionsFound.results.map(trx => trx.hash).toSorted()).toEqual(transactionsWithAddressRef.map(trx => trx.hash).toSorted());
});

it("returns an empty list if the address has no transactions", async () => {
const { repository } = await setup();
const address = createAkashAddress();
const transactionsFound = await repository.getTransactionsByAddress({ address, skip: 0, limit: 5 });
expect(transactionsFound.count).toBe(0);
expect(transactionsFound.results).toEqual([]);
});
});

async function setup() {
const repository = container.resolve(TransactionRepository);
const block = await createAkashBlock();

const transactions = await Promise.all(
Array.from({ length: 101 }, (_, i) => {
return createTransaction({
height: block.height,
index: i + 1
});
})
);

return { transactions, repository };
}
});
108 changes: 0 additions & 108 deletions apps/api/test/functional/certificate.spec.ts

This file was deleted.

Loading
Loading