(feat): better connection management, utility functions & tests
This commit is contained in:
parent
b8889812c5
commit
822dfea577
14 changed files with 448 additions and 39 deletions
62
test/crypto.test.ts
Normal file
62
test/crypto.test.ts
Normal 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",
|
||||
);
|
||||
});
|
||||
});
|
||||
39
test/index-helpers.test.ts
Normal file
39
test/index-helpers.test.ts
Normal 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
39
test/schema.test.ts
Normal 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
82
test/user.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue