From 1b28233afcecaec290f431ef5c0185f42d05ff99 Mon Sep 17 00:00:00 2001 From: Joe Wagner Date: Wed, 31 May 2023 18:00:25 -0600 Subject: [PATCH 1/4] Abstract console calls into logger for test spies --- src/commands/chains.ts | 8 +- src/commands/controller.ts | 14 +-- src/commands/create.ts | 20 ++--- src/commands/info.ts | 7 +- src/commands/init.ts | 6 +- src/commands/list.ts | 9 +- src/commands/namespace.ts | 7 +- src/commands/read.ts | 15 ++-- src/commands/receipt.ts | 7 +- src/commands/schema.ts | 7 +- src/commands/shell.ts | 60 ++++++------- src/commands/transfer.ts | 5 +- src/commands/write.ts | 20 ++--- src/lib/EnsResolver.ts | 7 +- src/utils.ts | 14 +++ test/chains.test.ts | 31 ++++--- test/controller.test.ts | 61 ++++++------- test/create.test.ts | 169 +++++++++++++++++-------------------- test/info.test.ts | 29 ++++--- test/list.test.ts | 63 ++++++-------- test/namespace.test.ts | 36 +++----- test/read.test.ts | 156 +++++++++++++++------------------- test/receipt.test.ts | 85 ++++++++++--------- test/schema.test.ts | 38 +++++---- test/setup.ts | 11 --- test/shell.test.ts | 78 ++++++++--------- test/transfer.test.ts | 40 ++++----- test/write.test.ts | 129 ++++++++++++---------------- 28 files changed, 535 insertions(+), 597 deletions(-) diff --git a/src/commands/chains.ts b/src/commands/chains.ts index 25edbe6..efb8a92 100644 --- a/src/commands/chains.ts +++ b/src/commands/chains.ts @@ -1,6 +1,6 @@ import type { Arguments, CommandBuilder } from "yargs"; import { GlobalOptions } from "../cli.js"; -import { getChains } from "../utils.js"; +import { getChains, logger } from "../utils.js"; import type yargs from "yargs"; export interface Options extends GlobalOptions { @@ -24,11 +24,11 @@ export const handler = async (_argv: Arguments): Promise => { const { format } = _argv; if (format === "pretty") { - console.log(JSON.stringify(chains, null, 4)); + logger.log(JSON.stringify(chains, null, 4)); return; } if (format === "jsonl") { - console.log( + logger.log( Object.entries(chains) .map((chain) => JSON.stringify(chain[1])) .join("\n") @@ -36,5 +36,5 @@ export const handler = async (_argv: Arguments): Promise => { return; } // default is "json" - console.log(JSON.stringify(chains)); + logger.log(JSON.stringify(chains)); }; diff --git a/src/commands/controller.ts b/src/commands/controller.ts index 9543411..f168fa3 100644 --- a/src/commands/controller.ts +++ b/src/commands/controller.ts @@ -1,7 +1,7 @@ import type yargs from "yargs"; import type { Arguments, CommandBuilder } from "yargs"; import { Registry } from "@tableland/sdk"; -import { getWalletWithProvider, getLink } from "../utils.js"; +import { getWalletWithProvider, getLink, logger } from "../utils.js"; import { GlobalOptions } from "../cli.js"; export interface Options extends GlobalOptions { @@ -36,10 +36,10 @@ export const builder: CommandBuilder<{}, Options> = (yargs) => const res = await reg.getController(name); - console.log(res); + logger.log(res); /* c8 ignore next 3 */ } catch (err: any) { - console.error(err.message); + logger.error(err.message); } } ) @@ -71,10 +71,10 @@ export const builder: CommandBuilder<{}, Options> = (yargs) => const link = getLink(chain, res.hash); const out = { ...res, link }; - console.log(JSON.stringify(out)); + logger.log(JSON.stringify(out)); /* c8 ignore next 3 */ } catch (err: any) { - console.error(err.message); + logger.error(err.message); } } ) @@ -102,10 +102,10 @@ export const builder: CommandBuilder<{}, Options> = (yargs) => const link = getLink(chain, res.hash); const out = { ...res, link }; - console.log(JSON.stringify(out)); + logger.log(JSON.stringify(out)); /* c8 ignore next 3 */ } catch (err: any) { - console.error(err.message); + logger.error(err.message); } } ); diff --git a/src/commands/create.ts b/src/commands/create.ts index 77679ee..6d50d49 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -1,6 +1,6 @@ import type yargs from "yargs"; import type { Arguments, CommandBuilder } from "yargs"; -import { getLink } from "../utils.js"; +import { getLink, logger } from "../utils.js"; import { createInterface } from "readline"; import { promises } from "fs"; import { GlobalOptions } from "../cli.js"; @@ -43,11 +43,11 @@ export const handler = async (argv: Arguments): Promise => { const { chain, file, prefix, privateKey } = argv; // enforce that all args required for this command are available if (!privateKey) { - console.error("missing required flag (`-k` or `--privateKey`)"); + logger.error("missing required flag (`-k` or `--privateKey`)"); return; } if (!chain) { - console.error("missing required flag (`-c` or `--chain`)"); + logger.error("missing required flag (`-c` or `--chain`)"); return; } if (file != null) { @@ -59,7 +59,7 @@ export const handler = async (argv: Arguments): Promise => { schema = value; } if (!schema) { - console.error( + logger.error( "missing input value (`schema`, `file`, or piped input from stdin required)" ); return; @@ -70,7 +70,7 @@ export const handler = async (argv: Arguments): Promise => { if (check) { statement = schema; } else if (prefix === undefined) { - console.error( + logger.error( "Must specify --prefix if you do not provide a full Create statement" ); } else { @@ -100,11 +100,11 @@ export const handler = async (argv: Arguments): Promise => { ); if (!normalized.every((norm) => norm.type === "create")) { - console.error("the `create` command can only accept create queries"); + logger.error("the `create` command can only accept create queries"); return; } if (statements.length < 1) { - console.error( + logger.error( "after normalizing the statement there was no create query, hence nothing to do" ); return; @@ -123,7 +123,7 @@ export const handler = async (argv: Arguments): Promise => { out.ensNameRegistered = register; } - console.log(JSON.stringify(out)); + logger.log(JSON.stringify(out)); return; } @@ -142,9 +142,9 @@ export const handler = async (argv: Arguments): Promise => { out.ensNameRegistered = register; } - console.log(JSON.stringify(out)); + logger.log(JSON.stringify(out)); /* c8 ignore next 3 */ } catch (err: any) { - console.error(err?.cause?.message || err.message); + logger.error(err?.cause?.message || err.message); } }; diff --git a/src/commands/info.ts b/src/commands/info.ts index 8afb33a..4b784ee 100644 --- a/src/commands/info.ts +++ b/src/commands/info.ts @@ -2,6 +2,7 @@ import type yargs from "yargs"; import type { Arguments, CommandBuilder } from "yargs"; import { GlobalOptions } from "../cli.js"; import { setupCommand } from "../lib/commandSetup.js"; +import { logger } from "../utils.js"; export interface Options extends GlobalOptions { name: string; @@ -24,7 +25,7 @@ export const handler = async (argv: Arguments): Promise => { const parts = name.split("_"); if (parts.length < 3 && !argv.enableEnsExperiment) { - console.error( + logger.error( "invalid table name (name format is `{prefix}_{chainId}_{tableId}`)" ); return; @@ -43,9 +44,9 @@ export const handler = async (argv: Arguments): Promise => { tableId, chainId: parseInt(chainId), }); - console.log(JSON.stringify(res)); + logger.log(JSON.stringify(res)); /* c8 ignore next 3 */ } catch (err: any) { - console.error(err?.cause?.message || err.message); + logger.error(err?.cause?.message || err.message); } }; diff --git a/src/commands/init.ts b/src/commands/init.ts index e5ff0c1..c41d005 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -4,7 +4,7 @@ import yaml from "js-yaml"; import { resolve, dirname } from "path"; import { mkdirSync, createWriteStream, WriteStream } from "fs"; import inquirer from "inquirer"; -import { getChains } from "../utils.js"; +import { getChains, logger } from "../utils.js"; import { GlobalOptions } from "../cli.js"; export interface Options extends GlobalOptions { @@ -111,10 +111,10 @@ export const handler = async (argv: Arguments): Promise => { break; } if (path !== ".") { - console.log(`Config created at ${filePath}`); + logger.log(`Config created at ${filePath}`); } } catch (err: any) { - console.error(err.message); + logger.error(err.message); return; } finally { stream.end("\n"); diff --git a/src/commands/list.ts b/src/commands/list.ts index b33f9a6..a21d715 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -3,6 +3,7 @@ import type { Arguments, CommandBuilder } from "yargs"; import { Wallet } from "ethers"; import { GlobalOptions } from "../cli.js"; import { setupCommand } from "../lib/commandSetup.js"; +import { logger } from "../utils.js"; export interface Options extends GlobalOptions { address: string; @@ -23,14 +24,14 @@ export const handler = async (argv: Arguments): Promise => { const { chain, privateKey } = argv; let { address } = argv; if (!chain) { - console.error("missing required flag (`-c` or `--chain`)"); + logger.error("missing required flag (`-c` or `--chain`)"); return; } if (!address) { if (privateKey) { address = new Wallet(privateKey).address; } else { - console.error("must supply `--privateKey` or `address` positional"); + logger.error("must supply `--privateKey` or `address` positional"); return; } } @@ -39,9 +40,9 @@ export const handler = async (argv: Arguments): Promise => { const res = await registry.listTables(address); - console.log(JSON.stringify(res)); + logger.log(JSON.stringify(res)); /* c8 ignore next 3 */ } catch (err: any) { - console.error(err.message); + logger.error(err.message); } }; diff --git a/src/commands/namespace.ts b/src/commands/namespace.ts index a566cc7..6a5e2d4 100644 --- a/src/commands/namespace.ts +++ b/src/commands/namespace.ts @@ -2,6 +2,7 @@ import type yargs from "yargs"; import { Arguments, CommandBuilder } from "yargs"; import { GlobalOptions } from "../cli.js"; import { setupCommand } from "../lib/commandSetup.js"; +import { logger } from "../utils.js"; export interface Options extends GlobalOptions { domain: string; @@ -16,13 +17,13 @@ async function getHandler(argv: yargs.ArgumentsCamelCase) { const { record } = argv; const { ens } = await setupCommand(argv); if (!ens) { - console.log( + logger.log( "To use ENS, ensure you have set the enableEnsExperiment flag to true" ); return; } - console.log(JSON.stringify({ value: await ens.resolveTable(record) })); + logger.log(JSON.stringify({ value: await ens.resolveTable(record) })); } async function setHandler(argv: yargs.ArgumentsCamelCase) { @@ -55,7 +56,7 @@ async function setHandler(argv: yargs.ArgumentsCamelCase) { mappings, }; - console.log(JSON.stringify(response)); + logger.log(JSON.stringify(response)); } } diff --git a/src/commands/read.ts b/src/commands/read.ts index 4d234f4..09b5ed6 100644 --- a/src/commands/read.ts +++ b/src/commands/read.ts @@ -4,6 +4,7 @@ import { promises } from "fs"; import { createInterface } from "readline"; import { GlobalOptions } from "../cli.js"; import { setupCommand } from "../lib/commandSetup.js"; +import { logger } from "../utils.js"; export interface Options extends GlobalOptions { statement?: string; @@ -69,7 +70,7 @@ export const handler = async (argv: Arguments): Promise => { statement = value; } if (!statement) { - console.error( + logger.error( "missing input value (`statement`, `file`, or piped input from stdin required)" ); return; @@ -97,7 +98,7 @@ export const handler = async (argv: Arguments): Promise => { }); } catch (e: any) { if (e.message.includes("in JSON at position")) { - console.log("Can't unwrap multiple rows. Use --unwrap=false"); + logger.log("Can't unwrap multiple rows. Use --unwrap=false"); } else { throw e; } @@ -108,16 +109,16 @@ export const handler = async (argv: Arguments): Promise => { switch (format) { case "pretty": - console.table(res); + logger.table(res); break; case "objects": - console.log(JSON.stringify(res)); + logger.log(JSON.stringify(res)); break; case "table": - console.log(JSON.stringify(transformTableData(res))); + logger.log(JSON.stringify(transformTableData(res))); break; case "raw": - console.log( + logger.log( JSON.stringify(transformTableData(await db.prepare(statement).raw())) ); break; @@ -125,6 +126,6 @@ export const handler = async (argv: Arguments): Promise => { /* c8 ignore next 3 */ } catch (err: any) { - console.error(err?.cause?.message || err?.message); + logger.error(err?.cause?.message || err?.message); } }; diff --git a/src/commands/receipt.ts b/src/commands/receipt.ts index 2d8e5b1..c8f4428 100644 --- a/src/commands/receipt.ts +++ b/src/commands/receipt.ts @@ -3,6 +3,7 @@ import type { Arguments, CommandBuilder } from "yargs"; import { helpers } from "@tableland/sdk"; import { GlobalOptions } from "../cli.js"; import { setupCommand } from "../lib/commandSetup.js"; +import { logger } from "../utils.js"; export interface Options extends GlobalOptions { hash: string; @@ -22,7 +23,7 @@ export const handler = async (argv: Arguments): Promise => { try { const { hash, chain } = argv; if (!chain) { - console.error("missing required flag (`-c` or `--chain`)"); + logger.error("missing required flag (`-c` or `--chain`)"); return; } const { validator } = await setupCommand(argv); @@ -30,9 +31,9 @@ export const handler = async (argv: Arguments): Promise => { chainId: helpers.getChainId(chain), transactionHash: hash, }); - console.log(JSON.stringify(res)); + logger.log(JSON.stringify(res)); /* c8 ignore next 3 */ } catch (err: any) { - console.error(err.message); + logger.error(err.message); } }; diff --git a/src/commands/schema.ts b/src/commands/schema.ts index ff6d61b..10ce9e0 100644 --- a/src/commands/schema.ts +++ b/src/commands/schema.ts @@ -2,6 +2,7 @@ import type yargs from "yargs"; import type { Arguments, CommandBuilder } from "yargs"; import { GlobalOptions } from "../cli.js"; import { setupCommand } from "../lib/commandSetup.js"; +import { logger } from "../utils.js"; export interface Options extends GlobalOptions { name: string; @@ -24,7 +25,7 @@ export const handler = async (argv: Arguments): Promise => { const parts = name.split("_"); if (parts.length < 3 && !argv.enableEnsExperiment) { - console.error( + logger.error( "invalid table name (name format is `{prefix}_{chainId}_{tableId}`)" ); return; @@ -39,9 +40,9 @@ export const handler = async (argv: Arguments): Promise => { tableId, chainId: parseInt(chainId), }); - console.log(JSON.stringify(res.schema)); + logger.log(JSON.stringify(res.schema)); /* c8 ignore next 3 */ } catch (err: any) { - console.error(err.message); + logger.error(err.message); } }; diff --git a/src/commands/shell.ts b/src/commands/shell.ts index ca87a73..a88f7ef 100644 --- a/src/commands/shell.ts +++ b/src/commands/shell.ts @@ -2,6 +2,7 @@ import yargs, { Arguments, CommandBuilder } from "yargs"; import { createInterface } from "readline"; import { GlobalOptions } from "../cli.js"; import { Connections, setupCommand } from "../lib/commandSetup.js"; +import { logger } from "../utils.js"; const help = `Commands: [query] - run a query @@ -21,7 +22,7 @@ export const desc = export const aliases = ["s", "sh"]; process.on("SIGINT", function () { - console.log("Caught interrupt signal"); + logger.log("Caught interrupt signal"); process.exit(); }); @@ -42,7 +43,7 @@ async function confirmQuery() { if (response === "y" || response === "yes") { resolve(true); } else { - console.log("Aborting."); + logger.log("Aborting."); resolve(false); } } @@ -64,30 +65,23 @@ async function fireFullQuery( const { type } = await globalThis.sqlparser.normalize(statement); if (type !== "read" && !(await confirmQuery())) return; - try { - const stmt = database.prepare(statement); - const response = await stmt.all(); - - console.log(JSON.stringify(response.results)); - switch (type) { - case "create": - console.log( - JSON.stringify({ createdTable: response.meta.txn?.name }) - ); - break; - case "write": - console.log( - JSON.stringify({ updatedTable: response.meta.txn?.name }) - ); - break; - default: - } - } catch (e) { - console.error(e); + const stmt = database.prepare(statement); + const response = await stmt.all(); + + logger.log(JSON.stringify(response.results)); + switch (type) { + case "create": + logger.log(JSON.stringify({ createdTable: response.meta.txn?.name })); + break; + case "write": + logger.log(JSON.stringify({ updatedTable: response.meta.txn?.name })); + break; + default: } /* c8 ignore next 3 */ - } catch (e) { - console.error(e); + } catch (err: any) { + logger.error(err?.cause?.message || err?.message); + logger.error(err); } } @@ -130,7 +124,7 @@ async function shellYeah( break; case "help": default: - console.log(help); + logger.log(help); break; } @@ -148,9 +142,9 @@ async function shellYeah( shellYeah(argv, tablelandConnection, history); } catch (err: any) { - console.error(err.message); + logger.error(err.message); if (argv.verbose) { - console.log(err); + logger.log(err); } } } @@ -177,24 +171,24 @@ export const handler = async (argv: Arguments): Promise => { try { const { chain } = argv; if (!chain) { - console.error("missing required flag (`-c` or `--chain`)"); + logger.error("missing required flag (`-c` or `--chain`)"); return; } const connections = await setupCommand(argv); const { signer, network } = connections; - console.log("Welcome to Tableland"); - console.log(`Tableland CLI shell`); - console.log( + logger.log("Welcome to Tableland"); + logger.log(`Tableland CLI shell`); + logger.log( `Connected to ${network.chainName} using ${await signer.getAddress()}` ); if (argv.enableEnsExperiment) { - console.log( + logger.log( "ENS namespace is experimental, no promises that it will exist in future builds" ); } await shellYeah(argv, connections); } catch (e: any) { - console.error(e.message); + logger.error(e.message); } }; diff --git a/src/commands/transfer.ts b/src/commands/transfer.ts index 638de44..2c5a4c4 100644 --- a/src/commands/transfer.ts +++ b/src/commands/transfer.ts @@ -3,6 +3,7 @@ import type { Arguments, CommandBuilder } from "yargs"; import { GlobalOptions } from "../cli.js"; import { setupCommand } from "../lib/commandSetup.js"; import { init } from "@tableland/sqlparser"; +import { logger } from "../utils.js"; export interface Options extends GlobalOptions { name: string; @@ -39,9 +40,9 @@ export const handler = async (argv: Arguments): Promise => { tableName: name, to: receiver, }); - console.log(JSON.stringify(res)); + logger.log(JSON.stringify(res)); /* c8 ignore next 3 */ } catch (err: any) { - console.error(err.message); + logger.error(err.message); } }; diff --git a/src/commands/write.ts b/src/commands/write.ts index 68aa391..e7f373c 100644 --- a/src/commands/write.ts +++ b/src/commands/write.ts @@ -1,6 +1,6 @@ import type yargs from "yargs"; import type { Arguments, CommandBuilder } from "yargs"; -import { getLink } from "../utils.js"; +import { getLink, logger } from "../utils.js"; import { promises } from "fs"; import { createInterface } from "readline"; import { GlobalOptions } from "../cli.js"; @@ -33,11 +33,11 @@ export const handler = async (argv: Arguments): Promise => { try { // enforce that all args required for this command are available if (!privateKey) { - console.error("missing required flag (`-k` or `--privateKey`)"); + logger.error("missing required flag (`-k` or `--privateKey`)"); return; } if (!chain) { - console.error("missing required flag (`-c` or `--chain`)"); + logger.error("missing required flag (`-c` or `--chain`)"); return; } if (file != null) { @@ -49,7 +49,7 @@ export const handler = async (argv: Arguments): Promise => { statement = value; } if (!statement) { - console.error( + logger.error( "missing input value (`statement`, `file`, or piped input from stdin required)" ); return; @@ -68,11 +68,11 @@ export const handler = async (argv: Arguments): Promise => { const normalized = await normalize(statement); if (normalized.type !== "write") { - console.error("the `write` command can only accept write queries"); + logger.error("the `write` command can only accept write queries"); return; } if (normalized.tables.length < 1) { - console.error( + logger.error( "after normalizing the statement there was no write query, hence nothing to do" ); return; @@ -83,7 +83,7 @@ export const handler = async (argv: Arguments): Promise => { const link = getLink(chain, res?.meta?.txn?.transactionHash as string); const out = { ...res, link }; - console.log(JSON.stringify(out)); + logger.log(JSON.stringify(out)); return; } @@ -129,10 +129,10 @@ export const handler = async (argv: Arguments): Promise => { const [res] = await db.batch(preparedStatements); const link = getLink(chain, res?.meta?.txn?.transactionHash as string); const out = { ...res, link }; - console.log(JSON.stringify(out)); + logger.log(JSON.stringify(out)); /* c8 ignore next 3 */ } catch (err: any) { - console.error(err?.cause?.message || err?.message); - console.error(err); + logger.error(err?.cause?.message || err?.message); + logger.error(err); } }; diff --git a/src/lib/EnsResolver.ts b/src/lib/EnsResolver.ts index eda859e..531fad6 100644 --- a/src/lib/EnsResolver.ts +++ b/src/lib/EnsResolver.ts @@ -1,7 +1,8 @@ import ethers, { Signer } from "ethers"; -import ensLib from "./EnsCommand.js"; import { JsonRpcProvider } from "@ethersproject/providers"; import { ENS } from "@ensdomains/ensjs"; +import ensLib from "./EnsCommand.js"; +import { logger } from "../utils.js"; interface EnsResolverOptions { ensProviderUrl: string; @@ -50,8 +51,8 @@ export default class EnsResolver { }); return true; } catch (e: any) { - console.log("Adding table to ENS failed"); - console.error(e.message); + logger.log("Adding table to ENS failed"); + logger.error(e.message); } return true; } diff --git a/src/utils.ts b/src/utils.ts index 21ae097..9753b65 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -99,3 +99,17 @@ export async function getWalletWithProvider({ /* c8 ignore stop */ return wallet.connect(provider); } + +// Wrap any direct calls to console.log, so that test spies can distinguise between +// the CLI's output, and messaging that originates outside the CLI +export const logger = { + log: function (message: string) { + console.log(message); + }, + table: function (message: unknown[] | undefined) { + console.table(message); + }, + error: function (message: string | unknown) { + console.error(message); + }, +}; diff --git a/test/chains.test.ts b/test/chains.test.ts index 1612b3a..f51e641 100644 --- a/test/chains.test.ts +++ b/test/chains.test.ts @@ -1,8 +1,9 @@ +import { equal } from "node:assert"; import { describe, test, afterEach, before } from "mocha"; -import { spy, restore, assert } from "sinon"; +import { spy, restore } from "sinon"; import yargs from "yargs/yargs"; import * as mod from "../src/commands/chains.js"; -import { getChains } from "../src/utils.js"; +import { getChains, logger } from "../src/utils.js"; describe("commands/chains", function () { before(async function () { @@ -15,31 +16,39 @@ describe("commands/chains", function () { test("chains defaults to json output", async function () { const chains = getChains(); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs(["chains"]).command(mod).parse(); - assert.calledWith(consoleLog, JSON.stringify(chains)); + + const value = consoleLog.getCall(0).firstArg; + equal(value, JSON.stringify(chains)); }); test("chains returns json output", async function () { const chains = getChains(); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs(["chains", "--format=json"]).command(mod).parse(); - assert.calledWith(consoleLog, JSON.stringify(chains)); + + const value = consoleLog.getCall(0).firstArg; + equal(value, JSON.stringify(chains)); }); test("chains format returns readable output", async function () { const chains = getChains(); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs(["chains", "--format=pretty"]).command(mod).parse(); - assert.calledWith(consoleLog, JSON.stringify(chains, null, 4)); + + const value = consoleLog.getCall(0).firstArg; + equal(value, JSON.stringify(chains, null, 4)); }); test("chains format returns jsonl output", async function () { const chains = getChains(); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs(["chains", "--format=jsonl"]).command(mod).parse(); - assert.calledWith( - consoleLog, + + const value = consoleLog.getCall(0).firstArg; + equal( + value, Object.entries(chains) .map((chain) => JSON.stringify(chain[1])) .join("\n") diff --git a/test/controller.test.ts b/test/controller.test.ts index 692c83f..7c50999 100644 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -1,10 +1,10 @@ -import { equal } from "node:assert"; +import { equal, match } from "node:assert"; import { describe, test, afterEach, before } from "mocha"; -import { spy, restore, assert, match } from "sinon"; +import { spy, restore } from "sinon"; import yargs from "yargs/yargs"; import { getAccounts } from "@tableland/local"; import * as mod from "../src/commands/controller.js"; -import { wait } from "../src/utils.js"; +import { wait, logger } from "../src/utils.js"; describe("commands/controller", function () { this.timeout("30s"); @@ -18,18 +18,17 @@ describe("commands/controller", function () { }); test("throws without privateKey", async function () { - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["controller", "get", "blah"]).command(mod).parse(); - assert.calledWith( - consoleError, - "missing required flag (`-k` or `--privateKey`)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "missing required flag (`-k` or `--privateKey`)"); }); test("throws with invalid chain", async function () { const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "controller", "set", @@ -40,16 +39,15 @@ describe("commands/controller", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleError, - "unsupported chain (see `chains` command for details)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "unsupported chain (see `chains` command for details)"); }); test("throws with invalid get argument", async function () { const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "controller", "get", @@ -61,16 +59,15 @@ describe("commands/controller", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleError, - "error validating name: table name has wrong format: invalid" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "error validating name: table name has wrong format: invalid"); }); test("throws with invalid set arguments", async function () { const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "controller", "set", @@ -85,16 +82,13 @@ describe("commands/controller", function () { .parse(); const value = consoleError.getCall(0).firstArg; - equal( - value.includes("error validating name: table name has wrong format: "), - true - ); + match(value, /error validating name: table name has wrong format:/); }); test("passes when setting a controller", async function () { const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs([ "controller", @@ -108,19 +102,18 @@ describe("commands/controller", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleLog, - match(function (value: any) { - const { hash, link } = JSON.parse(value); - return typeof hash === "string" && hash.startsWith("0x") && !link; - }, "does not match") - ); + + const res = consoleLog.getCall(0).firstArg; + const { hash, link } = JSON.parse(res); + equal(typeof hash, "string"); + equal(hash.startsWith("0x"), true); + equal(!link, true); }); test("passes when getting a controller", async function () { const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs([ "controller", "get", @@ -132,7 +125,9 @@ describe("commands/controller", function () { ]) .command(mod) .parse(); - assert.calledWith(consoleLog, `0x0000000000000000000000000000000000000000`); + + const value = consoleLog.getCall(0).firstArg; + equal(value, "0x0000000000000000000000000000000000000000"); }); // TODO: Create tests for locking a controller diff --git a/test/create.test.ts b/test/create.test.ts index ab88fdc..da76fc6 100644 --- a/test/create.test.ts +++ b/test/create.test.ts @@ -1,12 +1,12 @@ -import { equal } from "node:assert"; +import { equal, match } from "node:assert"; import { describe, test, afterEach, before } from "mocha"; -import { spy, restore, assert, match, stub } from "sinon"; +import { spy, restore, stub } from "sinon"; import yargs from "yargs/yargs"; import { temporaryWrite } from "tempy"; import mockStd from "mock-stdin"; import { getAccounts } from "@tableland/local"; import * as mod from "../src/commands/create.js"; -import { wait } from "../src/utils.js"; +import { wait, logger } from "../src/utils.js"; import { ethers } from "ethers"; import { getResolverMock } from "./mock.js"; @@ -24,18 +24,17 @@ describe("commands/create", function () { }); test("throws without privateKey", async function () { - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["create", "blah"]).command(mod).parse(); - assert.calledWith( - consoleError, - "missing required flag (`-k` or `--privateKey`)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "missing required flag (`-k` or `--privateKey`)"); }); test("throws if chain not provided", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "create", "(id int primary key, desc text)", @@ -44,10 +43,9 @@ describe("commands/create", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleError, - "missing required flag (`-c` or `--chain`)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "missing required flag (`-c` or `--chain`)"); }); test("Create namespace with table using ENS", async () => { @@ -56,7 +54,7 @@ describe("commands/create", function () { "getResolver" ).callsFake(getResolverMock); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); await yargs([ @@ -78,36 +76,36 @@ describe("commands/create", function () { fullReolverStub.restore(); - assert.match(consoleLog.getCall(1).args[0], function (value: any) { - value = JSON.parse(value); - return value.ensNameRegistered === true; - }); + const res = consoleLog.getCall(1).firstArg; + const value = JSON.parse(res); + equal(value.ensNameRegistered, true); }); test("throws with invalid chain", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "create", "(id int primary key, desc text)", "--privateKey", privateKey, + "--prefix", + "invalid_chain_table", "--chain", "foozbaaz", ]) .command(mod) .parse(); - assert.calledWith( - consoleError, - "unsupported chain (see `chains` command for details)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "unsupported chain (see `chains` command for details)"); }); test("throws with invalid statement", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "create", "invalid", @@ -121,14 +119,17 @@ describe("commands/create", function () { .command(mod) .parse(); - const res = consoleError.getCall(0).firstArg; - equal(res, "error parsing statement: syntax error at position 32 near ')'"); + const value = consoleError.getCall(0).firstArg; + equal( + value, + "error parsing statement: syntax error at position 32 near ')'" + ); }); test("throws when mixing create and write statements", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "create", "create table fooz (a int);insert into fooz (a) values (1);", @@ -142,14 +143,14 @@ describe("commands/create", function () { .command(mod) .parse(); - const res = consoleError.getCall(0).firstArg; - equal(res, "the `create` command can only accept create queries"); + const value = consoleError.getCall(0).firstArg; + equal(value, "the `create` command can only accept create queries"); }); test("throws with missing file", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "create", "--file", @@ -161,19 +162,16 @@ describe("commands/create", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleError, - match((value) => { - return value.startsWith("ENOENT: no such file or directory"); - }, "Didn't throw ENOENT.") - ); + + const value = consoleError.getCall(0).firstArg; + match(value, /ENOENT: no such file or directory/i); }); test("throws with empty stdin", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); const stdin = mockStd.stdin(); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); setTimeout(() => { stdin.send("\n").end(); }, 100); @@ -186,8 +184,10 @@ describe("commands/create", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleError, + + const value = consoleError.getCall(0).firstArg; + equal( + value, "missing input value (`schema`, `file`, or piped input from stdin required)" ); }); @@ -195,7 +195,7 @@ describe("commands/create", function () { test("Create passes with local-tableland", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs([ "create", "id int primary key, name text", @@ -208,28 +208,23 @@ describe("commands/create", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleLog, - match(function (value: any) { - value = JSON.parse(value); - const { prefix, name, chainId, tableId, transactionHash } = - value.meta.txn; - return ( - prefix === "first_table" && - chainId === 31337 && - name.startsWith(prefix) && - name.endsWith(tableId) && - typeof transactionHash === "string" && - transactionHash.startsWith("0x") - ); - }, "does not match") - ); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + const { prefix, name, chainId, tableId, transactionHash } = value.meta.txn; + + equal(prefix, "first_table"); + equal(chainId, 31337); + equal(name.startsWith(prefix), true); + equal(name.endsWith(tableId), true); + equal(typeof transactionHash, "string"); + equal(transactionHash.startsWith("0x"), true); }); test("passes with full create statement (override prefix)", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs([ "create", "create table second_table (id int primary key, name text);", @@ -242,28 +237,23 @@ describe("commands/create", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleLog, - match(function (value: any) { - value = JSON.parse(value); - const { prefix, name, chainId, tableId, transactionHash } = - value.meta.txn; - return ( - prefix === "second_table" && - chainId === 31337 && - name.startsWith(prefix) && - name.endsWith(tableId) && - typeof transactionHash === "string" && - transactionHash.startsWith("0x") - ); - }, "does not match") - ); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + const { prefix, name, chainId, tableId, transactionHash } = value.meta.txn; + + equal(prefix, "second_table"); + equal(chainId, 31337); + equal(name.startsWith(prefix), true); + equal(name.endsWith(tableId), true); + equal(typeof transactionHash, "string"); + equal(transactionHash.startsWith("0x"), true); }); test("passes with two create statements", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs([ "create", `create table first_table (id int primary key, name text); @@ -299,7 +289,7 @@ describe("commands/create", function () { test("passes when provided input from file", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const path = await temporaryWrite(`\nid int primary key,\nname text\n`); await yargs([ "create", @@ -330,7 +320,7 @@ describe("commands/create", function () { test("passes when provided input from stdin", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const stdin = mockStd.stdin(); setTimeout(() => { stdin @@ -346,21 +336,16 @@ describe("commands/create", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleLog, - match(function (value: any) { - value = JSON.parse(value); - const { prefix, name, chainId, tableId, transactionHash } = - value.meta.txn; - return ( - prefix === "stdin_test" && - chainId === 31337 && - name.startsWith(prefix) && - name.endsWith(tableId) && - typeof transactionHash === "string" && - transactionHash.startsWith("0x") - ); - }, "does not match") - ); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + const { prefix, name, chainId, tableId, transactionHash } = value.meta.txn; + + equal(prefix, "stdin_test"); + equal(chainId, 31337); + equal(name.startsWith(prefix), true); + equal(name.endsWith(tableId), true); + equal(typeof transactionHash, "string"); + equal(transactionHash.startsWith("0x"), true); }); }); diff --git a/test/info.test.ts b/test/info.test.ts index 7f666dc..166d884 100644 --- a/test/info.test.ts +++ b/test/info.test.ts @@ -1,9 +1,9 @@ import { equal } from "node:assert"; import { describe, test, afterEach, before } from "mocha"; -import { spy, restore, assert } from "sinon"; +import { spy, restore } from "sinon"; import yargs from "yargs/yargs"; import * as mod from "../src/commands/info.js"; -import { wait } from "../src/utils.js"; +import { wait, logger } from "../src/utils.js"; describe("commands/info", function () { this.timeout("30s"); @@ -17,25 +17,26 @@ describe("commands/info", function () { }); test("info throws with invalid table name", async function () { - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["info", "invalid_name"]).command(mod).parse(); - assert.calledWith( - consoleError, + + const value = consoleError.getCall(0).firstArg; + equal( + value, "invalid table name (name format is `{prefix}_{chainId}_{tableId}`)" ); }); test("info throws with invalid chain", async function () { - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["info", "valid_9999_0"]).command(mod).parse(); - assert.calledWith( - consoleError, - "unsupported chain (see `chains` command for details)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "cannot use unsupported chain: 9999"); }); test("Info passes with local-tableland", async function () { - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs(["info", "healthbot_31337_1"]).command(mod).parse(); const res = consoleLog.getCall(0).firstArg; @@ -48,8 +49,10 @@ describe("commands/info", function () { }); test("info throws with missing table", async function () { - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["info", "ignored_31337_99"]).command(mod).parse(); - assert.calledWith(consoleError, "Not Found"); + + const value = consoleError.getCall(0).firstArg; + equal(value, "Not Found"); }); }); diff --git a/test/list.test.ts b/test/list.test.ts index 809be18..c9e5837 100644 --- a/test/list.test.ts +++ b/test/list.test.ts @@ -1,8 +1,10 @@ +import { equal } from "node:assert"; import { describe, test, afterEach, before } from "mocha"; -import { spy, restore, assert, match } from "sinon"; +import { spy, restore } from "sinon"; import { getAccounts } from "@tableland/local"; import yargs from "yargs/yargs"; import * as mod from "../src/commands/list.js"; +import { logger } from "../src/utils.js"; describe("commands/list", function () { before(async function () { @@ -14,55 +16,51 @@ describe("commands/list", function () { }); test("throws without privateKey or address", async function () { - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["list", "--chain", "maticmum"]).command(mod).parse(); - assert.calledWith( - consoleError, - "must supply `--privateKey` or `address` positional" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "must supply `--privateKey` or `address` positional"); }); test("List throws without chain", async function () { const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["list", "--privateKey", privateKey]).command(mod).parse(); - assert.calledWith( - consoleError, - "missing required flag (`-c` or `--chain`)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "missing required flag (`-c` or `--chain`)"); }); test("List throws with invalid chain", async function () { const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["list", "--privateKey", privateKey, "--chain", "foozbazz"]) .command(mod) .parse(); - assert.calledWith( - consoleError, - "unsupported chain (see `chains` command for details)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "unsupported chain (see `chains` command for details)"); }); test("throws with custom network", async function () { const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["list", "--chain", "custom", "--privateKey", privateKey]) .command(mod) .parse(); - assert.calledWith( - consoleError, - "unsupported chain (see `chains` command for details)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "unsupported chain (see `chains` command for details)"); }); test("List passes with local-tableland", async function () { const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs([ "list", "--chain", @@ -72,18 +70,13 @@ describe("commands/list", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleLog, - match(function (value: any) { - value = JSON.parse(value); - const array = value; - return ( - Array.isArray(array) && - array.length > 0 && - array[0].tableId === "1" && - array[0].chainId === 31337 - ); - }, "does not match") - ); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + + equal(Array.isArray(value), true); + equal(value.length > 0, true); + equal(value[0].tableId, "1"); + equal(value[0].chainId, 31337); }); }); diff --git a/test/namespace.test.ts b/test/namespace.test.ts index cca9546..4953c7f 100644 --- a/test/namespace.test.ts +++ b/test/namespace.test.ts @@ -1,10 +1,12 @@ +import { equal } from "node:assert"; import { describe, test, afterEach, beforeEach } from "mocha"; -import { spy, restore, assert, stub, match } from "sinon"; +import { spy, restore, stub } from "sinon"; import yargs from "yargs/yargs"; import * as mod from "../src/commands/namespace.js"; import ensLib from "../src/lib/EnsCommand"; import { ethers } from "ethers"; import { getResolverMock } from "./mock.js"; +import { logger } from "../src/utils.js"; describe("commands/namespace", function () { beforeEach(async function () { @@ -31,7 +33,7 @@ describe("commands/namespace", function () { }); test("Get ENS name", async function () { - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs([ "namespace", "get", @@ -43,18 +45,13 @@ describe("commands/namespace", function () { .command(mod) .parse(); - assert.calledWith( - consoleLog, - match((value) => { - value = JSON.parse(value); - - return value.value === "healthbot_31337_1"; - }, "Doesn't match expected output") - ); + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + equal(value.value, "healthbot_31337_1"); }); test("Set ENS name", async function () { - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs([ "namespace", "set", @@ -67,17 +64,10 @@ describe("commands/namespace", function () { .command(mod) .parse(); - assert.calledWith( - consoleLog, - match((value) => { - value = JSON.parse(value); - - return ( - value.domain === "foo.bar.eth" && - value.records[0].key === "healthbot" && - value.records[0].value === "healthbot_31337_1" - ); - }, "Doesn't match expected output") - ); + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + equal(value.domain, "foo.bar.eth"); + equal(value.records[0].key, "healthbot"); + equal(value.records[0].value, "healthbot_31337_1"); }); }); diff --git a/test/read.test.ts b/test/read.test.ts index 69f4f92..aa388d2 100644 --- a/test/read.test.ts +++ b/test/read.test.ts @@ -1,10 +1,11 @@ +import { equal, deepStrictEqual, match } from "node:assert"; import { describe, test, afterEach, before } from "mocha"; -import { spy, restore, assert, match, stub } from "sinon"; +import { spy, restore, stub } from "sinon"; import yargs from "yargs/yargs"; import { temporaryWrite } from "tempy"; import mockStd from "mock-stdin"; import * as mod from "../src/commands/read.js"; -import { wait } from "../src/utils.js"; +import { wait, logger } from "../src/utils.js"; import { ethers } from "ethers"; import { getResolverMock } from "./mock.js"; @@ -19,31 +20,35 @@ describe("commands/read", function () { }); test("throws with invalid table name", async function () { - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); const tableName = "something"; const statement = `select * from ${tableName};`; await yargs(["read", statement, "--baseUrl", "http://127.0.0.1:8080"]) .command(mod) .parse(); - assert.calledWith( - consoleError, + + const value = consoleError.getCall(0).firstArg; + equal( + value, `error validating name: table name has wrong format: ${tableName}` ); }); test("throws with invalid statement", async function () { - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["read", "invalid;", "--baseUrl", "http://127.0.0.1:8080"]) .command(mod) .parse(); - assert.calledWith( - consoleError, + + const value = consoleError.getCall(0).firstArg; + equal( + value, "error parsing statement: syntax error at position 7 near 'invalid'\ninvalid;\n^^^^^^^" ); }); test("throws with missing file", async function () { - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "read", "--file", @@ -53,31 +58,30 @@ describe("commands/read", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleError, - match((value) => { - return value.startsWith("ENOENT: no such file or directory"); - }, "Didn't throw ENOENT.") - ); + + const value = consoleError.getCall(0).firstArg; + match(value, /^ENOENT: no such file or directory/i); }); test("throws with empty stdin", async function () { const stdin = mockStd.stdin(); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); setTimeout(() => { stdin.send("\n").end(); }, 100); await yargs(["read", "--baseUrl", "http://127.0.0.1:8080"]) .command(mod) .parse(); - assert.calledWith( - consoleError, + + const value = consoleError.getCall(0).firstArg; + equal( + value, "missing input value (`statement`, `file`, or piped input from stdin required)" ); }); test("passes with extract option", async function () { - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs([ "read", "select counter from healthbot_31337_1;", @@ -87,17 +91,16 @@ describe("commands/read", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleLog, - match((value) => { - value = JSON.parse(value); - return Array.isArray(value) && value.includes(1); - }, "Doesn't match expected output") - ); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + + equal(Array.isArray(value), true); + equal(value.includes(1), true); }); test("passes with unwrap option", async function () { - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs([ "read", "select counter from healthbot_31337_1 where counter = 1;", @@ -107,31 +110,25 @@ describe("commands/read", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleLog, - match((value) => { - value = JSON.parse(value); - return value.counter === 1; - }, "Doesn't match expected output") - ); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + equal(value.counter, 1); }); test("Read passes with local-tableland (defaults to 'objects' format)", async function () { - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs(["read", "select * from healthbot_31337_1"]) .command(mod) .parse(); - assert.calledWith( - consoleLog, - match((value) => { - value = JSON.parse(value); - return value[0].counter === 1; - }, "Doesn't match expected output") - ); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + equal(value[0].counter, 1); }); test("passes with alternate output format (objects)", async function () { - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs([ "read", "select * from healthbot_31337_1;", @@ -140,13 +137,10 @@ describe("commands/read", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleLog, - match((value) => { - value = JSON.parse(value); - return value[0].counter === 1; - }, "Doesn't match expected output") - ); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + equal(value[0].counter, 1); }); test("ENS experimental replaces shorthand with tablename", async function () { @@ -154,7 +148,7 @@ describe("commands/read", function () { ethers.providers.JsonRpcProvider.prototype, "getResolver" ).callsFake(getResolverMock); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs([ "read", "select * from [foo.bar.ens];", @@ -169,33 +163,25 @@ describe("commands/read", function () { fullReolverStub.restore(); - assert.calledWith( - consoleLog, - match((value) => { - value = JSON.parse(value); - return value[0].counter === 1; - }, "Doesn't match expected output") - ); + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + equal(value[0].counter, 1); }); test("passes when provided input from file", async function () { - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const path = await temporaryWrite("select * from healthbot_31337_1;\n"); await yargs(["read", "--file", path, "--format", "objects"]) .command(mod) .parse(); - assert.calledWith( - consoleLog, - match((value) => { - value = JSON.parse(value); - const res = value; - return res[0].counter === 1; - }, "Doesn't match expected output") - ); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + equal(value[0].counter, 1); }); test("passes when provided input from stdin", async function () { - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const stdin = mockStd.stdin(); setTimeout(() => { stdin.send("select * from healthbot_31337_1;\n").end(); @@ -209,13 +195,10 @@ describe("commands/read", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleLog, - match((value) => { - value = JSON.parse(value); - return value[0].counter === 1; - }, "Doesn't match expected output") - ); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + equal(value[0].counter, 1); }); test("Custom baseUrl is called", async function () { @@ -231,14 +214,12 @@ describe("commands/read", function () { .command(mod) .parse(); - assert.calledWith( - fetchSpy, - match((v: any) => v.includes("https://localhost:8909/")) - ); + const url = fetchSpy.getCall(0).firstArg; + match(url, /^https:\/\/localhost:8909\//); }); - test.skip("passes with alternate output format (pretty)", async function () { - const consoleLog = spy(console, "log"); + test("passes with alternate output format (pretty)", async function () { + const consoleLog = spy(logger, "table"); await yargs([ "read", "select * from healthbot_31337_1;", @@ -247,14 +228,9 @@ describe("commands/read", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleLog, - ` -┌─────────┬─────────┐ -│ (index) │ counter │ -├─────────┼─────────┤ -│ 0 │ 1 │ -└─────────┴─────────┘`.trimStart() - ); + + const value = consoleLog.getCall(0).firstArg; + + deepStrictEqual(value, [{ counter: 1 }]); }); }); diff --git a/test/receipt.test.ts b/test/receipt.test.ts index c3e038c..66285cf 100644 --- a/test/receipt.test.ts +++ b/test/receipt.test.ts @@ -1,14 +1,18 @@ -import { getAccounts } from "@tableland/local"; +import { equal, match } from "node:assert"; +import { getAccounts, getDatabase } from "@tableland/local"; import { describe, test, afterEach, before } from "mocha"; -import { spy, restore, assert } from "sinon"; +import { spy, restore } from "sinon"; import yargs from "yargs/yargs"; -import { Database } from "@tableland/sdk"; -import { getWalletWithProvider, wait } from "../src/utils.js"; +import { wait, logger } from "../src/utils.js"; import * as mod from "../src/commands/receipt.js"; describe("commands/receipt", function () { this.timeout("30s"); + const accounts = getAccounts(); + // using the validator wallet since the test is updating healthbot + const db = getDatabase(accounts[0]); + before(async function () { await wait(10000); }); @@ -20,20 +24,19 @@ describe("commands/receipt", function () { test("Receipt throws without chain", async function () { const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["receipt", "--privateKey", privateKey, "ignored"]) .command(mod) .parse(); - assert.calledWith( - consoleError, - "missing required flag (`-c` or `--chain`)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "missing required flag (`-c` or `--chain`)"); }); test("Receipt throws with invalid chain", async function () { const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "receipt", "--privateKey", @@ -44,16 +47,15 @@ describe("commands/receipt", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleError, - "unsupported chain (see `chains` command for details)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "unsupported chain (see `chains` command for details)"); }); test("throws with invalid tx hash", async function () { const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "receipt", "--privateKey", @@ -64,38 +66,41 @@ describe("commands/receipt", function () { ]) .command(mod) .parse(); - assert.calledWith(consoleError, "Not Found"); + + const value = consoleError.getCall(0).firstArg; + equal(value, "Not Found"); }); test("Receipt passes with local-tableland", async function () { const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); - const signer = await getWalletWithProvider({ + const { meta } = await db + .prepare("update healthbot_31337_1 set counter=1;") + .all(); + const hash = meta.txn?.transactionHash ?? ""; + + equal(typeof hash, "string"); + equal(hash.length > 0, true); + + await yargs([ + "receipt", + "--privateKey", privateKey, - chain: "local-tableland", - providerUrl: undefined, - }); + "--chain", + "local-tableland", + hash, + ]) + .command(mod) + .parse(); - const db = new Database({ signer }) - .prepare("update healthbot_31337_1 set counter=1;") - .all() as any; - - db.then(async () => { - await yargs([ - "receipt", - "--privateKey", - privateKey, - "--chain", - "local-tableland", - - db.transactionHash, - ]) - .command(mod) - .parse(); - // TODO: Ideally, we check the response here, but the hashes aren't deterministic - assert.calledOnce(consoleLog); - }); + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + + match(value.transactionHash, /0x[a-f0-9]+/i); + equal(typeof value.tableId, "string"); + equal(typeof value.blockNumber, "number"); + equal(value.chainId, 31337); }); }); diff --git a/test/schema.test.ts b/test/schema.test.ts index 7c3db8f..8ccffe2 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -1,11 +1,13 @@ +import { equal } from "node:assert"; import { describe, test, afterEach, before } from "mocha"; -import { spy, restore, assert } from "sinon"; +import { spy, restore } from "sinon"; import yargs from "yargs/yargs"; import * as mod from "../src/commands/schema.js"; +import { wait, logger } from "../src/utils.js"; describe("commands/schema", function () { before(async function () { - await new Promise((resolve) => setTimeout(resolve, 1000)); + await wait(1000); }); afterEach(function () { @@ -13,35 +15,37 @@ describe("commands/schema", function () { }); test("throws without invalid table name", async function () { - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["schema", "invalid_name"]).command(mod).parse(); - assert.calledWith( - consoleError, + + const value = consoleError.getCall(0).firstArg; + equal( + value, "invalid table name (name format is `{prefix}_{chainId}_{tableId}`)" ); }); test("throws with invalid chain", async function () { - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["schema", "valid_9999_0"]).command(mod).parse(); - assert.calledWith( - consoleError, - "unsupported chain (see `chains` command for details)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "cannot use unsupported chain: 9999"); }); test("throws with missing table", async function () { - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["schema", "ignored_31337_99"]).command(mod).parse(); - assert.calledWith(consoleError, "Not Found"); + + const value = consoleError.getCall(0).firstArg; + equal(value, "Not Found"); }); test("Schema passes with local-tableland", async function () { - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs(["schema", "healthbot_31337_1"]).command(mod).parse(); - assert.calledWith( - consoleLog, - `{"columns":[{"name":"counter","type":"integer"}]}` - ); + + const value = consoleLog.getCall(0).firstArg; + equal(value, `{"columns":[{"name":"counter","type":"integer"}]}`); }); }); diff --git a/test/setup.ts b/test/setup.ts index 8df03d2..58a6597 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,17 +1,6 @@ import { after, before } from "mocha"; import { LocalTableland } from "@tableland/local"; -import fetch, { Headers, Request, Response } from "node-fetch"; -if (!globalThis.fetch) { - (globalThis as any).fetch = fetch; - (globalThis as any).Headers = Headers; - (globalThis as any).Request = Request; - (globalThis as any).Response = Response; -} - -// TODO: most tests rely on a spy on the global `console.log`. This means we must -// use silent: true here. As an alternative we could explore using a `logger` -// that can be mocked, or spied on, or expose an extension api depending on the test. const lt = new LocalTableland({ silent: true }); before(async function () { diff --git a/test/shell.test.ts b/test/shell.test.ts index 5c9bda8..f842f54 100644 --- a/test/shell.test.ts +++ b/test/shell.test.ts @@ -1,11 +1,11 @@ -import { equal } from "node:assert"; +import { equal, match } from "node:assert"; import { describe, test } from "mocha"; -import { spy, assert, restore, match, stub } from "sinon"; +import { spy, restore, stub, assert } from "sinon"; import yargs from "yargs/yargs"; import mockStd from "mock-stdin"; import { getAccounts } from "@tableland/local"; import * as mod from "../src/commands/shell.js"; -import { wait } from "../src/utils.js"; +import { wait, logger } from "../src/utils.js"; import { ethers } from "ethers"; import { getResolverMock } from "./mock.js"; @@ -21,7 +21,7 @@ describe("commands/shell", function () { }); test("Shell Works with single line", async function () { - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const stdin = mockStd.stdin(); setTimeout(() => { @@ -42,7 +42,8 @@ describe("commands/shell", function () { .command(mod) .parse(); - assert.match(consoleLog.getCall(3).args[0], '[{"counter":1}]'); + const value = consoleLog.getCall(3).args[0]; + equal(value, '[{"counter":1}]'); }); test("ENS in shell with single line", async function () { @@ -51,7 +52,7 @@ describe("commands/shell", function () { "getResolver" ).callsFake(getResolverMock); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const stdin = mockStd.stdin(); setTimeout(() => { @@ -87,7 +88,7 @@ describe("commands/shell", function () { "getResolver" ).callsFake(getResolverMock); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const stdin = mockStd.stdin(); setTimeout(() => { @@ -123,7 +124,7 @@ describe("commands/shell", function () { "getResolver" ).callsFake(getResolverMock); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const stdin = mockStd.stdin(); setTimeout(() => { @@ -149,12 +150,12 @@ describe("commands/shell", function () { fullResolverStub.reset(); - const call4 = consoleLog.getCall(4); - equal(call4.args[0], '[{"counter":1}]'); + const value = consoleLog.getCall(4).args[0]; + equal(value, '[{"counter":1}]'); }); test("Shell Works with initial input", async function () { - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); await yargs([ @@ -170,11 +171,12 @@ describe("commands/shell", function () { .command(mod) .parse(); - assert.match(consoleLog.getCall(3).args[0], '[{"counter":1}]'); + const value = consoleLog.getCall(3).args[0]; + equal(value, '[{"counter":1}]'); }); test("Shell handles invalid query", async function () { - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); const stdin = mockStd.stdin(); setTimeout(() => { @@ -195,18 +197,16 @@ describe("commands/shell", function () { .command(mod) .parse(); - assert.calledWith( - consoleError, - match((v: any) => v.message.includes("error parsing statement")) - ); + const value = consoleError.getCall(0).args[0] as string; + match(value, /error parsing statement/); }); test("Write queries continue with 'y' input", async function () { - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const stdin = mockStd.stdin(); setTimeout(() => { - stdin.send("CREATE TABLE SomeTable (id integer, message text);\n"); + stdin.send("CREATE TABLE sometable (id integer, message text);\n"); setTimeout(() => { stdin.send("y\n"); }, 500); @@ -226,14 +226,13 @@ describe("commands/shell", function () { .command(mod) .parse(); - assert.match(consoleLog.getCall(4).args[0], (v: any) => { - const value = JSON.parse(v); - return value.createdTable; - }); + const value = consoleLog.getCall(4).args[0]; + match(value, /"createdTable":/); + match(value, /sometable_31337_\d+/); }); test("Write queries aborts with 'n' input", async function () { - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const stdin = mockStd.stdin(); setTimeout(() => { @@ -257,31 +256,29 @@ describe("commands/shell", function () { .command(mod) .parse(); - assert.match(consoleLog.getCall(3).args[0], "Aborting."); + match(consoleLog.getCall(3).args[0], /Aborting\./i); }); test("Shell throws without chain", async function () { const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["shell", "--privateKey", privateKey]).command(mod).parse(); - assert.calledWith( - consoleError, - "missing required flag (`-c` or `--chain`)" - ); + + const value = consoleError.getCall(0).args[0]; + equal(value, "missing required flag (`-c` or `--chain`)"); }); test("Shell throws with invalid chain", async function () { const [account] = getAccounts(); const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["shell", "--privateKey", privateKey, "--chain", "foozbazz"]) .command(mod) .parse(); - assert.calledWith( - consoleError, - "unsupported chain (see `chains` command for details)" - ); + + const value = consoleError.getCall(0).args[0]; + equal(value, "unsupported chain (see `chains` command for details)"); }); test("Custom baseUrl is called", async function () { @@ -308,10 +305,8 @@ describe("commands/shell", function () { .command(mod) .parse(); - assert.calledWith( - fetchSpy, - match((v: any) => v.includes("https://localhost:8909/")) - ); + const value = fetchSpy.getCall(0).args[0] as string; + match(value, /https:\/\/localhost:8909\//); }); test(".exit exits the shell", async function () { @@ -346,7 +341,7 @@ describe("commands/shell", function () { }); test("Shell Works with multi-line", async function () { - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const stdin = mockStd.stdin(); setTimeout(() => { @@ -367,6 +362,7 @@ describe("commands/shell", function () { .command(mod) .parse(); - assert.match(consoleLog.getCall(3).args[0], '[{"counter":1}]'); + const value = consoleLog.getCall(3).args[0]; + equal(value, '[{"counter":1}]'); }); }); diff --git a/test/transfer.test.ts b/test/transfer.test.ts index abe2663..697720f 100644 --- a/test/transfer.test.ts +++ b/test/transfer.test.ts @@ -1,10 +1,10 @@ import { equal } from "node:assert"; import { describe, test, afterEach, before } from "mocha"; -import { spy, restore, assert } from "sinon"; +import { spy, restore } from "sinon"; import yargs from "yargs/yargs"; import { getAccounts, getDatabase } from "@tableland/local"; import * as mod from "../src/commands/transfer.js"; -import { wait } from "../src/utils.js"; +import { wait, logger } from "../src/utils.js"; import { helpers } from "@tableland/sdk"; describe("commands/transfer", function () { @@ -27,12 +27,14 @@ describe("commands/transfer", function () { }); test("throws without privateKey", async function () { - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["transfer", tableName, "0x0000000000000000000000"]) .command(mod) .parse(); - assert.calledWith( - consoleError, + + const value = consoleError.getCall(0).firstArg; + equal( + value, "No registry. This may be because you did not specify a private key with which to interact with the registry." ); }); @@ -40,7 +42,7 @@ describe("commands/transfer", function () { test("throws with invalid chain", async function () { const account = accounts[1]; const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "transfer", tableName, @@ -52,29 +54,27 @@ describe("commands/transfer", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleError, - "unsupported chain (see `chains` command for details)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "unsupported chain (see `chains` command for details)"); }); test("throws with invalid table name", async function () { const account = accounts[1]; const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["transfer", "fooz", "blah", "-k", privateKey]) .command(mod) .parse(); - assert.calledWith( - consoleError, - "error validating name: table name has wrong format: fooz" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "error validating name: table name has wrong format: fooz"); }); test("throws with invalid receiver address", async function () { const account = accounts[1]; const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "transfer", tableName, @@ -86,8 +86,10 @@ describe("commands/transfer", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleError, + + const value = consoleError.getCall(0).firstArg; + equal( + value, 'invalid address (argument="address", value="0x00", code=INVALID_ARGUMENT, version=address/5.7.0)' ); }); @@ -96,7 +98,7 @@ describe("commands/transfer", function () { test("Write passes with local-tableland", async function () { const [, account1, account2] = accounts; const account2Address = account2.address; - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const privateKey = account1.privateKey.slice(2); await yargs([ "transfer", diff --git a/test/write.test.ts b/test/write.test.ts index 1cd99a5..d40f2a0 100644 --- a/test/write.test.ts +++ b/test/write.test.ts @@ -1,12 +1,12 @@ -import { equal } from "node:assert"; +import { equal, match } from "node:assert"; import { describe, test, afterEach, before } from "mocha"; -import { spy, restore, assert, match } from "sinon"; +import { spy, restore } from "sinon"; import yargs from "yargs/yargs"; import { temporaryWrite } from "tempy"; import mockStd from "mock-stdin"; import { getAccounts, getDatabase } from "@tableland/local"; import * as mod from "../src/commands/write.js"; -import { wait } from "../src/utils.js"; +import { wait, logger } from "../src/utils.js"; const accounts = getAccounts(); const db = getDatabase(accounts[1]); @@ -23,18 +23,17 @@ describe("commands/write", function () { }); test("throws without privateKey", async function () { - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs(["write", "blah"]).command(mod).parse(); - assert.calledWith( - consoleError, - "missing required flag (`-k` or `--privateKey`)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "missing required flag (`-k` or `--privateKey`)"); }); test("throws missing chain", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "write", "insert into fake_31337_1 values (1, 2, 3);", @@ -43,16 +42,15 @@ describe("commands/write", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleError, - "missing required flag (`-c` or `--chain`)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "missing required flag (`-c` or `--chain`)"); }); test("throws with invalid chain", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "write", "insert into fake_31337_1 values (1, 2, 3);", @@ -63,16 +61,15 @@ describe("commands/write", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleError, - "unsupported chain (see `chains` command for details)" - ); + + const value = consoleError.getCall(0).firstArg; + equal(value, "unsupported chain (see `chains` command for details)"); }); test("throws with invalid statement", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "write", // Note: cannot have a table named "table" @@ -85,30 +82,17 @@ describe("commands/write", function () { .command(mod) .parse(); - assert.calledWith( - consoleError, - match(function (value) { - if (typeof value !== "string") { - // console.error is being called with the error string, - // and the error object. We want to ignore the object. - return true; - } - - return ( - value.includes( - `error parsing statement: syntax error at position 12 near 'table'` - ) && - value.includes(`update table set counter=1 where rowid=0; - ^^^^^`) - ); - }, "error does not match") + const value = consoleError.getCall(0).firstArg; + match( + value, + /error parsing statement: syntax error at position 12 near 'table'/ ); }); test("throws when mixing write and create statements", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "write", "insert into fooz (a) values (1);create table fooz (a int);", @@ -122,9 +106,9 @@ describe("commands/write", function () { .command(mod) .parse(); - const res = consoleError.getCall(0).firstArg; + const value = consoleError.getCall(0).firstArg; equal( - res, + value, "error parsing statement: syntax error at position 38 near 'create'" ); }); @@ -132,7 +116,7 @@ describe("commands/write", function () { test("throws with missing file", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); await yargs([ "write", "--file", @@ -144,19 +128,16 @@ describe("commands/write", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleError, - match((value) => { - return value.startsWith("ENOENT: no such file or directory"); - }, "Didn't throw ENOENT.") - ); + + const value = consoleError.getCall(0).firstArg; + match(value, /ENOENT: no such file or directory/); }); test("throws with empty stdin", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); const stdin = mockStd.stdin(); - const consoleError = spy(console, "error"); + const consoleError = spy(logger, "error"); setTimeout(() => { stdin.send("\n").end(); }, 100); @@ -169,8 +150,10 @@ describe("commands/write", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleError, + + const value = consoleError.getCall(0).firstArg; + equal( + value, "missing input value (`statement`, `file`, or piped input from stdin required)" ); }); @@ -178,7 +161,7 @@ describe("commands/write", function () { test("Write passes with local-tableland", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs([ "write", "update healthbot_31337_1 set counter=1 where rowid=0;", // This just updates in place @@ -212,7 +195,7 @@ describe("commands/write", function () { const tableName2 = meta2.txn!.name; const privateKey = account.privateKey.slice(2); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); await yargs([ "write", `insert into ${tableName1} (a, b) values (1, 'one'); @@ -250,7 +233,7 @@ describe("commands/write", function () { test("passes when provided input from file", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const path = await temporaryWrite( "update healthbot_31337_1 set counter=1;\n" ); @@ -265,24 +248,20 @@ describe("commands/write", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleLog, - match(function (value: any) { - value = JSON.parse(value); - const { transactionHash, link } = value.meta.txn; - return ( - typeof transactionHash === "string" && - transactionHash.startsWith("0x") && - !link - ); - }, "does not match") - ); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + const { transactionHash, link } = value.meta?.txn; + + equal(typeof transactionHash, "string"); + equal(transactionHash.startsWith("0x"), true); + equal(!link, true); }); test("passes when provided input from stdin", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); - const consoleLog = spy(console, "log"); + const consoleLog = spy(logger, "log"); const stdin = mockStd.stdin(); setTimeout(() => { stdin.send("update healthbot_31337_1 set counter=1;\n").end(); @@ -296,17 +275,13 @@ describe("commands/write", function () { ]) .command(mod) .parse(); - assert.calledWith( - consoleLog, - match(function (value: any) { - value = JSON.parse(value); - const { transactionHash, link } = value.meta.txn; - return ( - typeof transactionHash === "string" && - transactionHash.startsWith("0x") && - !link - ); - }, "does not match") - ); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + const { transactionHash, link } = value.meta?.txn; + + equal(typeof transactionHash, "string"); + equal(transactionHash.startsWith("0x"), true); + equal(!link, true); }); }); From b8e18c91b86bab8d058036a5968e05405d9cf22c Mon Sep 17 00:00:00 2001 From: Joe Wagner Date: Thu, 1 Jun 2023 16:43:53 -0600 Subject: [PATCH 2/4] wip: nearly full test coverage --- package.json | 2 +- src/commands/info.ts | 1 + src/commands/namespace.ts | 60 ++++++++++------- src/commands/read.ts | 9 +-- src/commands/shell.ts | 4 +- src/commands/write.ts | 4 ++ src/lib/EnsResolver.ts | 6 +- src/lib/commandSetup.ts | 10 ++- test/controller.test.ts | 56 +++++++++++----- test/create.test.ts | 112 ++++++++++++++++++++++--------- test/namespace.test.ts | 75 +++++++++++++++++++++ test/read.test.ts | 98 +++++++++++++++++++++++++-- test/setup.ts | 10 +++ test/shell.test.ts | 137 +++++++++++++++++++++++++++----------- test/transfer.test.ts | 3 +- test/write.test.ts | 21 ++++++ 16 files changed, 481 insertions(+), 127 deletions(-) diff --git a/package.json b/package.json index c9da371..84bf44e 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "format": "npm run prettier:fix && npm run lint:fix", "prepublishOnly": "npm run build", "test": "mocha", - "coverage": "c8 mocha", + "coverage": "TEST_TIMEOUT_FACTOR=3 c8 --100 --exclude test mocha --exit", "clean": "rm -rf dist", "build": "npx tsc && chmod +x dist/cli.js", "tableland": "node --experimental-specifier-resolution=node ./dist/cli.js" diff --git a/src/commands/info.ts b/src/commands/info.ts index 4b784ee..1f0ddf7 100644 --- a/src/commands/info.ts +++ b/src/commands/info.ts @@ -36,6 +36,7 @@ export const handler = async (argv: Arguments): Promise => { chain: parseInt(chainId) as any, }); + /* c8 ignore next 3 */ if (argv.enableEnsExperiment && ens) { name = await ens.resolveTable(name); } diff --git a/src/commands/namespace.ts b/src/commands/namespace.ts index 6a5e2d4..2d995bf 100644 --- a/src/commands/namespace.ts +++ b/src/commands/namespace.ts @@ -27,36 +27,46 @@ async function getHandler(argv: yargs.ArgumentsCamelCase) { } async function setHandler(argv: yargs.ArgumentsCamelCase) { - const { domain, mappings } = argv; - const { ens } = await setupCommand(argv); - if (!ens) return; + try { + const { domain, mappings } = argv; + const { ens } = await setupCommand(argv); + if (!ens) { + logger.log( + "To use ENS, ensure you have set the enableEnsExperiment flag to true" + ); + return; + } - const records = mappings.map((entry: any) => { - const [key, value] = entry.split("="); + const records = mappings.map((entry: any) => { + const [key, value] = entry.split("="); - const keyRegex = /^[a-zA-Z0-9_]*$/; - const valueRegex = /^[a-zA-Z_][a-zA-Z0-9_]*_[0-9]+_[0-9]+$/; + const keyRegex = /^[a-zA-Z0-9_]*$/; + const valueRegex = /^[a-zA-Z_][a-zA-Z0-9_]*_[0-9]+_[0-9]+$/; - if (keyRegex.exec(key) === null) { - throw new Error("Only letters or underscores in key name"); - } - if (valueRegex.exec(value) === null) { - throw new Error("Tablename is invalid"); - } - return { - key, - value, - }; - }); + if (keyRegex.exec(key) === null) { + throw new Error("Only letters or underscores in key name"); + } + if (valueRegex.exec(value) === null) { + throw new Error("Tablename is invalid"); + } + return { + key, + value, + }; + }); - if (await ens.addTableRecords(domain, records)) { - const response = { - domain, - records, - mappings, - }; + if (await ens.addTableRecords(domain, records)) { + const response = { + domain, + records, + mappings, + }; - logger.log(JSON.stringify(response)); + logger.log(JSON.stringify(response)); + } + /* c8 ignore next 3 */ + } catch (err: any) { + logger.error(err?.cause?.message || err?.message); } } diff --git a/src/commands/read.ts b/src/commands/read.ts index 09b5ed6..c55fdb2 100644 --- a/src/commands/read.ts +++ b/src/commands/read.ts @@ -96,11 +96,12 @@ export const handler = async (argv: Arguments): Promise => { format: "objects", unwrap: argv.unwrap, }); - } catch (e: any) { - if (e.message.includes("in JSON at position")) { - logger.log("Can't unwrap multiple rows. Use --unwrap=false"); + } catch (err: any) { + if (err.message.includes("in JSON at position")) { + logger.error("Can't unwrap multiple rows. Use --unwrap=false"); + /* c8 ignore next 3 */ } else { - throw e; + throw err; } } } else { diff --git a/src/commands/shell.ts b/src/commands/shell.ts index a88f7ef..d989f49 100644 --- a/src/commands/shell.ts +++ b/src/commands/shell.ts @@ -22,8 +22,8 @@ export const desc = export const aliases = ["s", "sh"]; process.on("SIGINT", function () { + /* c8 ignore next 2 */ logger.log("Caught interrupt signal"); - process.exit(); }); @@ -108,6 +108,7 @@ async function shellYeah( history = newHistory; }); rl.on("SIGINT", () => { + /* c8 ignore next 1 */ process.exit(); }); @@ -141,6 +142,7 @@ async function shellYeah( } shellYeah(argv, tablelandConnection, history); + /* c8 ignore next 6 */ } catch (err: any) { logger.error(err.message); if (argv.verbose) { diff --git a/src/commands/write.ts b/src/commands/write.ts index e7f373c..3cf35ef 100644 --- a/src/commands/write.ts +++ b/src/commands/write.ts @@ -71,7 +71,10 @@ export const handler = async (argv: Arguments): Promise => { logger.error("the `write` command can only accept write queries"); return; } + // Note: I can't figure out a write statement that updates 2 tables and makes + // it through the parser, but leaving this here because one might exist. if (normalized.tables.length < 1) { + /* c8 ignore next 5 */ logger.error( "after normalizing the statement there was no write query, hence nothing to do" ); @@ -95,6 +98,7 @@ export const handler = async (argv: Arguments): Promise => { // re-normalize so we can be sure we've isolated each statement and it's tableId const norm = await normalize(stmt); if (norm.tables.length > 1) { + /* c8 ignore next 4 */ throw new Error( "cannot normalize if single query affects more then one table" ); diff --git a/src/lib/EnsResolver.ts b/src/lib/EnsResolver.ts index 531fad6..f1f4987 100644 --- a/src/lib/EnsResolver.ts +++ b/src/lib/EnsResolver.ts @@ -21,7 +21,10 @@ export default class EnsResolver { constructor(options: EnsResolverOptions) { const { signer, ensProviderUrl } = options; - if (!ensProviderUrl) throw new Error("No ensProviderUrl given"); + /* c8 ignore next 3 */ + if (!ensProviderUrl) { + throw new Error("No ensProviderUrl given"); + } this.signer = signer; this.provider = new JsonRpcProvider(ensProviderUrl); @@ -37,6 +40,7 @@ export default class EnsResolver { const domain = domainArray.join("."); const address = await this.provider.getResolver(domain); + // TODO: mock `this.provider.getResolver` so address is undefined and we get coverage on the or clause return (await address?.getText(textRecord)) || tablename; } diff --git a/src/lib/commandSetup.ts b/src/lib/commandSetup.ts index 98fc4f2..c7d3912 100644 --- a/src/lib/commandSetup.ts +++ b/src/lib/commandSetup.ts @@ -21,6 +21,7 @@ export class Connections { readyCheck() { if (!this._readyResolved) + /* c8 ignore next 3 */ throw new Error( "You must await the 'ready' method before using this class" ); @@ -42,8 +43,10 @@ export class Connections { get validator(): Validator { this.readyCheck(); - if (!this._validator) + /* c8 ignore next 3 */ + if (!this._validator) { throw new Error("No validator. Set a chain or a baseURL."); + } return this._validator; } @@ -59,15 +62,18 @@ export class Connections { get database(): Database { this.readyCheck(); - if (!this._database) + /* c8 ignore next 5 */ + if (!this._database) { throw new Error( "No database defined. You must specify a providerUrl or chain." ); + } return this._database; } get network(): helpers.ChainInfo { this.readyCheck(); + /* c8 ignore next 1 */ if (!this._network) throw new Error("No network"); return this._network; } diff --git a/test/controller.test.ts b/test/controller.test.ts index 7c50999..e83d32c 100644 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -2,15 +2,24 @@ import { equal, match } from "node:assert"; import { describe, test, afterEach, before } from "mocha"; import { spy, restore } from "sinon"; import yargs from "yargs/yargs"; -import { getAccounts } from "@tableland/local"; +import { getAccounts, getDatabase } from "@tableland/local"; import * as mod from "../src/commands/controller.js"; import { wait, logger } from "../src/utils.js"; describe("commands/controller", function () { this.timeout("30s"); + // account[0] is the Validator's wallet, try to avoid using that + const accounts = getAccounts(); + const db = getDatabase(accounts[1]); + + let tableName: string; before(async function () { await wait(500); + const { meta } = await db + .prepare("CREATE TABLE test_controller (a int);") + .all(); + tableName = meta.txn?.name ?? ""; }); afterEach(function () { @@ -26,8 +35,7 @@ describe("commands/controller", function () { }); test("throws with invalid chain", async function () { - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[1].privateKey.slice(2); const consoleError = spy(logger, "error"); await yargs([ "controller", @@ -45,8 +53,7 @@ describe("commands/controller", function () { }); test("throws with invalid get argument", async function () { - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[1].privateKey.slice(2); const consoleError = spy(logger, "error"); await yargs([ "controller", @@ -65,8 +72,7 @@ describe("commands/controller", function () { }); test("throws with invalid set arguments", async function () { - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[1].privateKey.slice(2); const consoleError = spy(logger, "error"); await yargs([ "controller", @@ -86,15 +92,14 @@ describe("commands/controller", function () { }); test("passes when setting a controller", async function () { - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[1].privateKey.slice(2); const consoleLog = spy(logger, "log"); await yargs([ "controller", "set", - "0x0000000000000000000000000000000000000000", - "healthbot_31337_1", + accounts[2].address, + tableName, "--privateKey", privateKey, "--chain", @@ -111,13 +116,12 @@ describe("commands/controller", function () { }); test("passes when getting a controller", async function () { - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[1].privateKey.slice(2); const consoleLog = spy(logger, "log"); await yargs([ "controller", "get", - "healthbot_31337_1", + tableName, "--privateKey", privateKey, "--chain", @@ -127,8 +131,28 @@ describe("commands/controller", function () { .parse(); const value = consoleLog.getCall(0).firstArg; - equal(value, "0x0000000000000000000000000000000000000000"); + equal(value, accounts[2].address); }); - // TODO: Create tests for locking a controller + test("passes when locking a controller", async function () { + const privateKey = accounts[1].privateKey.slice(2); + const consoleLog = spy(logger, "log"); + await yargs([ + "controller", + "lock", + tableName, + "--privateKey", + privateKey, + "--chain", + "local-tableland", + ]) + .command(mod) + .parse(); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + + equal(value.hash.startsWith("0x"), true); + equal(value.from, accounts[1].address); + }); }); diff --git a/test/create.test.ts b/test/create.test.ts index da76fc6..60a6062 100644 --- a/test/create.test.ts +++ b/test/create.test.ts @@ -48,39 +48,6 @@ describe("commands/create", function () { equal(value, "missing required flag (`-c` or `--chain`)"); }); - test("Create namespace with table using ENS", async () => { - const fullReolverStub = stub( - ethers.providers.JsonRpcProvider.prototype, - "getResolver" - ).callsFake(getResolverMock); - - const consoleLog = spy(logger, "log"); - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); - await yargs([ - "create", - "id integer, message text", - "hello", - "--chain", - "local-tableland", - "--privateKey", - privateKey, - "--ns", - "foo.bar.eth", - "--enableEnsExperiment", - "--ensProviderUrl", - "https://localhost:8080", - ]) - .command(mod) - .parse(); - - fullReolverStub.restore(); - - const res = consoleLog.getCall(1).firstArg; - const value = JSON.parse(res); - equal(value.ensNameRegistered, true); - }); - test("throws with invalid chain", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); @@ -192,6 +159,28 @@ describe("commands/create", function () { ); }); + test("throws if prefix not provided", async function () { + const [account] = accounts; + const privateKey = account.privateKey.slice(2); + const consoleError = spy(logger, "error"); + await yargs([ + "create", + "(id int primary key, desc text)", + "--privateKey", + privateKey, + "--chain", + "local-tableland", + ]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + equal( + value, + "Must specify --prefix if you do not provide a full Create statement" + ); + }); + test("Create passes with local-tableland", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); @@ -348,4 +337,61 @@ describe("commands/create", function () { equal(typeof transactionHash, "string"); equal(transactionHash.startsWith("0x"), true); }); + + test("Create namespace with table using ENS", async () => { + const fullReolverStub = stub( + ethers.providers.JsonRpcProvider.prototype, + "getResolver" + ).callsFake(getResolverMock); + + const consoleLog = spy(logger, "log"); + const [account] = getAccounts(); + const privateKey = account.privateKey.slice(2); + await yargs([ + "create", + "id integer, message text", + "hello", + "--chain", + "local-tableland", + "--privateKey", + privateKey, + "--ns", + "foo.bar.eth", + "--enableEnsExperiment", + "--ensProviderUrl", + "https://localhost:8080", + ]) + .command(mod) + .parse(); + + fullReolverStub.restore(); + + const res = consoleLog.getCall(1).firstArg; + const value = JSON.parse(res); + equal(value.ensNameRegistered, true); + }); + + test("create can accept custom providerUrl", async function () { + const [account] = accounts; + const privateKey = account.privateKey.slice(2); + const consoleError = spy(logger, "error"); + + await yargs([ + "create", + "id int primary key, name text", + "--prefix", + "custom_url_table", + "--chain", + "local-tableland", + "--privateKey", + privateKey, + "--providerUrl", + "http://localhost:9876", + ]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + equal(value, "cannot determine provider chain ID"); + }); }); diff --git a/test/namespace.test.ts b/test/namespace.test.ts index 4953c7f..94937aa 100644 --- a/test/namespace.test.ts +++ b/test/namespace.test.ts @@ -32,6 +32,81 @@ describe("commands/namespace", function () { restore(); }); + test("get fails if used without experiment flag", async function () { + const consoleLog = spy(logger, "log"); + await yargs([ + "namespace", + "get", + "foo.bar.eth", + "--ensProviderUrl", + "https://localhost:7070", + ]) + .command(mod) + .parse(); + + const value = consoleLog.getCall(0).firstArg; + equal( + value, + "To use ENS, ensure you have set the enableEnsExperiment flag to true" + ); + }); + + test("set fails if used without experiment flag", async function () { + const consoleLog = spy(logger, "log"); + await yargs([ + "namespace", + "set", + "foo.bar.eth", + "mytable=my_table_31337_4", + "--ensProviderUrl", + "https://localhost:7070", + ]) + .command(mod) + .parse(); + + const value = consoleLog.getCall(0).firstArg; + equal( + value, + "To use ENS, ensure you have set the enableEnsExperiment flag to true" + ); + }); + + test("fails if ens name is invalid", async function () { + const consoleError = spy(logger, "error"); + await yargs([ + "namespace", + "set", + "foo.bar.eth", + "invalid&ensname=my_table_31337_4", + "--enableEnsExperiment", + "--ensProviderUrl", + "https://localhost:7070", + ]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + equal(value, "Only letters or underscores in key name"); + }); + + test("fails if table name is invalid", async function () { + const consoleError = spy(logger, "error"); + await yargs([ + "namespace", + "set", + "foo.bar.eth", + "mytable=123-invalid_31337_4", + "--enableEnsExperiment", + "--ensProviderUrl", + "https://localhost:7070", + ]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + equal(value, "Tablename is invalid"); + }); + test("Get ENS name", async function () { const consoleLog = spy(logger, "log"); await yargs([ diff --git a/test/read.test.ts b/test/read.test.ts index aa388d2..b1ca1f2 100644 --- a/test/read.test.ts +++ b/test/read.test.ts @@ -4,6 +4,7 @@ import { spy, restore, stub } from "sinon"; import yargs from "yargs/yargs"; import { temporaryWrite } from "tempy"; import mockStd from "mock-stdin"; +import { getAccounts, getDatabase } from "@tableland/local"; import * as mod from "../src/commands/read.js"; import { wait, logger } from "../src/utils.js"; import { ethers } from "ethers"; @@ -11,15 +12,21 @@ import { getResolverMock } from "./mock.js"; describe("commands/read", function () { this.timeout(10000); + + const accounts = getAccounts(); + const db = getDatabase(accounts[1]); + before(async function () { await wait(5000); }); - afterEach(function () { + afterEach(async function () { restore(); + // ensure these tests don't hit rate limitting errors + await wait(500); }); - test("throws with invalid table name", async function () { + test("fails with invalid table name", async function () { const consoleError = spy(logger, "error"); const tableName = "something"; const statement = `select * from ${tableName};`; @@ -34,7 +41,7 @@ describe("commands/read", function () { ); }); - test("throws with invalid statement", async function () { + test("fails with invalid statement", async function () { const consoleError = spy(logger, "error"); await yargs(["read", "invalid;", "--baseUrl", "http://127.0.0.1:8080"]) .command(mod) @@ -47,7 +54,48 @@ describe("commands/read", function () { ); }); - test("throws with missing file", async function () { + test("fails when using unwrap without chainId", async function () { + const consoleError = spy(logger, "error"); + await yargs([ + "read", + "select counter from healthbot_31337_1 where counter = 1;", + "--unwrap", + ]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + equal(value, "Chain ID is required to use unwrap or extract"); + }); + + test("fails when using unwrap for multiple rows", async function () { + const { meta } = await db + .prepare("CREATE TABLE test_unwrap_multi (a int);") + .all(); + const tableName = meta.txn?.name ?? ""; + await db.batch([ + db.prepare(`INSERT INTO ${tableName} VALUES (1);`), + db.prepare(`INSERT INTO ${tableName} VALUES (2);`), + ]); + + const consoleError = spy(logger, "error"); + await yargs([ + "read", + `select * from ${tableName};`, + "--format", + "objects", + "--unwrap", + "--chain", + "local-tableland", + ]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + equal(value, "Can't unwrap multiple rows. Use --unwrap=false"); + }); + + test("fails with missing file", async function () { const consoleError = spy(logger, "error"); await yargs([ "read", @@ -63,7 +111,7 @@ describe("commands/read", function () { match(value, /^ENOENT: no such file or directory/i); }); - test("throws with empty stdin", async function () { + test("fails with empty stdin", async function () { const stdin = mockStd.stdin(); const consoleError = spy(logger, "error"); setTimeout(() => { @@ -143,6 +191,16 @@ describe("commands/read", function () { equal(value[0].counter, 1); }); + test("passes with alternate output format (raw)", async function () { + const consoleLog = spy(logger, "log"); + await yargs(["read", "select * from healthbot_31337_1;", "--format", "raw"]) + .command(mod) + .parse(); + + const value = consoleLog.getCall(0).firstArg; + equal(value, '{"columns":[{"name":"0"}],"rows":[[1]]}'); + }); + test("ENS experimental replaces shorthand with tablename", async function () { const fullReolverStub = stub( ethers.providers.JsonRpcProvider.prototype, @@ -233,4 +291,34 @@ describe("commands/read", function () { deepStrictEqual(value, [{ counter: 1 }]); }); + + test("passes with alternate output format (table)", async function () { + const consoleLog = spy(logger, "log"); + await yargs([ + "read", + "select * from healthbot_31337_1;", + "--format", + "table", + ]) + .command(mod) + .parse(); + + const value = consoleLog.getCall(0).firstArg; + deepStrictEqual(value, '{"columns":[{"name":"counter"}],"rows":[[1]]}'); + }); + + test("passes withoutput format (table) when results are empty", async function () { + const { meta } = await db + .prepare("CREATE TABLE empty_table (a int);") + .all(); + const tableName = meta.txn?.name ?? ""; + + const consoleLog = spy(logger, "log"); + await yargs(["read", `select * from ${tableName};`, "--format", "table"]) + .command(mod) + .parse(); + + const value = consoleLog.getCall(0).firstArg; + deepStrictEqual(value, '{"columns":[],"rows":[]}'); + }); }); diff --git a/test/setup.ts b/test/setup.ts index 58a6597..d34d78c 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,6 +1,16 @@ import { after, before } from "mocha"; import { LocalTableland } from "@tableland/local"; +const getTimeoutFactor = function (): number { + const envFactor = Number(process.env.TEST_TIMEOUT_FACTOR); + if (!isNaN(envFactor) && envFactor > 0) { + return envFactor; + } + return 1; +}; + +export const TEST_TIMEOUT_FACTOR = getTimeoutFactor(); + const lt = new LocalTableland({ silent: true }); before(async function () { diff --git a/test/shell.test.ts b/test/shell.test.ts index f842f54..7483ade 100644 --- a/test/shell.test.ts +++ b/test/shell.test.ts @@ -3,7 +3,7 @@ import { describe, test } from "mocha"; import { spy, restore, stub, assert } from "sinon"; import yargs from "yargs/yargs"; import mockStd from "mock-stdin"; -import { getAccounts } from "@tableland/local"; +import { getAccounts, getDatabase } from "@tableland/local"; import * as mod from "../src/commands/shell.js"; import { wait, logger } from "../src/utils.js"; import { ethers } from "ethers"; @@ -12,6 +12,9 @@ import { getResolverMock } from "./mock.js"; describe("commands/shell", function () { this.timeout("30s"); + const accounts = getAccounts(); + const db = getDatabase(accounts[1]); + before(async function () { await wait(10000); }); @@ -20,7 +23,19 @@ describe("commands/shell", function () { restore(); }); - test("Shell Works with single line", async function () { + test("fails without private key", async function () { + const consoleError = spy(logger, "error"); + + await yargs(["shell", "--chain", "local-tableland"]).command(mod).parse(); + + const value = consoleError.getCall(0).args[0]; + equal( + value, + "To send transactions, you need to specify a privateKey, providerUrl, and chain" + ); + }); + + test("works with single line", async function () { const consoleLog = spy(logger, "log"); const stdin = mockStd.stdin(); @@ -28,8 +43,7 @@ describe("commands/shell", function () { stdin.send("select * from healthbot_31337_1;\n").end(); }, 1000); - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[0].privateKey.slice(2); await yargs([ "shell", "--chain", @@ -59,8 +73,7 @@ describe("commands/shell", function () { stdin.send("select * from [foo.bar.eth];\n").end(); }, 2000); - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[0].privateKey.slice(2); await yargs([ "shell", "--chain", @@ -95,8 +108,7 @@ describe("commands/shell", function () { stdin.send("select * from `foo.bar.eth`;\n").end(); }, 2000); - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[0].privateKey.slice(2); await yargs([ "shell", "--chain", @@ -131,8 +143,7 @@ describe("commands/shell", function () { stdin.send(`select * from "foo.bar.eth";\n`).end(); }, 2000); - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[0].privateKey.slice(2); await yargs([ "shell", "--chain", @@ -156,8 +167,8 @@ describe("commands/shell", function () { test("Shell Works with initial input", async function () { const consoleLog = spy(logger, "log"); - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + + const privateKey = accounts[0].privateKey.slice(2); await yargs([ "shell", "--chain", @@ -183,8 +194,7 @@ describe("commands/shell", function () { stdin.send("select non_existent_table;\n").end(); }, 1000); - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[0].privateKey.slice(2); await yargs([ "shell", "--chain", @@ -212,8 +222,7 @@ describe("commands/shell", function () { }, 500); }, 1000); - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[0].privateKey.slice(2); await yargs([ "shell", "--chain", @@ -236,14 +245,13 @@ describe("commands/shell", function () { const stdin = mockStd.stdin(); setTimeout(() => { - stdin.send("CREATE TABLE SomeTable (id integer, message text);\n"); + stdin.send("UPDATE SomeTable SET message = 'yay' WHERE id = 1;\n"); setTimeout(() => { stdin.send("n\n"); }, 500); }, 1000); - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[0].privateKey.slice(2); await yargs([ "shell", "--chain", @@ -260,8 +268,7 @@ describe("commands/shell", function () { }); test("Shell throws without chain", async function () { - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[0].privateKey.slice(2); const consoleError = spy(logger, "error"); await yargs(["shell", "--privateKey", privateKey]).command(mod).parse(); @@ -270,8 +277,7 @@ describe("commands/shell", function () { }); test("Shell throws with invalid chain", async function () { - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[0].privateKey.slice(2); const consoleError = spy(logger, "error"); await yargs(["shell", "--privateKey", privateKey, "--chain", "foozbazz"]) .command(mod) @@ -289,8 +295,7 @@ describe("commands/shell", function () { stdin.send("select * from\n").send("healthbot_31337_1;\n").end(); }, 1000); - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[0].privateKey.slice(2); await yargs([ "shell", "--chain", @@ -310,22 +315,14 @@ describe("commands/shell", function () { }); test(".exit exits the shell", async function () { - const ogExit = process.exit; - - // @ts-ignore - process.exit = function (code: any) { - console.log("Skiped process.exit in exit test"); - }; - const stdin = mockStd.stdin(); - const exit = spy(process, "exit"); + const exit = stub(process, "exit"); setTimeout(() => { stdin.send(".exit\n").end(); }, 1000); - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[0].privateKey.slice(2); await yargs([ "shell", "--chain", @@ -336,8 +333,40 @@ describe("commands/shell", function () { .command(mod) .parse(); assert.called(exit); + exit.restore(); + }); + + test("Shell Works with write statement", async function () { + const { meta } = await db + .prepare("CREATE TABLE shell_write (a int);") + .all(); + const tableName = meta.txn?.name ?? ""; + + const consoleLog = spy(logger, "log"); + const stdin = mockStd.stdin(); + + setTimeout(() => { + stdin.send(`INSERT INTO ${tableName} VALUES (1);\n`); + setTimeout(() => { + stdin.send("y\n"); + }, 500); + }, 1000); - process.exit = ogExit; + const privateKey = accounts[1].privateKey.slice(2); + await yargs([ + "shell", + "--chain", + "local-tableland", + "--format", + "objects", + "--privateKey", + privateKey, + ]) + .command(mod) + .parse(); + + const value = consoleLog.getCall(3).args[0]; + equal(value, "[]"); }); test("Shell Works with multi-line", async function () { @@ -348,8 +377,7 @@ describe("commands/shell", function () { stdin.send("select * from\n").send("healthbot_31337_1;\n").end(); }, 1000); - const [account] = getAccounts(); - const privateKey = account.privateKey.slice(2); + const privateKey = accounts[0].privateKey.slice(2); await yargs([ "shell", "--chain", @@ -365,4 +393,37 @@ describe("commands/shell", function () { const value = consoleLog.getCall(3).args[0]; equal(value, '[{"counter":1}]'); }); + + test("Shell can print help statement", async function () { + const consoleLog = spy(logger, "log"); + const stdin = mockStd.stdin(); + + setTimeout(() => { + stdin.send(`.help\n`); + }, 1000); + + const privateKey = accounts[1].privateKey.slice(2); + await yargs([ + "shell", + "--chain", + "local-tableland", + "--format", + "objects", + "--privateKey", + privateKey, + ]) + .command(mod) + .parse(); + + const value = consoleLog.getCall(3).args[0]; + equal( + value, + `Commands: +[query] - run a query +.exit - exit the shell +.help - show this help + +SQL Queries can be multi-line, and must end with a semicolon (;).` + ); + }); }); diff --git a/test/transfer.test.ts b/test/transfer.test.ts index 697720f..535482b 100644 --- a/test/transfer.test.ts +++ b/test/transfer.test.ts @@ -10,9 +10,10 @@ import { helpers } from "@tableland/sdk"; describe("commands/transfer", function () { this.timeout("30s"); + // account[0] is the Validator's wallet, try to avoid using that const accounts = getAccounts(); - // account 0 is the Validator's wallet, try to avoid using that const db = getDatabase(accounts[1]); + let tableName: string; before(async function () { await wait(500); diff --git a/test/write.test.ts b/test/write.test.ts index d40f2a0..df9b4f1 100644 --- a/test/write.test.ts +++ b/test/write.test.ts @@ -113,6 +113,27 @@ describe("commands/write", function () { ); }); + test("throws when used with create statement", async function () { + const [account] = accounts; + const privateKey = account.privateKey.slice(2); + const consoleError = spy(logger, "error"); + await yargs([ + "write", + "create table fooz (a int);", + "--chain", + "local-tableland", + "--prefix", + "cooltable", + "--privateKey", + privateKey, + ]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + equal(value, "the `write` command can only accept write queries"); + }); + test("throws with missing file", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); From 011ba0964304c16afa9b6810dc7a5d795555a1f1 Mon Sep 17 00:00:00 2001 From: Joe Wagner Date: Thu, 1 Jun 2023 17:09:06 -0600 Subject: [PATCH 3/4] Show local tableland logs --- test/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/setup.ts b/test/setup.ts index d34d78c..c7e4a5d 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -11,7 +11,7 @@ const getTimeoutFactor = function (): number { export const TEST_TIMEOUT_FACTOR = getTimeoutFactor(); -const lt = new LocalTableland({ silent: true }); +const lt = new LocalTableland({ silent: false }); before(async function () { this.timeout(30000); From 2a553e67fbc4a870e76979e4430749cab26c034d Mon Sep 17 00:00:00 2001 From: Joe Wagner Date: Thu, 8 Jun 2023 09:54:11 -0600 Subject: [PATCH 4/4] coverage tests all pass --- package.json | 1 + src/commands/create.ts | 3 +++ src/commands/namespace.ts | 1 + src/commands/write.ts | 5 +++-- test/create.test.ts | 2 +- test/mock.ts | 4 ++++ test/namespace.test.ts | 10 +++++---- test/write.test.ts | 43 ++++++++++++++++++++++++++++++++++++++- 8 files changed, 61 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 84bf44e..2243239 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "prepublishOnly": "npm run build", "test": "mocha", "coverage": "TEST_TIMEOUT_FACTOR=3 c8 --100 --exclude test mocha --exit", + "coverage:report": "c8 report --reporter=html", "clean": "rm -rf dist", "build": "npx tsc && chmod +x dist/cli.js", "tableland": "node --experimental-specifier-resolution=node ./dist/cli.js" diff --git a/src/commands/create.ts b/src/commands/create.ts index 6d50d49..ea7c084 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -135,6 +135,9 @@ export const handler = async (argv: Arguments): Promise => { const link = getLink(chain, res.meta.txn?.transactionHash as string); const out = { ...res, link, ensNameRegistered: false }; + // TODO: I'm not sure how `check` would be false and statements.length < 2 + // so I didn't write a test for it + /* c8 ignore next 6 */ if (!check && argv.ns && argv.enableEnsExperiment && prefix) { const register = (await ens?.addTableRecords(argv.ns, [ { key: prefix, value: out.meta.txn?.name as string }, diff --git a/src/commands/namespace.ts b/src/commands/namespace.ts index 2d995bf..e62ce66 100644 --- a/src/commands/namespace.ts +++ b/src/commands/namespace.ts @@ -96,4 +96,5 @@ export const builder: CommandBuilder<{}, Options> = (yargs) => ) .usage(``) as yargs.Argv; +/* c8 ignore next */ export const handler = async (argv: Arguments): Promise => {}; diff --git a/src/commands/write.ts b/src/commands/write.ts index 3cf35ef..3dd2edd 100644 --- a/src/commands/write.ts +++ b/src/commands/write.ts @@ -73,8 +73,8 @@ export const handler = async (argv: Arguments): Promise => { } // Note: I can't figure out a write statement that updates 2 tables and makes // it through the parser, but leaving this here because one might exist. + /* c8 ignore next 6 */ if (normalized.tables.length < 1) { - /* c8 ignore next 5 */ logger.error( "after normalizing the statement there was no write query, hence nothing to do" ); @@ -97,8 +97,8 @@ export const handler = async (argv: Arguments): Promise => { normalized.statements.map(async function (stmt) { // re-normalize so we can be sure we've isolated each statement and it's tableId const norm = await normalize(stmt); + /* c8 ignore next 5 */ if (norm.tables.length > 1) { - /* c8 ignore next 4 */ throw new Error( "cannot normalize if single query affects more then one table" ); @@ -126,6 +126,7 @@ export const handler = async (argv: Arguments): Promise => { id, stmt, ]) { + /* c8 ignore next 1 */ if (typeof stmt !== "string") throw new Error("cannot prepare statement"); return db.prepare(stmt); }); diff --git a/test/create.test.ts b/test/create.test.ts index 60a6062..1adbdf3 100644 --- a/test/create.test.ts +++ b/test/create.test.ts @@ -338,7 +338,7 @@ describe("commands/create", function () { equal(transactionHash.startsWith("0x"), true); }); - test("Create namespace with table using ENS", async () => { + test("create namespace with table using ENS", async () => { const fullReolverStub = stub( ethers.providers.JsonRpcProvider.prototype, "getResolver" diff --git a/test/mock.ts b/test/mock.ts index 2ee5576..37054cf 100644 --- a/test/mock.ts +++ b/test/mock.ts @@ -3,3 +3,7 @@ export const getResolverMock = async (): Promise => { getText: async () => "healthbot_31337_1", }; }; + +export const getResolverUndefinedMock = async (): Promise => { + return undefined; +}; diff --git a/test/namespace.test.ts b/test/namespace.test.ts index 94937aa..a081745 100644 --- a/test/namespace.test.ts +++ b/test/namespace.test.ts @@ -22,10 +22,6 @@ describe("commands/namespace", function () { }, }; }); - - stub(ethers.providers.JsonRpcProvider.prototype, "getResolver") - // @ts-ignore - .callsFake(getResolverMock); }); afterEach(function () { @@ -108,6 +104,12 @@ describe("commands/namespace", function () { }); test("Get ENS name", async function () { + stub( + ethers.providers.JsonRpcProvider.prototype, + "getResolver" + // @ts-ignore + ).callsFake(getResolverMock); + const consoleLog = spy(logger, "log"); await yargs([ "namespace", diff --git a/test/write.test.ts b/test/write.test.ts index df9b4f1..18b021e 100644 --- a/test/write.test.ts +++ b/test/write.test.ts @@ -1,6 +1,8 @@ import { equal, match } from "node:assert"; import { describe, test, afterEach, before } from "mocha"; -import { spy, restore } from "sinon"; +import { spy, stub, restore } from "sinon"; +import { ethers } from "ethers"; +import { getResolverUndefinedMock } from "./mock.js"; import yargs from "yargs/yargs"; import { temporaryWrite } from "tempy"; import mockStd from "mock-stdin"; @@ -305,4 +307,43 @@ describe("commands/write", function () { equal(transactionHash.startsWith("0x"), true); equal(!link, true); }); + + test("resolves table name to literal name if ens is not set", async function () { + const resolverMock = stub( + ethers.providers.JsonRpcProvider.prototype, + "getResolver" + // @ts-ignore + ).callsFake(getResolverUndefinedMock); + + const { meta } = await db.prepare("CREATE TABLE ens_write (a int);").all(); + const tableName = meta.txn?.name ?? ""; + + const account = accounts[1]; + const privateKey = account.privateKey.slice(2); + const consoleLog = spy(logger, "log"); + + await yargs([ + "write", + `insert into ${tableName} (a) values (1);`, + "--chain", + "local-tableland", + "--privateKey", + privateKey, + "--enableEnsExperiment", + "--ensProviderUrl", + "https://localhost:7070", + ]) + .command(mod) + .parse(); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + const { transactionHash, link } = value.meta?.txn; + + equal(typeof transactionHash, "string"); + equal(transactionHash.startsWith("0x"), true); + equal(!link, true); + + equal(resolverMock.calledOnce, true); + }); });