(feat): better connection management, utility functions & tests

This commit is contained in:
Alois 2026-04-14 01:37:18 +02:00
commit 822dfea577
14 changed files with 448 additions and 39 deletions

62
test/crypto.test.ts Normal file
View file

@ -0,0 +1,62 @@
import { describe, expect, test } from "bun:test";
import { x448 } from "@noble/curves/ed448.js";
import { decrypt, encrypt, getSharedSecret } from "../src/crypto";
function toBase64(bytes: Uint8Array): string {
if (typeof Buffer !== "undefined") {
return Buffer.from(bytes).toString("base64");
}
let binary = "";
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}
return btoa(binary);
}
describe("crypto", () => {
test("encrypt/decrypt roundtrip", async () => {
const privA = new Uint8Array(56);
crypto.getRandomValues(privA);
const pubA = x448.getPublicKey(privA);
const privB = new Uint8Array(56);
crypto.getRandomValues(privB);
const pubB = x448.getPublicKey(privB);
const sharedA = await getSharedSecret(
toBase64(privA),
toBase64(pubA),
toBase64(pubB),
);
const sharedB = await getSharedSecret(
toBase64(privB),
toBase64(pubB),
toBase64(pubA),
);
expect(sharedA).toBe(sharedB);
const message = "hello tauth sdk";
const cipher = await encrypt(sharedA, message);
const plain = await decrypt(sharedB, cipher);
expect(plain).toBe(message);
});
test("decrypt fails when using wrong shared secret", async () => {
const secretA = "01".repeat(56);
const secretB = "02".repeat(56);
const cipher = await encrypt(secretA, "sensitive");
await expect(decrypt(secretB, cipher)).rejects.toThrow();
});
test("getSharedSecret rejects malformed key material", async () => {
await expect(getSharedSecret("AA==", "AA==", "AA==")).rejects.toThrow(
"not a valid X448",
);
});
});

View file

@ -0,0 +1,39 @@
import { describe, expect, test } from "bun:test";
import { READY_STATE } from "@tensamin/ttp-core";
import { TAuthClient, generateKeyPair, getFriendlyReadyState } from "../src/index";
describe("index helpers", () => {
test("generateKeyPair returns base64 keys with expected length", () => {
const pair = generateKeyPair();
const priv = Buffer.from(pair.private, "base64");
const pub = Buffer.from(pair.public, "base64");
expect(priv.length).toBe(56);
expect(pub.length).toBe(56);
});
test("getFriendlyReadyState maps numeric state to key", () => {
for (const [name, value] of Object.entries(READY_STATE)) {
expect(getFriendlyReadyState(value)).toBe(name);
}
});
test("generateLink builds auth URL with and without challenge", () => {
const client = Object.create(TAuthClient.prototype) as TAuthClient;
client.frontendUrl = "https://tauth.example.com/login";
client.identifier = "my-app";
client.redirectUrl = new URL("https://app.example.com/callback");
const withoutChallenge = client.generateLink();
const withChallenge = client.generateLink("abcdef");
const parsedWithout = new URL(withoutChallenge);
const parsedWith = new URL(withChallenge);
expect(parsedWithout.searchParams.get("identifier")).toBe("my-app");
expect(parsedWithout.searchParams.get("redirect")).toBe(
"https://app.example.com/callback",
);
expect(parsedWith.searchParams.get("challenge")).toBe("abcdef");
});
});

39
test/schema.test.ts Normal file
View file

@ -0,0 +1,39 @@
import { describe, expect, test } from "bun:test";
import z from "zod";
import { createSchema } from "../src/schema";
describe("schema", () => {
const appData = z.object({
my: z.string(),
cool: z.string(),
});
const schema = createSchema(appData);
test("accepts valid identification request", () => {
const parsed = schema.identification.request.parse({
app_identifier: "app-1",
app_session_id: 42,
app_public_key: "base64key",
user_id: 123,
});
expect(parsed.user_id).toBe(123);
});
test("rejects invalid challenge response request", () => {
const result = schema.challenge_response.request.safeParse({
challenge: "not-base64",
});
expect(result.success).toBe(false);
});
test("accepts valid save_app_data request", () => {
const result = schema.save_app_data.request.safeParse({
app_data: JSON.stringify({ my: "a", cool: "b" }),
});
expect(result.success).toBe(true);
});
});

82
test/user.test.ts Normal file
View file

@ -0,0 +1,82 @@
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { get } from "../src/user";
describe("user.get", () => {
const originalFetch = globalThis.fetch;
beforeEach(() => {
// Reset mocks each test and isolate IDs to avoid cache collisions.
mock.restore();
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
test("fetches and returns user data", async () => {
const fetchMock = mock(async () => {
return {
json: async () => ({
status: "ok",
username: "alice",
public_key: "pub",
user_id: 9001,
iota_id: 1,
sub_level: 0,
sub_end: 0,
display: "Alice",
status_message: "hi",
about: "about",
}),
} as Response;
});
globalThis.fetch = fetchMock as typeof fetch;
const result = await get(9001);
expect(result?.username).toBe("alice");
expect(fetchMock).toHaveBeenCalledTimes(1);
});
test("returns cached value for repeated user id", async () => {
const fetchMock = mock(async () => {
return {
json: async () => ({
status: "ok",
username: "bob",
public_key: "pub2",
user_id: 9002,
iota_id: 1,
sub_level: 0,
sub_end: 0,
display: "Bob",
status_message: "hi",
about: "about",
}),
} as Response;
});
globalThis.fetch = fetchMock as typeof fetch;
const first = await get(9002);
const second = await get(9002);
expect(first?.username).toBe("bob");
expect(second?.username).toBe("bob");
expect(fetchMock).toHaveBeenCalledTimes(1);
});
test("returns undefined when fetch fails", async () => {
const fetchMock = mock(async () => {
throw new Error("network down");
});
globalThis.fetch = fetchMock as typeof fetch;
const result = await get(9003);
expect(result).toBeUndefined();
expect(fetchMock).toHaveBeenCalledTimes(1);
});
});