Skip to content

Commit

Permalink
test(wallet): add functional tests for /blocks and /transactions
Browse files Browse the repository at this point in the history
refs #719
  • Loading branch information
jzsfkzm committed Feb 10, 2025
1 parent 64202f7 commit 3c11e0e
Show file tree
Hide file tree
Showing 12 changed files with 533 additions and 4 deletions.
2 changes: 1 addition & 1 deletion apps/api/src/services/db/transactionsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export async function getTransactions(limit: number) {
export async function getTransaction(hash: string): Promise<ApiTransactionResponse | null> {
const tx = await Transaction.findOne({
where: {
hash: hash
hash
},
include: [
{
Expand Down
144 changes: 144 additions & 0 deletions apps/api/test/functional/blocks.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { AkashBlock, AkashMessage } from "@akashnetwork/database/dbSchemas/akash";
import { Day, Transaction, Validator } from "@akashnetwork/database/dbSchemas/base";

import { app } from "@src/app";

import { BlockSeeder } from "@test/seeders/block.seeder";
import { MessageSeeder } from "@test/seeders/message.seeder";
import { TransactionSeeder } from "@test/seeders/transaction.seeder";
import { ValidatorSeeder } from "@test/seeders/validator.seeder";

jest.setTimeout(20000);

const getMaxHeight = async () => {
const height = await AkashBlock.max("height");

return (height as number) ?? 0;
};

describe("Blocks", () => {
describe("GET /v1/blocks", () => {
it("resolves list of most recent blocks", async () => {
const response = await app.request("/v1/blocks?limit=2", {
method: "GET",
headers: new Headers({ "Content-Type": "application/json" })
});

expect(response.status).toBe(200);
const blocks = await response.json();
expect(blocks.length).toBe(2);
blocks.forEach((block: unknown) => {
expect(block).toMatchObject({
height: expect.any(Number),
proposer: {
address: expect.any(String),
operatorAddress: expect.any(String),
moniker: expect.any(String),
avatarUrl: expect.toBeTypeOrNull(String)
},
transactionCount: expect.any(Number),
totalTransactionCount: expect.any(Number),
datetime: expect.dateTimeZ()
});
});
});

it("will not resolve more than 100 blocks", async () => {
const response = await app.request("/v1/blocks?limit=101", {
method: "GET",
headers: new Headers({ "Content-Type": "application/json" })
});

expect(response.status).toBe(200);
const blocks = await response.json();
expect(blocks.length).toBe(100);
});
});

describe("GET /v1/blocks/{height}", () => {
it("resolves block by height", async () => {
const maxHeight = await getMaxHeight();
const nextHeight = maxHeight + 1;

const day = await Day.findOne({ order: [["date", "DESC"]] });

const validator = ValidatorSeeder.create();
await Validator.create(validator);

const block = BlockSeeder.create({
height: nextHeight,
proposer: validator.hexAddress,
dayId: day.id
});
await AkashBlock.create(block);

const transaction = TransactionSeeder.create({
height: nextHeight,
hasProcessingError: false
});
await Transaction.create(transaction);

const message = MessageSeeder.create({
txId: transaction.id,
height: nextHeight,
amount: "1000"
});
await AkashMessage.create(message);

const response = await app.request(`/v1/blocks/${block.height}`, {
method: "GET",
headers: new Headers({ "Content-Type": "application/json" })
});

expect(response.status).toBe(200);
const blockLoaded = await response.json();
expect(blockLoaded).toEqual({
height: block.height,
datetime: block.datetime,
proposer: {
address: validator.accountAddress,
operatorAddress: validator.operatorAddress,
moniker: validator.moniker,
avatarUrl: validator.keybaseAvatarUrl
},
hash: blockLoaded.hash,
gasUsed: blockLoaded.gasUsed,
gasWanted: blockLoaded.gasWanted,
transactions: [
{
hash: transaction.hash,
isSuccess: true,
error: null,
fee: parseInt(transaction.fee),
datetime: block.datetime,
messages: [
{
id: message.id,
type: message.type,
amount: parseInt(message.amount)
}
]
}
]
});
});

it("responds 400 for invalid height", async () => {
const response = await app.request("/v1/blocks/a", {
method: "GET",
headers: new Headers({ "Content-Type": "application/json" })
});

expect(response.status).toBe(400);
});

it("responds 404 for a block not found", async () => {
const response = await app.request("/v1/blocks/0", {
method: "GET",
headers: new Headers({ "Content-Type": "application/json" })
});

expect(response.status).toBe(404);
});
});
});
153 changes: 153 additions & 0 deletions apps/api/test/functional/transactions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { AkashBlock, AkashMessage } from "@akashnetwork/database/dbSchemas/akash";
import { Day, Transaction, Validator } from "@akashnetwork/database/dbSchemas/base";
import { get } from "lodash";

import { app } from "@src/app";

import { BlockSeeder } from "@test/seeders/block.seeder";
import { MessageSeeder } from "@test/seeders/message.seeder";
import { TransactionSeeder } from "@test/seeders/transaction.seeder";
import { ValidatorSeeder } from "@test/seeders/validator.seeder";

jest.setTimeout(20000);

const getMaxHeight = async () => {
const height = await AkashBlock.max("height");

return (height as number) ?? 0;
};

describe("Transactions", () => {
describe("GET /v1/transactions", () => {
it("resolves list of most recent transactions", async () => {
const response = await app.request("/v1/transactions?limit=2", {
method: "GET",
headers: new Headers({ "Content-Type": "application/json" })
});

expect(response.status).toBe(200);
const transactions = await response.json();
expect(transactions.length).toBe(2);
transactions.forEach((transaction: unknown) => {
expect(transaction).toMatchObject({
height: expect.any(Number),
datetime: expect.any(String),
hash: expect.any(String),
isSuccess: expect.any(Boolean),
error: expect.toBeTypeOrNull(String),
gasUsed: expect.any(Number),
gasWanted: expect.any(Number),
fee: expect.any(Number),
memo: expect.any(String)
});

get(transaction, "messages", []).forEach((message: unknown) => {
expect(message).toMatchObject({
id: expect.any(String),
type: expect.any(String),
amount: expect.any(Number)
});
});
});
});

it("will not resolve more than 100 transactions", async () => {
const response = await app.request("/v1/transactions?limit=101", {
method: "GET",
headers: new Headers({ "Content-Type": "application/json" })
});

expect(response.status).toBe(200);
const blocks = await response.json();
expect(blocks.length).toBe(100);
});

it("responds 400 when limit is not set", async () => {
const response = await app.request("/v1/transactions", {
method: "GET",
headers: new Headers({ "Content-Type": "application/json" })
});

expect(response.status).toBe(400);
});
});

describe("GET /v1/transactions/{hash}", () => {
it("resolves trancation by hash", async () => {
const maxHeight = await getMaxHeight();
const nextHeight = maxHeight + 1;

const day = await Day.findOne({ order: [["date", "DESC"]] });

const validator = ValidatorSeeder.create();
await Validator.create(validator);

const block = BlockSeeder.create({
height: nextHeight,
proposer: validator.hexAddress,
dayId: day.id
});
await AkashBlock.create(block);

const transaction = TransactionSeeder.create({
height: nextHeight,
hasProcessingError: false
});
await Transaction.create(transaction);

const message = MessageSeeder.create({
txId: transaction.id,
height: nextHeight,
amount: "1000"
});
await AkashMessage.create(message);

const response = await app.request(`/v1/transactions/${transaction.hash}`, {
method: "GET",
headers: new Headers({ "Content-Type": "application/json" })
});

expect(response.status).toBe(200);
const transactionLoaded = await response.json();
expect(transactionLoaded).toEqual({
height: block.height,
datetime: block.datetime,
hash: transaction.hash,
isSuccess: !transaction.hasProcessingError,
multisigThreshold: null,
signers: [],
error: transaction.hasProcessingError ? transaction.log : null,
gasUsed: transaction.gasUsed,
gasWanted: transaction.gasWanted,
fee: parseInt(transaction.fee),
memo: transaction.memo,
messages: [
{
id: message.id,
type: message.type,
data: {
amount: [
{
amount: "10000",
denom: "uakt"
}
],
fromAddress: "akash10ml4dz5npgyhzx3xq0myl44dzycmkgytmc9rhe",
toAddress: "akash1gxglu3ny085vnwearp3kf6tvhqagadyawy05gq"
},
relatedDeploymentId: null
}
]
});
});

it("responds 404 for an unknown hash", async () => {
const response = await app.request("/v1/transactions/unknown-hash", {
method: "GET",
headers: new Headers({ "Content-Type": "application/json" })
});

expect(response.status).toBe(404);
});
});
});
43 changes: 43 additions & 0 deletions apps/api/test/seeders/block.seeder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { faker } from "@faker-js/faker";
import { merge } from "lodash";

import { AkashAddressSeeder } from "./akash-address.seeder";

type Block = {
height: number;
datetime: string;
hash: string;
proposer: string;
dayId: string;
txCount: number;
isProcessed: boolean;
totalUAktSpent?: number;
totalUUsdcSpent?: number;
totalUUsdSpent?: number;
activeLeaseCount?: number;
totalLeaseCount?: number;
activeCPU?: number;
activeGPU?: number;
activeMemory?: number;
activeEphemeralStorage?: number;
activePersistentStorage?: number;
activeProviderCount?: number;
};

export class BlockSeeder {
static create(input: Partial<Block> = {}): Block {
return merge(
{
height: faker.number.int({ min: 0, max: 10000000 }),
datetime: faker.date.past().toISOString(),
hash: AkashAddressSeeder.create(),
proposer: AkashAddressSeeder.create(),
dayId: faker.string.uuid(),
txCount: faker.number.int({ min: 0, max: 10000000 }),
isProcessed: faker.datatype.boolean(),
totalTxCount: faker.number.int({ min: 0, max: 10000000 })
},
input
);
}
}
27 changes: 27 additions & 0 deletions apps/api/test/seeders/day.seeder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { faker } from "@faker-js/faker";
import { merge } from "lodash";

type Day = {
id: string;
date: Date;
aktPrice?: number;
firstBlockHeight: number;
lastBlockHeight?: number;
lastBlockHeightYet: number;
aktPriceChanged: boolean;
};

export class DaySeeder {
static create(input: Partial<Day> = {}): Day {
return merge(
{
id: faker.string.uuid(),
date: faker.date.past(),
firstBlockHeight: faker.number.int({ min: 0, max: 10000000 }),
lastBlockHeightYet: faker.number.int({ min: 0, max: 10000000 }),
aktPriceChanged: faker.datatype.boolean()
},
input
);
}
}
Loading

0 comments on commit 3c11e0e

Please sign in to comment.