← back to AbramsOS
tick 14: unit tests for lib/crypto.js + lib/ids.js (backlog item 16)
8adb08330f508911bb90fa5fb24d8df47cf1dd60 · 2026-05-10 09:51:01 -0700 · Steve
- tests/crypto.test.js (7): AES-256-GCM round-trip, IV uniqueness, tamper
detection on ciphertext + auth tag, unicode, empty-string, env-var validation
- tests/ids.test.js (6): prefix per kind, unknown-kind throws, distinctness,
ULID monotonic-over-time, lowercase invariant
- 13/13 green; pure unit tests, no PG, no server
Files touched
A tests/crypto.test.jsA tests/ids.test.js
Diff
commit 8adb08330f508911bb90fa5fb24d8df47cf1dd60
Author: Steve <steve@designerwallcoverings.com>
Date: Sun May 10 09:51:01 2026 -0700
tick 14: unit tests for lib/crypto.js + lib/ids.js (backlog item 16)
- tests/crypto.test.js (7): AES-256-GCM round-trip, IV uniqueness, tamper
detection on ciphertext + auth tag, unicode, empty-string, env-var validation
- tests/ids.test.js (6): prefix per kind, unknown-kind throws, distinctness,
ULID monotonic-over-time, lowercase invariant
- 13/13 green; pure unit tests, no PG, no server
---
tests/crypto.test.js | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/ids.test.js | 40 ++++++++++++++++++++++++++++++++
2 files changed, 105 insertions(+)
diff --git a/tests/crypto.test.js b/tests/crypto.test.js
new file mode 100644
index 0000000..8a7da1f
--- /dev/null
+++ b/tests/crypto.test.js
@@ -0,0 +1,65 @@
+// AES-256-GCM crypto helper tests.
+// We need a 64-hex-char ENCRYPTION_KEY in env; load it from .env if present,
+// otherwise inject a deterministic test key.
+
+const test = require('node:test');
+const assert = require('node:assert');
+
+require('dotenv').config();
+if (!process.env.ENCRYPTION_KEY || process.env.ENCRYPTION_KEY.length < 64) {
+ process.env.ENCRYPTION_KEY = 'a'.repeat(64);
+}
+
+const { encrypt, decrypt } = require('../lib/crypto');
+
+test('round-trip plaintext returns same value', () => {
+ const plain = 'gmail-refresh-token-1//04abc-supersecret';
+ const { ciphertext, iv, tag } = encrypt(plain);
+ const decoded = decrypt(ciphertext, iv, tag);
+ assert.strictEqual(decoded, plain);
+});
+
+test('IV differs between encryptions of the same plaintext', () => {
+ const plain = 'same-plaintext';
+ const a = encrypt(plain);
+ const b = encrypt(plain);
+ assert.notStrictEqual(a.iv.toString('hex'), b.iv.toString('hex'), 'IVs should be random');
+ // Both decrypt back to the same plaintext
+ assert.strictEqual(decrypt(a.ciphertext, a.iv, a.tag), plain);
+ assert.strictEqual(decrypt(b.ciphertext, b.iv, b.tag), plain);
+});
+
+test('tampering with ciphertext throws', () => {
+ const { ciphertext, iv, tag } = encrypt('hello');
+ const tampered = Buffer.from(ciphertext);
+ tampered[0] ^= 0xff;
+ assert.throws(() => decrypt(tampered, iv, tag), /Unsupported state|unable to authenticate/i);
+});
+
+test('tampering with auth tag throws', () => {
+ const { ciphertext, iv, tag } = encrypt('hello');
+ const tamperedTag = Buffer.from(tag);
+ tamperedTag[0] ^= 0xff;
+ assert.throws(() => decrypt(ciphertext, iv, tamperedTag), /Unsupported state|unable to authenticate/i);
+});
+
+test('handles empty string', () => {
+ const { ciphertext, iv, tag } = encrypt('');
+ assert.strictEqual(decrypt(ciphertext, iv, tag), '');
+});
+
+test('handles unicode', () => {
+ const plain = 'héllo · 世界 · 🌍';
+ const { ciphertext, iv, tag } = encrypt(plain);
+ assert.strictEqual(decrypt(ciphertext, iv, tag), plain);
+});
+
+test('encrypt rejects when ENCRYPTION_KEY is missing or too short', () => {
+ const orig = process.env.ENCRYPTION_KEY;
+ process.env.ENCRYPTION_KEY = '';
+ // Re-require to pick up env change — but our crypto reads env at call time, so just call:
+ assert.throws(() => encrypt('x'), /ENCRYPTION_KEY/);
+ process.env.ENCRYPTION_KEY = '0123456789abcdef'; // 16 chars (too short)
+ assert.throws(() => encrypt('x'), /ENCRYPTION_KEY/);
+ process.env.ENCRYPTION_KEY = orig;
+});
diff --git a/tests/ids.test.js b/tests/ids.test.js
new file mode 100644
index 0000000..2e4e294
--- /dev/null
+++ b/tests/ids.test.js
@@ -0,0 +1,40 @@
+// ULID-prefix ID helper tests.
+
+const test = require('node:test');
+const assert = require('node:assert');
+const { id } = require('../lib/ids');
+
+test('returns prefix_<lowercase-ulid>', () => {
+ const x = id('user');
+ assert.match(x, /^user_[0-9a-z]{26}$/, `got: ${x}`);
+});
+
+test('all known kinds emit a recognized prefix', () => {
+ for (const kind of ['user', 'consent', 'connector', 'source', 'document', 'purchase', 'item']) {
+ const x = id(kind);
+ assert.match(x, /^[a-z]+_[0-9a-z]{26}$/);
+ }
+});
+
+test('throws on unknown kind', () => {
+ assert.throws(() => id('totally-bogus'), /Unknown id kind/);
+});
+
+test('two consecutive ids are distinct', () => {
+ const a = id('purchase');
+ const b = id('purchase');
+ assert.notStrictEqual(a, b);
+});
+
+test('ULIDs are sortable lexicographically (monotonic over time)', async () => {
+ const a = id('purchase');
+ await new Promise((r) => setTimeout(r, 5));
+ const b = id('purchase');
+ // ULID timestamps mean later id should be >= earlier when compared as strings
+ assert.ok(a.localeCompare(b) <= 0, `expected ${a} <= ${b}`);
+});
+
+test('id strings are lowercase (db convention)', () => {
+ const x = id('document');
+ assert.strictEqual(x, x.toLowerCase());
+});
← 3877b56 tick 13: /recalls dashboard UI (read-only viewer for recall_
·
back to AbramsOS
·
tick 15: auto-parse Drive PDFs on sync (backlog item 17) a5da75e →