Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add code testing #585

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
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
84 changes: 84 additions & 0 deletions .github/workflows/code:test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Code Snippets testing

on:
pull_request:
branches: [main]
paths:
- "code/**/*.test.ts" # Only trigger on test file changes
schedule:
- cron: "0 0 * * *" # Run daily at midnight UTC

jobs:
snippets-tests:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [20]
fail-fast: false

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Get changed test files
id: changed-files
uses: tj-actions/changed-files@v39
with:
files: |
code/**/*.test.ts

- name: Install pnpm
uses: pnpm/action-setup@v4

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"

- name: Install dependencies
run: pnpm install

- name: Change Directory
run: cd code

- name: Run tests on changed files
if: github.event_name == 'pull_request'
run: |
CHANGED_FILES="${{ steps.changed-files.outputs.all_changed_files }}"

if [ -z "$CHANGED_FILES" ]; then
echo "No test files were changed"
exit 0
fi

# Create array for test files
declare -a test_files=()

# Collect only .test.ts files
for file in $CHANGED_FILES; do
if [[ $file == *.test.ts ]]; then
test_files+=("$file")
echo "Added test file: $file"
fi
done

# If we found test files, run them together
if [ ${#test_files[@]} -gt 0 ]; then
echo "Running tests for the following files:"
printf '%s\n' "${test_files[@]}"

# Run all test files in a single command
node --import tsx --test "${test_files[@]}" || exit 1
else
echo "No test files to run"
fi

- name: Run all tests
if: github.event_name == 'schedule'
run: |
echo "Running all tests in code directory"
pnpm turbo test
2 changes: 0 additions & 2 deletions .github/workflows/contentlayer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ jobs:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ jobs:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Install Vercel CLI and pnpm
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/formatting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ jobs:
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ package-lock.json
# translations are stored in the `i18n` via crowdin
i18n

# turborepo
.turbo

# code-import
code/node_modules
Expand Down
70 changes: 67 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ transparent as possible, whether it's:
- content translations are supported via Crowdin
- code blocks must use code-import for file snippets (via filesystem)
- code file should be [tests](https://nodejs.org/api/test.html) and should add
code ranges instead of whole test file
code ranges instead of whole file

## Style guidelines

Expand Down Expand Up @@ -285,8 +285,8 @@ source files.

To use code-import, follow these steps:

Ensure your code file is a test file located in the appropriate directory within
the repo. Use the following syntax to import code snippets:
Ensure your code file is located in the appropriate directory within the repo.
Use the following syntax to import code snippets:

```javascript file="/path/to/your/file.ts#L1-L10,#L15-L20"

Expand Down Expand Up @@ -1012,3 +1012,67 @@ pnpm dev
```

> Note: The developer content API normally runs locally on port `3001`

## Testing

The repository uses Node's native test runner for testing code examples. This
ensures all code snippets are correct and working as expected.

### Test Structure

There are two supported ways to structure your tests:

1. **Adjacent test files** - For simpler modules with a single code file:

```
pda/
├── index.ts
└── index.test.ts
```

2. **Tests directory** - For modules with multiple code files or complex test
cases:
```
pda/
├── index.ts
├── utils.ts
└── tests/
├── index.test.ts
└── utils.test.ts
```

Choose the structure that best fits your needs:

- Use adjacent test files (`index.test.ts`) when you have a simple 1:1
relationship between code and test files
- Use a `tests` directory when you have multiple code files that need testing or
need multiple test files for a single code file

### Running Tests

You can run tests in two ways:

1. Test the entire codebase:

```shell
pnpm install
pnpm turbo test
```

2. Test specific code imports:
```shell
cd code
pnpm install
pnpm turbo test
```

### Writing Tests

We use [Behavior-Driven Development (BDD)](https://cucumber.io/docs/bdd/) style
testing, which uses `describe` and `it` blocks to create readable test
descriptions. When tests run, they read like natural sentences.

Example:
[generate-mnemonic.test.ts](https://github.com/solana-foundation/developer-content/blob/master/code/content/cookbook/wallets/tests/generate-mnemonic.test.ts)

All tests should handle both success and failure cases appropriately.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PublicKey } from "@solana/web3.js";
import { PublicKey, Keypair } from "@solana/web3.js";

// Note that Keypair.generate() will always give a public key that is valid for users

Expand All @@ -15,5 +15,14 @@ const offCurveAddress = new PublicKey(
// Not on the ed25519 curve, therefore not suitable for users
console.log(PublicKey.isOnCurve(offCurveAddress.toBytes()));

// Not a valid public key
const errorPubkey = new PublicKey("testPubkey");
let errorPubkey;
try {
// Not a valid public key
errorPubkey = new PublicKey("testPubkey");
} catch (err) {
// Error will be caught here
}

const onCurve = PublicKey.isOnCurve(key.toBytes());

export { key, onCurve, offCurveAddress, errorPubkey };
5 changes: 5 additions & 0 deletions code/content/cookbook/wallets/create-keypair.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Keypair } from "@solana/web3.js";

const keypair = Keypair.generate();

export { keypair };
5 changes: 5 additions & 0 deletions code/content/cookbook/wallets/generate-mnemonic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as bip39 from "bip39";

const mnemonic = bip39.generateMnemonic();

export { mnemonic };
1 change: 1 addition & 0 deletions code/content/cookbook/wallets/generate-vanity-address.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
solana-keygen grind --starts-with e1v1s:1
15 changes: 15 additions & 0 deletions code/content/cookbook/wallets/restore-bip39-mnemonic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Keypair } from "@solana/web3.js";
import * as bip39 from "bip39";

const mnemonic =
"pill tomorrow foster begin walnut borrow virtual kick shift mutual shoe scatter";

// arguments: (mnemonic, password)
const seed = bip39.mnemonicToSeedSync(mnemonic, "");
const keypair = Keypair.fromSeed(seed.slice(0, 32));

console.log(`${keypair.publicKey.toBase58()}`);

// output: 5ZWj7a1f8tWkjBESHKgrLmXshuXxqeY9SYcfbshpAqPG

export { keypair };
29 changes: 29 additions & 0 deletions code/content/cookbook/wallets/restore-bip44-mnemonic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Keypair } from "@solana/web3.js";
import { HDKey } from "micro-key-producer/slip10.js";
import * as bip39 from "bip39";

type Wallet = {
path: string;
keypair: Keypair;
publicKey: string;
};

const mnemonic =
"neither lonely flavor argue grass remind eye tag avocado spot unusual intact";

const seed = bip39.mnemonicToSeedSync(mnemonic, "");
const hd = HDKey.fromMasterSeed(seed.toString("hex"));

const wallets: Wallet[] = [];

for (let i = 0; i < 10; i++) {
const path = `m/44'/501'/${i}'/0'`;
const keypair = Keypair.fromSeed(hd.derive(path).privateKey);
wallets.push({
path,
keypair,
publicKey: keypair.publicKey.toBase58(),
});
}

export { mnemonic, wallets };
10 changes: 10 additions & 0 deletions code/content/cookbook/wallets/restore-keypair-from-bs58.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Keypair } from "@solana/web3.js";
import bs58 from "bs58";

const keypair = Keypair.fromSecretKey(
bs58.decode(
"4UzFMkVbk1q6ApxvDS8inUxg4cMBxCQRVXRx5msqQyktbi1QkJkt574Jda6BjZThSJi54CHfVoLFdVFX8XFn233L",
),
);

export { keypair as bs58Keypair };
12 changes: 12 additions & 0 deletions code/content/cookbook/wallets/restore-keypair-from-bytes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Keypair } from "@solana/web3.js";

