feat(db): mesh data model — meshes, members, invites, audit log
- pgSchema "mesh" with 4 tables isolating the peer mesh domain - Enums: visibility, transport, tier, role - audit_log is metadata-only (E2E encryption enforced at broker/client) - Cascade on mesh delete, soft-delete via archivedAt/revokedAt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
45
packages/shared/src/utils/test/exceptions.test.ts
Normal file
45
packages/shared/src/utils/test/exceptions.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { HttpStatusCode } from "../../constants";
|
||||
import { HttpException, getStatusCode, isHttpStatus } from "../exceptions";
|
||||
|
||||
describe("isHttpStatus", () => {
|
||||
it.each([
|
||||
[200, true],
|
||||
[404, true],
|
||||
[500, true],
|
||||
[999, false],
|
||||
[1, false],
|
||||
])("should return $expected for status $status", (status, expected) => {
|
||||
expect(isHttpStatus(status)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HttpException", () => {
|
||||
it("should create an instance with status and message", () => {
|
||||
const error = new HttpException(HttpStatusCode.BAD_REQUEST, {
|
||||
message: "Bad Request",
|
||||
});
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error.status).toBe(HttpStatusCode.BAD_REQUEST);
|
||||
expect(error.message).toBe("Bad Request");
|
||||
});
|
||||
|
||||
it("should create an instance with code", () => {
|
||||
const error = new HttpException(HttpStatusCode.BAD_REQUEST, {
|
||||
code: "INVALID_INPUT",
|
||||
});
|
||||
expect(error.code).toBe("INVALID_INPUT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStatusCode", () => {
|
||||
it.each([
|
||||
[{ status: 404 }, 404],
|
||||
[{ message: "error" }, HttpStatusCode.INTERNAL_SERVER_ERROR],
|
||||
["error", HttpStatusCode.INTERNAL_SERVER_ERROR],
|
||||
[null, HttpStatusCode.INTERNAL_SERVER_ERROR],
|
||||
])("should return %s for input %s", (input, expected) => {
|
||||
expect(getStatusCode(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
79
packages/shared/src/utils/test/id.test.ts
Normal file
79
packages/shared/src/utils/test/id.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createIdGenerator, generateId } from "../id";
|
||||
|
||||
describe("generateId", () => {
|
||||
it("should generate a string of length 32 by default", () => {
|
||||
const id = generateId();
|
||||
expect(typeof id).toBe("string");
|
||||
expect(id.length).toBe(32);
|
||||
});
|
||||
|
||||
it("should generate unique ids", () => {
|
||||
const id1 = generateId();
|
||||
const id2 = generateId();
|
||||
expect(id1).not.toBe(id2);
|
||||
});
|
||||
|
||||
it("should only contain allowed characters (alphanumeric)", () => {
|
||||
const id = generateId();
|
||||
expect(id).toMatch(/^[0-9a-zA-Z]+$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createIdGenerator", () => {
|
||||
it("should allow custom size", () => {
|
||||
const generateShortId = createIdGenerator({ size: 10 });
|
||||
const id = generateShortId();
|
||||
expect(id.length).toBe(10);
|
||||
});
|
||||
|
||||
it("should allow custom alphabet", () => {
|
||||
const generateBinaryId = createIdGenerator({ alphabet: "01" });
|
||||
const id = generateBinaryId();
|
||||
expect(id).toMatch(/^[01]+$/);
|
||||
});
|
||||
|
||||
it("should allow custom alphabet with special characters", () => {
|
||||
const specialChars = "!@#$%^&*";
|
||||
const generateSpecialId = createIdGenerator({ alphabet: specialChars });
|
||||
const id = generateSpecialId();
|
||||
for (const char of id) {
|
||||
expect(specialChars).toContain(char);
|
||||
}
|
||||
});
|
||||
|
||||
it("should allow prefix", () => {
|
||||
const generatePrefixedId = createIdGenerator({ prefix: "user" });
|
||||
const id = generatePrefixedId();
|
||||
expect(id.startsWith("user-")).toBe(true);
|
||||
expect(id.length).toBe(32 + 5); // 32 random + 4 prefix + 1 separator
|
||||
});
|
||||
|
||||
it("should allow custom separator", () => {
|
||||
const generateUnderscoreId = createIdGenerator({
|
||||
prefix: "test",
|
||||
separator: "_",
|
||||
});
|
||||
const id = generateUnderscoreId();
|
||||
expect(id.startsWith("test_")).toBe(true);
|
||||
});
|
||||
|
||||
it("should throw if separator is in alphabet", () => {
|
||||
expect(() =>
|
||||
createIdGenerator({
|
||||
prefix: "fail",
|
||||
separator: "a",
|
||||
alphabet: "abc",
|
||||
}),
|
||||
).toThrow('The separator "a" must not be part of the alphabet "abc".');
|
||||
});
|
||||
|
||||
it("should allow empty prefix", () => {
|
||||
// If prefix is provided as empty string, it works like a prefix
|
||||
const generateEmptyPrefixedId = createIdGenerator({ prefix: "" });
|
||||
// prefix="" -> `${""}-${generator()}` -> `-${generator()}`
|
||||
const id = generateEmptyPrefixedId();
|
||||
expect(id.startsWith("-")).toBe(true);
|
||||
});
|
||||
});
|
||||
93
packages/shared/src/utils/test/url.test.ts
Normal file
93
packages/shared/src/utils/test/url.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
getHost,
|
||||
getOrigin,
|
||||
getProtocol,
|
||||
isExternal,
|
||||
matchesPattern,
|
||||
mergeSearchParams,
|
||||
} from "../url";
|
||||
|
||||
describe("isExternal", () => {
|
||||
it.each([
|
||||
["https://google.com", true],
|
||||
["http://example.com", true],
|
||||
["//cdn.example.com", true],
|
||||
["mailto:user@example.com", true],
|
||||
["/dashboard", false],
|
||||
["about", false],
|
||||
])("should return $expected for url $url", (url, expected) => {
|
||||
expect(isExternal(url)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrigin", () => {
|
||||
it.each([
|
||||
["https://example.com/path", "https://example.com"],
|
||||
["invalid-url", null],
|
||||
["exp://192.168.1.1:8081", null],
|
||||
])("should return %s for %s", (url, expected) => {
|
||||
expect(getOrigin(url)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getProtocol", () => {
|
||||
it.each([
|
||||
["https://example.com", "https:"],
|
||||
["mailto:user@example.com", "mailto:"],
|
||||
["not-a-url", null],
|
||||
])("should return %s for %s", (url, expected) => {
|
||||
expect(getProtocol(url)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getHost", () => {
|
||||
it.each([
|
||||
["https://example.com:8080/path", "example.com:8080"],
|
||||
["invalid", null],
|
||||
])("should return %s for %s", (url, expected) => {
|
||||
expect(getHost(url)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mergeSearchParams", () => {
|
||||
it("should merge params without overwrite by default", () => {
|
||||
const target = new URL("https://example.com?a=1");
|
||||
const source = new URL("https://other.com?a=2&b=3");
|
||||
mergeSearchParams(target, source);
|
||||
expect(target.searchParams.get("a")).toBe("1");
|
||||
expect(target.searchParams.get("b")).toBe("3");
|
||||
});
|
||||
|
||||
it("should overwrite params if specified", () => {
|
||||
const target = new URL("https://example.com?a=1");
|
||||
const source = new URL("https://other.com?a=2");
|
||||
mergeSearchParams(target, source, { overwrite: true });
|
||||
expect(target.searchParams.get("a")).toBe("2");
|
||||
});
|
||||
|
||||
it("should replace params if specified", () => {
|
||||
const target = new URL("https://example.com?a=1");
|
||||
const source = new URL("https://other.com?b=2");
|
||||
mergeSearchParams(target, source, { replace: true });
|
||||
expect(target.searchParams.has("a")).toBe(false);
|
||||
expect(target.searchParams.get("b")).toBe("2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("matchesPattern", () => {
|
||||
it.each([
|
||||
["/path", "https://example.com", false],
|
||||
["https://sub.example.com", "*.example.com", true],
|
||||
["https://example.com", "https://example.com", true],
|
||||
["https://example.org", "https://example.com", false],
|
||||
["https://example.com/foo", "https://example.com", true],
|
||||
["https://example.com", "https://*", true],
|
||||
])(
|
||||
"should return %s when matching %s with pattern %s",
|
||||
(url, pattern, expected) => {
|
||||
expect(matchesPattern(url, pattern)).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user