Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/testing/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
1 change: 1 addition & 0 deletions packages/testing/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
engine-strict=true
22 changes: 22 additions & 0 deletions packages/testing/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
node_modules
pnpm-lock.yaml

LICENSE
.changeset/
.github/PULL_REQUEST_TEMPLATE.md

declarations/
dist/
doc/
lib/
kit/

.docs
.turbo
.next
.vercel

**/generated/
**/generated/**
generated/
generated/**
78 changes: 78 additions & 0 deletions packages/testing/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"name": "@gillsdk/testing",
"license": "MIT",
"version": "0.0.1",
"description": "A modern JavaScript/TypeScript testing library for Solana blockchain development",
"scripts": {
"clean": "rimraf coverage dist build node_modules .turbo .docs",
"prebuild": "rimraf dist",
"compile:js": "tsup --config ./tsup.config.package.ts",
"compile:typedefs": "tsc -p ./tsconfig.declarations.json",
"prepublishOnly": "pnpm pkg delete devDependencies",
"publish-impl": "npm view $npm_package_name@$npm_package_version > /dev/null 2>&1 || (pnpm publish --tag ${PUBLISH_TAG:-canary} --access public --no-git-checks && (([ \"$PUBLISH_TAG\" != \"canary\" ] && pnpm dist-tag add $npm_package_name@$npm_package_version latest) || true))",
"publish-packages": "pnpm prepublishOnly && pnpm publish-impl",
"coverage": "pnpm test:unit:node --coverage",
"coverage:open": "export BROWSER=brave && xdg-open ./coverage/lcov-report/index.html > /dev/null",
"test:typecheck": "tsc --noEmit",
"test:unit:node": "TERM_OVERRIDE=\"${TURBO_HASH:+dumb}\" TERM=${TERM_OVERRIDE:-$TERM} jest -c ../../packages/test-config/jest-unit.config.node.ts --rootDir . --silent",
"test:unit:browser": "TERM_OVERRIDE=\"${TURBO_HASH:+dumb}\" TERM=${TERM_OVERRIDE:-$TERM} jest -c ../../packages/test-config/jest-unit.config.browser.ts --rootDir . --silent",
"test:treeshakability:browser": "agadoo dist/index.browser.mjs",
"test:treeshakability:native": "agadoo dist/index.native.mjs",
"test:treeshakability:node": "agadoo dist/index.node.mjs && agadoo dist/node/index.node.mjs",
"style:check": "prettier --check '{*,**/*}.{ts,tsx,js,jsx,css,json,md,mdx}'",
"style:fix": "pnpm style:check --write"
},
"exports": {
"types": "./dist/index.d.ts",
"import": "./dist/index.node.mjs",
"require": "./dist/index.node.cjs",
"default": "./dist/index.node.cjs"
},
"browser": {
"./dist/index.node.cjs": "./dist/index.browser.cjs",
"./dist/index.node.mjs": "./dist/index.browser.mjs"
},
"main": "./dist/index.node.cjs",
"module": "./dist/index.node.mjs",
"types": "./dist/index.d.ts",
"files": [
"./dist/"
],
"sideEffects": false,
"keywords": [
"blockchain",
"solana",
"web3",
"web3js v2",
"solana kit",
"solana helpers",
"@solana/web3.js",
"@solana/kit",
"testing",
"fixtures",
"solana-testing",
"web3-testing"
],
"author": "Nick Frostbutter <maintainers@gillsdk.com>",
"homepage": "https://gillsdk.com",
"repository": {
"type": "git",
"url": "https://github.com/gillsdk/gill.git"
},
"bugs": {
"url": "https://github.com/gillsdk/gill/issues"
},
"browserslist": [
"supports bigint and not dead",
"maintained node versions"
],
"dependencies": {
"gill": "workspace:*"
},
"peerDependencies": {
"typescript": ">=5"
},
"engines": {
"node": ">=20.18.0"
}
}
87 changes: 87 additions & 0 deletions packages/testing/src/__tests__/createAndFundedKeypair.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { createAndFundedKeypair } from "../fixtures/createAndFundedKeypair";
import { airdropFactory, generateKeyPairSigner, lamports } from "gill";

jest.mock("gill", () => {
const actual = jest.requireActual("gill");
return {
...actual,
airdropFactory: jest.fn(),
generateKeyPairSigner: jest.fn(),
};
});