const keypair = Keypair.fromSecretKey(
Uint8Array.from([
174, 47, 154, 16, 202, 193, 206, 113, 199, 190, 53, 133, 169, 175, 31, 56,
222, 53, 138, 189, 224, 216, 117, 173, 10, 149, 53, 45, 73, 251, 237, 246,
15, 185, 186, 82, 177, 240, 148, 69, 241, 227, 167, 80, 141, 89, 240, 121,
121, 35, 172, 247, 68, 251, 226, 218, 48, 63, 176, 109, 168, 89, 238, 135,
]),
);

export { keypair as bytesKeypair };
27 changes: 27 additions & 0 deletions code/content/cookbook/wallets/sign-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Keypair } from "@solana/web3.js";
import * as ed from "@noble/ed25519";
import { sha512 } from "@noble/hashes/sha512";
import { utf8ToBytes } from "@noble/hashes/utils";

// Enable synchronous methods for noble-ed25519
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));

const keypair = Keypair.fromSecretKey(
Uint8Array.from([
174, 47, 154, 16, 202, 193, 206, 113, 199, 190, 53, 133, 169, 175, 31, 56,
222, 53, 138, 189, 224, 216, 117, 173, 10, 149, 53, 45, 73, 251, 237, 246,
15, 185, 186, 82, 177, 240, 148, 69, 241, 227, 167, 80, 141, 89, 240, 121,
121, 35, 172, 247, 68, 251, 226, 218, 48, 63, 176, 109, 168, 89, 238, 135,
]),
);

const message = "The quick brown fox jumps over the lazy dog";
const messageBytes = utf8ToBytes(message);

// Sign using noble-ed25519
const signature = ed.sign(messageBytes, keypair.secretKey.slice(0, 32));

// Verify using noble-ed25519
const result = ed.verify(signature, messageBytes, keypair.publicKey.toBytes());

export { keypair, message, messageBytes, signature, result };
Loading
Loading