Testing
Test applications that use @cipherstash/stack encryption
Testing
This guide covers strategies for testing applications that use CipherStash encryption.
Test environment setup
CipherStash encryption requires valid credentials to derive keys via ZeroKMS. For testing, you have two options:
Option 1: Use a dedicated test workspace (recommended)
Create a separate CipherStash workspace for testing. This gives you real encryption behavior with isolated keys that don't affect production.
CS_WORKSPACE_CRN=crn:ap-southeast-2.aws:your-test-workspace-id
CS_CLIENT_ID=your-test-client-id
CS_CLIENT_KEY=your-test-client-key
CS_CLIENT_ACCESS_KEY=your-test-access-keyThis approach tests the full encryption path including ZeroKMS key derivation.
Option 2: Mock the encryption client
For unit tests where you don't need real encryption, mock the client to return predictable values:
import type { InferEncrypted } from "@cipherstash/stack/schema"
export function createMockClient() {
return {
encrypt: async (plaintext: unknown) => ({
data: { __mock: true, plaintext },
}),
decrypt: async (encrypted: unknown) => ({
data: (encrypted as any).plaintext,
}),
encryptModel: async (model: unknown) => ({
data: model,
}),
decryptModel: async (model: unknown) => ({
data: model,
}),
bulkEncrypt: async (items: any[]) => ({
data: items.map(i => ({ id: i.id, data: { __mock: true, plaintext: i.plaintext } })),
}),
bulkDecrypt: async (items: any[]) => ({
data: items.map(i => ({ id: i.id, data: i.plaintext })),
}),
}
}Mocking bypasses all encryption. Use this for testing business logic, not for validating that encryption works correctly. Always run integration tests with a real workspace.
Integration tests with PostgreSQL
For integration tests that verify searchable encryption queries, you need:
- A PostgreSQL database with EQL installed (see CipherStash Forge)
- A test CipherStash workspace
- Your schema definitions
import { Encryption } from "@cipherstash/stack"
import { users } from "../src/schema"
import { Pool } from "pg"
let client: Awaited<ReturnType<typeof Encryption>>
let pool: Pool
beforeAll(async () => {
pool = new Pool({ connectionString: process.env.TEST_DATABASE_URL })
// EQL must be installed before running tests.
// Run `npx stash-forge setup` (or `npx stash-forge install`) against your test database first.
// See: /stack/encryption/forge
// Initialize encryption client with test credentials
// Encryption() throws on failure, so wrap in try/catch
try {
client = await Encryption({ schemas: [users] })
} catch (error) {
throw new Error(`Test setup failed: ${(error as Error).message}`)
}
})
afterAll(async () => {
await pool.end()
})Testing encrypt and decrypt round-trips
test("encrypt and decrypt returns original value", async () => {
const original = "[email protected]"
const encrypted = await client.encrypt(original, {
column: users.email,
table: users,
})
expect(encrypted.failure).toBeUndefined()
const decrypted = await client.decrypt(encrypted.data)
expect(decrypted.failure).toBeUndefined()
expect(decrypted.data).toBe(original)
})Testing searchable queries
test("equality search finds encrypted record", async () => {
// Encrypt and store a value
const encrypted = await client.encrypt("[email protected]", {
column: users.email,
table: users,
})
await pool.query(
"INSERT INTO users (email) VALUES ($1::jsonb)",
[JSON.stringify(encrypted.data)]
)
// Encrypt the search term
const query = await client.encryptQuery("[email protected]", {
column: users.email,
table: users,
queryType: "equality",
})
// Query the database
const result = await pool.query(
"SELECT * FROM users WHERE email @> $1::jsonb",
[JSON.stringify(query.data)]
)
expect(result.rows.length).toBe(1)
})CI/CD setup
GitHub Actions
Store your test credentials as GitHub Actions secrets and expose them as environment variables:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: test
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: pnpm install --frozen-lockfile
- run: pnpm test
env:
CS_WORKSPACE_CRN: ${{ secrets.CS_TEST_WORKSPACE_CRN }}
CS_CLIENT_ID: ${{ secrets.CS_TEST_CLIENT_ID }}
CS_CLIENT_KEY: ${{ secrets.CS_TEST_CLIENT_KEY }}
CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_TEST_ACCESS_KEY }}
TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/testUse a dedicated test workspace with limited access keys. Never use production credentials in CI.
Docker-based tests
If your CI uses Docker, ensure the native addon loads correctly by using a Linux-compatible lockfile. See Bundling: Linux deployments for details.
Schema builders in test code
The @cipherstash/stack/client subpath provides schema builders without the native FFI module. This is useful for importing schemas in client-side test code or test utilities that don't perform encryption:
import { encryptedTable, encryptedColumn } from "@cipherstash/stack/client"
// This import works without the native addon
const users = encryptedTable("users", {
email: encryptedColumn("email").equality().freeTextSearch(),
})