describe("createAndFundedKeypair", () => {
let mockRpc: any;
let mockRpcSubscriptions: any;
let mockAirdrop: jest.Mock;

const DEFAULT_LAMPORTS = 10_000_000_000n;

beforeEach(() => {
jest.clearAllMocks();

mockAirdrop = jest.fn().mockResolvedValue("mock-signature-abc123");
(airdropFactory as jest.Mock).mockReturnValue(mockAirdrop);

(generateKeyPairSigner as jest.Mock).mockResolvedValue({ address: "mock-address" });

mockRpc = {
getBalance: jest.fn().mockReturnValue({
send: jest.fn().mockResolvedValue({ value: lamports(DEFAULT_LAMPORTS) }),
}),
};

mockRpcSubscriptions = {};
});

it("should create and fund a new keypair with default amount", async () => {
const result = await createAndFundedKeypair(mockRpc, mockRpcSubscriptions);

expect(result.fundedKeypair.address).toBe("mock-address");
expect(result.transactionSignature).toBe("mock-signature-abc123");
expect(result.balance).toBe(lamports(DEFAULT_LAMPORTS));

expect(mockAirdrop).toHaveBeenCalledWith({
commitment: "confirmed",
lamports: DEFAULT_LAMPORTS,
recipientAddress: "mock-address",
});
expect(mockRpc.getBalance).toHaveBeenCalledWith("mock-address");
});

it("should create and fund a new keypair with custom amount", async () => {
const customAmount = 5_000_000_000n;
mockRpc.getBalance = jest.fn().mockReturnValue({
send: jest.fn().mockResolvedValue({ value: lamports(customAmount) }),
});

const result = await createAndFundedKeypair(mockRpc, mockRpcSubscriptions, lamports(customAmount));

expect(result.balance).toBe(lamports(customAmount));
expect(mockAirdrop).toHaveBeenCalledWith({
commitment: "confirmed",
lamports: customAmount,
recipientAddress: "mock-address",
});
});

it("should throw an error if lamports amount is zero", async () => {
await expect(createAndFundedKeypair(mockRpc, mockRpcSubscriptions, lamports(0n))).rejects.toThrow(
"Airdrop amount must be greater than zero lamports",
);
});

it("should throw an error if airdrop fails", async () => {
mockAirdrop.mockRejectedValue(new Error("RPC error: Airdrop failed"));

await expect(createAndFundedKeypair(mockRpc, mockRpcSubscriptions)).rejects.toThrow("RPC error: Airdrop failed");
});

it("should throw an error if balance fetch fails", async () => {
mockRpc.getBalance = jest.fn().mockReturnValue({
send: jest.fn().mockRejectedValue(new Error("RPC error: getBalance failed")),
});

await expect(createAndFundedKeypair(mockRpc, mockRpcSubscriptions)).rejects.toThrow("RPC error: getBalance failed");
});
});
140 changes: 140 additions & 0 deletions packages/testing/src/__tests__/createMint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import {
createTransaction,
generateKeyPairSigner,
getMinimumBalanceForRentExemption,
signTransactionMessageWithSigners,
type SolanaClient,
} from "gill";
import { loadKeypairSignerFromFile } from "gill/node";
import { getCreateAccountInstruction, getInitializeMintInstruction, getMintSize } from "gill/programs";
import { createMint } from "../fixtures/createMint";

import {
MOCK_CUSTOM_PAYER,
MOCK_MINT_SIGNER,
MOCK_MINT_SPACE,
MOCK_PAYER,
MOCK_RENT_EXEMPT_LAMPORTS,
MOCK_TOKEN_PROGRAM_ADDRESS,
MOCK_TRANSACTION_SIGNATURE,
getMockRpc,
setupCommonFixtureMocks,
type CommonMocks,
} from "../helpers/common_setup";

jest.mock("gill", () => ({
...jest.requireActual("gill"),
generateKeyPairSigner: jest.fn(),
getMinimumBalanceForRentExemption: jest.fn(),
createTransaction: jest.fn(),
signTransactionMessageWithSigners: jest.fn(),
}));

jest.mock("gill/node", () => ({
loadKeypairSignerFromFile: jest.fn(),
}));

