Initial Commit
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Pins per-row error reporting + skipDuplicates against real SurrealDB.
|
||||
*/
|
||||
|
||||
import "@suaveplan/api-plugin-auth";
|
||||
import { OpenAPIHono } from "@hono/zod-openapi";
|
||||
import { createPluginContext } from "@suaveplan/api-plugin";
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
import { SurrealAdapter } from "@suaveplan/surrealdb";
|
||||
import {
|
||||
afterAll,
|
||||
beforeAll,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import { SurrealDBFixture } from "@suaveplan/testing-integration-surrealdb";
|
||||
import { applyContactsSchema, createContactsPlugin } from "../../src/index.js";
|
||||
|
||||
let fixture: SurrealDBFixture | null = null;
|
||||
let client: SurrealAdapter | null = null;
|
||||
|
||||
const noopLogger = {
|
||||
debug: () => undefined,
|
||||
info: () => undefined,
|
||||
warn: () => undefined,
|
||||
error: () => undefined,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
fixture = new SurrealDBFixture();
|
||||
const cfg = await fixture.start();
|
||||
client = new SurrealAdapter({
|
||||
url: cfg.url,
|
||||
namespace: cfg.namespace,
|
||||
database: cfg.database,
|
||||
username: cfg.username,
|
||||
password: cfg.password,
|
||||
});
|
||||
await client.connect();
|
||||
await applyContactsSchema({ db: client });
|
||||
}, 180_000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (client !== null) {
|
||||
await client.disconnect();
|
||||
client = null;
|
||||
}
|
||||
if (fixture !== null) {
|
||||
await fixture.stop();
|
||||
fixture = null;
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
describe("api-plugin-contacts bulk-import integration", () => {
|
||||
it("per-row errors are reported with the original index against real SurrealDB", {
|
||||
timeout: 60_000,
|
||||
}, async () => {
|
||||
if (client === null) throw new Error("client not ready");
|
||||
|
||||
const app = new OpenAPIHono();
|
||||
const services = new Map<string, unknown>();
|
||||
services.set("app", app);
|
||||
services.set("db", client);
|
||||
const ctx = createPluginContext({
|
||||
logger: noopLogger,
|
||||
eventBus: new EventBus(),
|
||||
signal: new AbortController().signal,
|
||||
config: {} as never,
|
||||
services,
|
||||
});
|
||||
const desc = createContactsPlugin({
|
||||
applySchemaOnInit: false,
|
||||
maxImportBatchSize: 5,
|
||||
});
|
||||
await desc.onInit?.(ctx as never);
|
||||
|
||||
// Mount a fake auth middleware that injects a tenantId.
|
||||
const wrapped = new OpenAPIHono();
|
||||
wrapped.use("*", async (c, next) => {
|
||||
c.set("user", { id: "u", tenantId: "t-import" } as never);
|
||||
await next();
|
||||
});
|
||||
wrapped.route("/", app);
|
||||
|
||||
const res = await wrapped.request("/contacts/import", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
contacts: [
|
||||
{
|
||||
displayName: "ok",
|
||||
channels: [{ type: "email", value: "ok@example.com" }],
|
||||
},
|
||||
{
|
||||
displayName: "ok2",
|
||||
channels: [{ type: "email", value: "ok2@example.com" }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as {
|
||||
data: { imported: number; skipped: number; errors: unknown[] };
|
||||
};
|
||||
expect(body.data.imported).toBe(2);
|
||||
expect(body.data.errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Full-boot integration test against a real SurrealDB v3 testcontainer.
|
||||
* Applies the bundled schema and walks the full contacts + groups +
|
||||
* memberships roundtrip via the three repositories.
|
||||
*/
|
||||
|
||||
import { SurrealAdapter } from "@suaveplan/surrealdb";
|
||||
import {
|
||||
afterAll,
|
||||
beforeAll,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import { SurrealDBFixture } from "@suaveplan/testing-integration-surrealdb";
|
||||
import {
|
||||
applyContactsSchema,
|
||||
getContactsApiInstruments,
|
||||
resetContactsApiInstruments,
|
||||
SurrealContactRepository,
|
||||
SurrealGroupRepository,
|
||||
SurrealMembershipRepository,
|
||||
} from "../../src/index.js";
|
||||
|
||||
let fixture: SurrealDBFixture | null = null;
|
||||
let client: SurrealAdapter | null = null;
|
||||
|
||||
beforeAll(async () => {
|
||||
fixture = new SurrealDBFixture();
|
||||
const cfg = await fixture.start();
|
||||
client = new SurrealAdapter({
|
||||
url: cfg.url,
|
||||
namespace: cfg.namespace,
|
||||
database: cfg.database,
|
||||
username: cfg.username,
|
||||
password: cfg.password,
|
||||
});
|
||||
await client.connect();
|
||||
resetContactsApiInstruments();
|
||||
}, 180_000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (client !== null) {
|
||||
await client.disconnect();
|
||||
client = null;
|
||||
}
|
||||
if (fixture !== null) {
|
||||
await fixture.stop();
|
||||
fixture = null;
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
describe("api-plugin-contacts full-boot integration", () => {
|
||||
it("onInit applies surql tables against real SurrealDB", {
|
||||
timeout: 120_000,
|
||||
}, async () => {
|
||||
if (client === null) throw new Error("client not ready");
|
||||
await applyContactsSchema({ db: client });
|
||||
const [info] = await client.query<unknown[]>("INFO FOR DB");
|
||||
expect(info).toBeDefined();
|
||||
});
|
||||
|
||||
it("contact roundtrip against real SurrealDB", {
|
||||
timeout: 60_000,
|
||||
}, async () => {
|
||||
if (client === null) throw new Error("client not ready");
|
||||
const repo = new SurrealContactRepository(client);
|
||||
const created = await repo.create({
|
||||
tenantId: "t-1",
|
||||
displayName: "Ada Lovelace",
|
||||
channels: [
|
||||
{
|
||||
type: "email",
|
||||
value: "ada@example.com",
|
||||
primary: true,
|
||||
verified: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(created.id.length).toBeGreaterThan(0);
|
||||
const fetched = await repo.getById(created.id);
|
||||
expect(fetched?.displayName).toBe("Ada Lovelace");
|
||||
const updated = await repo.update(created.id, {
|
||||
displayName: "Ada L.",
|
||||
});
|
||||
expect(updated.displayName).toBe("Ada L.");
|
||||
const count = await repo.countByTenant("t-1");
|
||||
expect(count).toBeGreaterThanOrEqual(1);
|
||||
await repo.delete(created.id);
|
||||
});
|
||||
|
||||
it("full contact + group + membership roundtrip", {
|
||||
timeout: 60_000,
|
||||
}, async () => {
|
||||
if (client === null) throw new Error("client not ready");
|
||||
const contactRepo = new SurrealContactRepository(client);
|
||||
const groupRepo = new SurrealGroupRepository(client, {
|
||||
maxGroupDepth: 5,
|
||||
enforceGroupDepth: true,
|
||||
});
|
||||
const memberRepo = new SurrealMembershipRepository(client);
|
||||
const contact = await contactRepo.create({
|
||||
tenantId: "t-2",
|
||||
displayName: "Grace Hopper",
|
||||
channels: [
|
||||
{
|
||||
type: "email",
|
||||
value: "grace@example.com",
|
||||
primary: true,
|
||||
verified: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
const root = await groupRepo.createGroup({
|
||||
tenantId: "t-2",
|
||||
name: "Pioneers",
|
||||
});
|
||||
await memberRepo.add({
|
||||
id: memberRepo.generateId(),
|
||||
groupId: root.id,
|
||||
contactId: contact.id,
|
||||
addedAt: new Date(),
|
||||
});
|
||||
const members = await memberRepo.getByGroup(root.id);
|
||||
expect(members.length).toBe(1);
|
||||
expect(await memberRepo.exists(root.id, contact.id)).toBe(true);
|
||||
await memberRepo.remove(root.id, contact.id);
|
||||
await contactRepo.delete(contact.id);
|
||||
await groupRepo.delete(root.id);
|
||||
});
|
||||
|
||||
it("emits contacts_api.contact_writes_total on POST /contacts", {
|
||||
timeout: 60_000,
|
||||
}, async () => {
|
||||
if (client === null) {
|
||||
throw new Error("client not ready");
|
||||
}
|
||||
// Re-bind to the global polyfill MeterProvider installed in
|
||||
// tests/setup/polyfills.ts so flushTestMetrics() sees the
|
||||
// increments emitted by getContactsApiInstruments().
|
||||
const { flushTestMetrics } = await import("../setup/polyfills.js");
|
||||
resetContactsApiInstruments();
|
||||
getContactsApiInstruments().contactWritesTotal.add(1, {
|
||||
outcome: "success",
|
||||
});
|
||||
const collected = await flushTestMetrics();
|
||||
const names = collected.flatMap((r) =>
|
||||
r.scopeMetrics.flatMap((s) =>
|
||||
s.metrics.map((m) => m.descriptor.name)
|
||||
)
|
||||
);
|
||||
expect(names).toContain("contacts_api.contact_writes_total");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Pins the `maxGroupDepth` repository guard against real SurrealDB.
|
||||
*/
|
||||
|
||||
import { SurrealAdapter } from "@suaveplan/surrealdb";
|
||||
import {
|
||||
afterAll,
|
||||
beforeAll,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import { SurrealDBFixture } from "@suaveplan/testing-integration-surrealdb";
|
||||
import {
|
||||
applyContactsSchema,
|
||||
CONTACTS_API_CODES,
|
||||
SurrealGroupRepository,
|
||||
} from "../../src/index.js";
|
||||
|
||||
let fixture: SurrealDBFixture | null = null;
|
||||
let client: SurrealAdapter | null = null;
|
||||
|
||||
beforeAll(async () => {
|
||||
fixture = new SurrealDBFixture();
|
||||
const cfg = await fixture.start();
|
||||
client = new SurrealAdapter({
|
||||
url: cfg.url,
|
||||
namespace: cfg.namespace,
|
||||
database: cfg.database,
|
||||
username: cfg.username,
|
||||
password: cfg.password,
|
||||
});
|
||||
await client.connect();
|
||||
await applyContactsSchema({ db: client });
|
||||
}, 180_000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (client !== null) {
|
||||
await client.disconnect();
|
||||
client = null;
|
||||
}
|
||||
if (fixture !== null) {
|
||||
await fixture.stop();
|
||||
fixture = null;
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
describe("api-plugin-contacts group-depth integration", () => {
|
||||
it("createGroup raises GROUP_DEPTH_EXCEEDED when enforceGroupDepth is on", {
|
||||
timeout: 60_000,
|
||||
}, async () => {
|
||||
if (client === null) throw new Error("client not ready");
|
||||
const repo = new SurrealGroupRepository(client, {
|
||||
maxGroupDepth: 2,
|
||||
enforceGroupDepth: true,
|
||||
});
|
||||
const a = await repo.createGroup({ tenantId: "t-depth", name: "A" });
|
||||
const b = await repo.createGroup({
|
||||
tenantId: "t-depth",
|
||||
name: "B",
|
||||
parentId: a.id,
|
||||
});
|
||||
// b.depth === 1; another child would be depth 2 == maxGroupDepth (>=).
|
||||
await expect(
|
||||
repo.createGroup({
|
||||
tenantId: "t-depth",
|
||||
name: "C",
|
||||
parentId: b.id,
|
||||
})
|
||||
).rejects.toMatchObject({
|
||||
code: CONTACTS_API_CODES.GROUP_DEPTH_EXCEEDED,
|
||||
});
|
||||
await repo.delete(b.id);
|
||||
await repo.delete(a.id);
|
||||
});
|
||||
|
||||
it("createGroup permits any depth when enforceGroupDepth is false", {
|
||||
timeout: 60_000,
|
||||
}, async () => {
|
||||
if (client === null) throw new Error("client not ready");
|
||||
const repo = new SurrealGroupRepository(client, {
|
||||
maxGroupDepth: 1,
|
||||
enforceGroupDepth: false,
|
||||
});
|
||||
const a = await repo.createGroup({
|
||||
tenantId: "t-depth-off",
|
||||
name: "A",
|
||||
});
|
||||
const b = await repo.createGroup({
|
||||
tenantId: "t-depth-off",
|
||||
name: "B",
|
||||
parentId: a.id,
|
||||
});
|
||||
const c = await repo.createGroup({
|
||||
tenantId: "t-depth-off",
|
||||
name: "C",
|
||||
parentId: b.id,
|
||||
});
|
||||
expect(c.depth).toBe(2);
|
||||
await repo.delete(c.id);
|
||||
await repo.delete(b.id);
|
||||
await repo.delete(a.id);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user