Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: etching api tests #36

Merged
merged 8 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
23 changes: 22 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,27 @@
"TS_NODE_SKIP_IGNORE": "true"
},
"killBehavior": "polite"
}
},
{
"type": "node",
"request": "launch",
"name": "Jest",
"program": "${workspaceFolder}/api/node_modules/jest/bin/jest",
"cwd": "${workspaceFolder}/api/",
"args": [
"--testTimeout=3600000",
"--runInBand",
"--no-cache",
],
"outputCapture": "std",
"console": "integratedTerminal",
"preLaunchTask": "npm: testenv:run",
"postDebugTask": "npm: testenv:stop",
"env": {
"PGHOST": "localhost",
"PGUSER": "postgres",
"PGPASSWORD": "postgres",
},
},
]
}
43 changes: 43 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "npm: testenv:run",
"type": "shell",
"command": "npm run testenv:run -- -d",
"isBackground": true,
"options": {
"cwd": "${workspaceFolder}/api/",
},
"problemMatcher": {
"pattern": {
"regexp": ".",
"file": 1,
"location": 2,
"message": 3
},
"background": {
"activeOnStart": true,
"beginsPattern": ".",
"endsPattern": "."
}
}
},
{
"label": "npm: testenv:stop",
"type": "shell",
"command": "npm run testenv:stop",
"options": {
"cwd": "${workspaceFolder}/api/",
},
"presentation": {
"echo": true,
"reveal": "silent",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
}
}
]
}
5 changes: 4 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
"generate:vercel": "npm run generate:git-info && npm run generate:openapi && npm run generate:docs",
"lint:eslint": "eslint . --ext .ts,.tsx -f unix",
"lint:prettier": "prettier --check src/**/*.ts tests/**/*.ts",
"lint:unused-exports": "ts-unused-exports tsconfig.json --showLineNumber --excludePathsFromReport=util/*"
"lint:unused-exports": "ts-unused-exports tsconfig.json --showLineNumber --excludePathsFromReport=util/*",
"testenv:run": "docker-compose -f ../docker/docker-compose.dev.postgres.yml up",
"testenv:stop": "docker-compose -f ../docker/docker-compose.dev.postgres.yml down -v -t 0",
"testenv:logs": "docker-compose -f ../docker/docker-compose.dev.postgres.yml logs -t -f"
},
"author": "Hiro Systems PBC <[email protected]> (https://hiro.so)",
"license": "Apache 2.0",
Expand Down
5 changes: 4 additions & 1 deletion api/src/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ const RuneNumberSchema = Type.RegEx(/^[0-9]+$/, { title: 'Rune number' });
export const RuneNumberSchemaCType = TypeCompiler.Compile(RuneNumberSchema);
const RuneNameSchema = Type.RegEx(/^[A-Z]+$/, { title: 'Rune name' });
export const RuneNameSchemaCType = TypeCompiler.Compile(RuneNameSchema);
const RuneSpacedNameSchema = Type.RegEx(/^[A-Z](•[A-Z]+)+$/, { title: 'Rune name with spacers' });
const RuneSpacedNameSchema = Type.RegEx(/^[A-Za-z]+(•[A-Za-z]+)+$/, {
title: 'Rune name with spacers',
});
export const RuneSpacedNameSchemaCType = TypeCompiler.Compile(RuneSpacedNameSchema);