jest.mock("gill/programs", () => {
const programs = jest.requireActual("gill/programs");
return {
...programs,
getCreateAccountInstruction: jest.fn(),
getInitializeMintInstruction: jest.fn(),
getMintSize: jest.fn(),
TOKEN_PROGRAM_ADDRESS: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
};
});

describe("createMint", () => {
let mockRpc: SolanaClient["rpc"];
let mockSendAndConfirmTransaction: jest.Mock;
const mockTransactionSignature = MOCK_TRANSACTION_SIGNATURE;
const mockSpace = MOCK_MINT_SPACE;
const mockLamports = MOCK_RENT_EXEMPT_LAMPORTS;
const commonMocks: CommonMocks = {
loadKeypairSignerFromFile: loadKeypairSignerFromFile as jest.Mock,
generateKeyPairSigner: generateKeyPairSigner as jest.Mock,
createTransaction: createTransaction as jest.Mock,
signTransactionMessageWithSigners: signTransactionMessageWithSigners as jest.Mock,
};

beforeEach(() => {
mockSendAndConfirmTransaction = jest.fn().mockResolvedValue(mockTransactionSignature);
mockRpc = getMockRpc(mockSendAndConfirmTransaction);

setupCommonFixtureMocks(commonMocks, MOCK_PAYER, MOCK_MINT_SIGNER);

(getMintSize as jest.Mock).mockReturnValue(mockSpace);
(getMinimumBalanceForRentExemption as jest.Mock).mockReturnValue(mockLamports);
(getCreateAccountInstruction as jest.Mock).mockReturnValue({ instruction: "mockCreateAccountInstruction" });
(getInitializeMintInstruction as jest.Mock).mockReturnValue({ instruction: "mockInitializeMintInstruction" });
});

afterEach(() => jest.clearAllMocks());

it("creates a mint with default parameters", async () => {
const result = await createMint(mockRpc, mockSendAndConfirmTransaction);

expect(result).toEqual({
mint: MOCK_MINT_SIGNER,
transactionSignature: mockTransactionSignature,
});

expect(getInitializeMintInstruction).toHaveBeenCalledWith(
{
mint: MOCK_MINT_SIGNER.address,
mintAuthority: MOCK_PAYER.address,
freezeAuthority: MOCK_PAYER.address,
decimals: 9,
},
{ programAddress: MOCK_TOKEN_PROGRAM_ADDRESS },
);
});

it("creates a mint with custom payer and decimals", async () => {
const result = await createMint(mockRpc, mockSendAndConfirmTransaction, {
payer: MOCK_CUSTOM_PAYER,
decimals: 4,
});

expect(result).toEqual({
mint: MOCK_MINT_SIGNER,
transactionSignature: mockTransactionSignature,
});

expect(getCreateAccountInstruction).toHaveBeenCalledWith(expect.objectContaining({ payer: MOCK_CUSTOM_PAYER }));

expect(getInitializeMintInstruction).toHaveBeenCalledWith(
{
mint: MOCK_MINT_SIGNER.address,
mintAuthority: MOCK_CUSTOM_PAYER.address,
freezeAuthority: MOCK_CUSTOM_PAYER.address,
decimals: 4,
},
{ programAddress: MOCK_TOKEN_PROGRAM_ADDRESS },
);
});

it("throws error for invalid decimals (<0 or >9)", async () => {
await expect(createMint(mockRpc, mockSendAndConfirmTransaction, { decimals: -1 })).rejects.toThrow(
"Invalid decimals value: -1. Must be between 0 and 9.",
);

await expect(createMint(mockRpc, mockSendAndConfirmTransaction, { decimals: 10 })).rejects.toThrow(
"Invalid decimals value: 10. Must be between 0 and 9.",
);
});

it("throws if RPC call fails", async () => {
mockRpc.getLatestBlockhash = jest
.fn()
.mockReturnValue({ send: jest.fn().mockRejectedValue(new Error("RPC failed")) });

await expect(createMint(mockRpc, mockSendAndConfirmTransaction)).rejects.toThrow("RPC failed");
});

it("throws if sendAndConfirmTransaction fails", async () => {
mockSendAndConfirmTransaction.mockRejectedValue(new Error("Transaction failed"));
await expect(createMint(mockRpc, mockSendAndConfirmTransaction)).rejects.toThrow("Transaction failed");
});
});
Loading