export const RuneSchema = Type.Union([
Expand Down Expand Up @@ -307,6 +309,7 @@ const RuneDetailResponseSchema = Type.Object({
{
id: RuneIdResponseSchema,
name: RuneNameResponseSchema,
// number: RuneNumberResponseSchema,
tippenein marked this conversation as resolved.
Show resolved Hide resolved
spaced_name: RuneSpacedNameResponseSchema,
},
{ title: 'Rune detail', description: 'Details of the rune affected by this activity' }
Expand Down
2 changes: 1 addition & 1 deletion api/src/pg/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

export type DbCountedQueryResult<T> = T & { total: number };

type DbRune = {
export type DbRune = {

Check warning on line 8 in api/src/pg/types.ts

View check run for this annotation

Codecov / codecov/patch

api/src/pg/types.ts#L8

Added line #L8 was not covered by tests
id: string;
number: number;
name: string;
Expand Down
76 changes: 74 additions & 2 deletions api/tests/api/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,75 @@
test('sample', () => {
expect(true);
import { ENV } from '../../src/env';
import { PgStore } from '../../src/pg/pg-store';
import { DbLedgerEntry } from '../../src/pg/types';
import {
dropDatabase,
insertDbEntry,
insertRune,
sampleRune,
runMigrations,
startTestApiServer,
TestFastifyServer,
insertSupply,
sampleLedgerEntry,
} from '../helpers';

describe('Etchings', () => {
let db: PgStore;
let fastify: TestFastifyServer;

const rune = sampleRune('1:1', 'Sample Rune');
const ledgerEntry = sampleLedgerEntry(rune.id);

beforeEach(async () => {
ENV.PGDATABASE = 'postgres';
db = await PgStore.connect();
fastify = await startTestApiServer(db);
await runMigrations(db);
await insertRune(db, rune);
const event_index = 0;
await insertDbEntry(db, ledgerEntry, event_index);
await insertSupply(db, rune.id, 1);
});

afterEach(async () => {
if (fastify) {
await fastify.close();
}

await dropDatabase(db);
await db.close();
});

test('lists runes', async () => {
const runesResponse = await fastify.inject({
method: 'GET',
url: '/runes/v1/etchings',
});
expect(runesResponse.statusCode).toBe(200);
expect(runesResponse.json().results).not.toHaveLength(0);

const response = await fastify.inject({
method: 'GET',
url: '/runes/v1/etchings/' + ledgerEntry.rune_id,
});
expect(response.statusCode).toBe(200);
expect(response.json().name).toEqual(rune.name);
});
tippenein marked this conversation as resolved.
Show resolved Hide resolved
test('can fetch by spaced name', async () => {
const url = '/runes/v1/etchings/' + rune.spaced_name;
const response = await fastify.inject({
method: 'GET',
url: url,
});
expect(response.statusCode).toBe(200);
expect(response.json().spaced_name).toEqual(rune.spaced_name);
});
test('can not fetch by spaced name if lacking bullets', async () => {
const url = '/runes/v1/etchings/' + rune.spaced_name.replaceAll('•', '-');
tippenein marked this conversation as resolved.
Show resolved Hide resolved
const response = await fastify.inject({
method: 'GET',
url: url,
});
expect(response.statusCode).toBe(400);
});
});
200 changes: 200 additions & 0 deletions api/tests/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { readdirSync } from 'fs';
import { PgStore } from '../src/pg/pg-store';
import { FastifyBaseLogger, FastifyInstance } from 'fastify';
import { IncomingMessage, Server, ServerResponse } from 'http';
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { buildApiServer } from '../src/api/init';
import { Rune } from '../src/api/schemas';
import { DbLedgerEntry, DbRune } from '../src/pg/types';

export type TestFastifyServer = FastifyInstance<
Server,
IncomingMessage,
ServerResponse,
FastifyBaseLogger,
TypeBoxTypeProvider
>;

export async function startTestApiServer(db: PgStore): Promise<TestFastifyServer> {
return await buildApiServer({ db });
}

export async function runMigrations(db: PgStore) {
const contents = readdirSync('../migrations');
await db.sqlWriteTransaction(async sql => {
for (const fileName of contents) {
if (!fileName.endsWith('.sql')) continue;
await db.sql.file('../migrations/' + fileName);
}
});
}

export async function dropDatabase(db: PgStore) {
await db.sqlWriteTransaction(async sql => {
// Drop all tables.
await sql`
DO $$ DECLARE
r RECORD;
BEGIN
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = current_schema()) LOOP
EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
END LOOP;
END $$
`;
// Drop all types.
await sql`
DO $$ DECLARE
r RECORD;
BEGIN
FOR r IN (SELECT typname FROM pg_type WHERE typtype = 'e' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema())) LOOP
EXECUTE 'DROP TYPE IF EXISTS ' || quote_ident(r.typname) || ' CASCADE';
END LOOP;
END $$;
`;
});
}
export function sampleLedgerEntry(rune_id: string, block_height?: string): DbLedgerEntry {
tippenein marked this conversation as resolved.
Show resolved Hide resolved
return {
rune_id: '1:1',
block_hash: '0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5',
block_height: block_height || '840000',
tx_index: 0,
tx_id: '2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e',
output: 0,
address: '0',
receiver_address: '0',
amount: '0',
operation: 'etching',
timestamp: 0,
};
}

function toSpacedName(name: string | null): string | null {
if (name === null) {
return null;
}
// should take "Some name" and make it "Some•name"
const words = name.split(' ');
return words.join('•');
}
export function sampleRune(id: string, name?: string): DbRune {
return {
id: '1:1',
name: name || 'SAMPLE RUNE NAME',
tippenein marked this conversation as resolved.
Show resolved Hide resolved
spaced_name: (name && toSpacedName(name)) || 'SAMPLE•RUNE•NAME',
number: 1,
block_hash: '0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5',
block_height: '840000',
tx_index: 1,
tx_id: '2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e',
divisibility: 2,
premine: '1000',
symbol: 'ᚠ',
cenotaph: true,
terms_amount: '100',
terms_cap: '5000000',
terms_height_start: null,
terms_height_end: null,
terms_offset_start: null,
terms_offset_end: null,
turbo: false,
minted: '1000',
total_mints: '1500',
burned: '500',
total_burns: '750',
total_operations: '1',
timestamp: 1713571767,
};
}

export async function insertDbEntry(
tippenein marked this conversation as resolved.
Show resolved Hide resolved
db: PgStore,
payload: DbLedgerEntry,
event_index: number
): Promise<void> {
await db.sqlWriteTransaction(async sql => {
const {
rune_id,
block_hash,
block_height,
tx_index,
tx_id,
output,
address,
receiver_address,
amount,
operation,
} = payload;

await sql`
INSERT INTO ledger (
rune_id, block_hash, block_height, tx_index, tx_id, output,
address, receiver_address, amount, operation, timestamp, event_index
)
VALUES (

${rune_id}, ${block_hash}, ${block_height}, ${tx_index}, ${tx_id}, ${output}, ${address}, ${receiver_address}, ${amount}, ${operation}, 0, ${event_index}
)
`;
});
}

export async function insertSupply(
tippenein marked this conversation as resolved.
Show resolved Hide resolved
db: PgStore,
rune_id: string,
block_height: number,
minted?: number,
total_mints?: number,
total_operations?: number
): Promise<void> {
await db.sqlWriteTransaction(async sql => {
const burned = 0;
const total_burned = 0;

await sql`
INSERT INTO supply_changes (
rune_id, block_height, minted, total_mints, burned, total_burns, total_operations
)
VALUES (

${rune_id}, ${block_height}, ${minted || 0}, ${
total_mints || 0
}, ${burned}, ${total_burned}, ${total_operations || 0}
)
`;
});
}

export async function insertRune(db: PgStore, payload: DbRune): Promise<void> {
await db.sqlWriteTransaction(async sql => {
const {
id,
name,
spaced_name,
number,
block_hash,
block_height,
tx_index,
tx_id,
symbol,
cenotaph,
terms_amount,
terms_cap,
terms_height_start,
terms_height_end,
} = payload;

await sql`
INSERT INTO runes (
id, number, name, spaced_name, block_hash, block_height, tx_index, tx_id, symbol, cenotaph,
terms_amount, terms_cap, terms_height_start, terms_height_end, timestamp
)
VALUES (

${id}, ${number}, ${name}, ${spaced_name}, ${block_hash}, ${block_height}, ${tx_index}, ${tx_id}, ${symbol}, ${cenotaph}, ${
terms_amount || ''
}, ${terms_cap || ''}, ${terms_height_start}, ${terms_height_end}, 0
)
`;
});
}
Loading