test: collapse nextcloud-talk helper suites

This commit is contained in:
Peter Steinberger
2026-03-25 05:33:14 +00:00
parent e8e45a4936
commit 7467f304a7
10 changed files with 353 additions and 412 deletions

View File

@@ -1,38 +0,0 @@
import { describe, expect, it } from "vitest";
import { nextcloudTalkPlugin } from "./channel.js";
import type { CoreConfig } from "./types.js";
describe("nextcloudTalkPlugin security", () => {
it("normalizes trimmed dm allowlist prefixes to lowercase ids", () => {
const resolveDmPolicy = nextcloudTalkPlugin.security?.resolveDmPolicy;
if (!resolveDmPolicy) {
throw new Error("resolveDmPolicy unavailable");
}
const cfg = {
channels: {
"nextcloud-talk": {
baseUrl: "https://cloud.example.com",
botSecret: "secret",
dmPolicy: "allowlist",
allowFrom: [" nc:User-Id "],
},
},
} as CoreConfig;
const result = resolveDmPolicy({
cfg,
account: nextcloudTalkPlugin.config.resolveAccount(cfg, "default"),
});
if (!result) {
throw new Error("nextcloud-talk resolveDmPolicy returned null");
}
expect(result.policy).toBe("allowlist");
expect(result.allowFrom).toEqual([" nc:User-Id "]);
expect(result.normalizeEntry?.(" nc:User-Id ")).toBe("user-id");
expect(nextcloudTalkPlugin.pairing?.normalizeAllowEntry?.(" nextcloud-talk:User-Id ")).toBe(
"user-id",
);
});
});

View File

@@ -1,4 +1,8 @@
import { describe, expect, it } from "vitest";
import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { nextcloudTalkPlugin } from "./channel.js";
import { NextcloudTalkConfigSchema } from "./config-schema.js";
import {
escapeNextcloudTalkMarkdown,
@@ -9,7 +13,37 @@ import {
stripNextcloudTalkFormatting,
truncateNextcloudTalkText,
} from "./format.js";
import {
looksLikeNextcloudTalkTargetId,
normalizeNextcloudTalkMessagingTarget,
stripNextcloudTalkTargetPrefix,
} from "./normalize.js";
import { resolveNextcloudTalkAllowlistMatch, resolveNextcloudTalkGroupAllow } from "./policy.js";
import { createNextcloudTalkReplayGuard } from "./replay-guard.js";
import { resolveNextcloudTalkOutboundSessionRoute } from "./session-route.js";
import {
extractNextcloudTalkHeaders,
generateNextcloudTalkSignature,
verifyNextcloudTalkSignature,
} from "./signature.js";
import type { CoreConfig } from "./types.js";
const tempDirs: string[] = [];
afterEach(async () => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
await rm(dir, { recursive: true, force: true });
}
}
});
async function makeTempDir(): Promise<string> {
const dir = await mkdtemp(path.join(os.tmpdir(), "nextcloud-talk-replay-"));
tempDirs.push(dir);
return dir;
}
describe("nextcloud talk core", () => {
it("accepts SecretRef botSecret and apiPassword at top-level", () => {
@@ -96,4 +130,257 @@ describe("nextcloud talk core", () => {
}),
).toBeNull();
});
it("normalizes and recognizes supported room target formats", () => {
expect(stripNextcloudTalkTargetPrefix(" room:abc123 ")).toBe("abc123");
expect(stripNextcloudTalkTargetPrefix("nextcloud-talk:room:AbC123")).toBe("AbC123");
expect(stripNextcloudTalkTargetPrefix("nc-talk:room:ops")).toBe("ops");
expect(stripNextcloudTalkTargetPrefix("nc:room:ops")).toBe("ops");
expect(stripNextcloudTalkTargetPrefix("room: ")).toBeUndefined();
expect(normalizeNextcloudTalkMessagingTarget("room:AbC123")).toBe("nextcloud-talk:abc123");
expect(normalizeNextcloudTalkMessagingTarget("nc-talk:room:Ops")).toBe("nextcloud-talk:ops");
expect(looksLikeNextcloudTalkTargetId("nextcloud-talk:room:abc12345")).toBe(true);
expect(looksLikeNextcloudTalkTargetId("nc:opsroom1")).toBe(true);
expect(looksLikeNextcloudTalkTargetId("abc12345")).toBe(true);
expect(looksLikeNextcloudTalkTargetId("")).toBe(false);
});
it("verifies generated signatures and extracts normalized headers", () => {
const body = JSON.stringify({ hello: "world" });
const generated = generateNextcloudTalkSignature({
body,
secret: "secret-123",
});
expect(generated.random).toMatch(/^[0-9a-f]{64}$/);
expect(generated.signature).toMatch(/^[0-9a-f]{64}$/);
expect(
verifyNextcloudTalkSignature({
signature: generated.signature,
random: generated.random,
body,
secret: "secret-123",
}),
).toBe(true);
expect(
verifyNextcloudTalkSignature({
signature: "",
random: "abc",
body: "body",
secret: "secret",
}),
).toBe(false);
expect(
verifyNextcloudTalkSignature({
signature: "deadbeef",
random: "abc",
body: "body",
secret: "secret",
}),
).toBe(false);
expect(
extractNextcloudTalkHeaders({
"x-nextcloud-talk-signature": "sig",
"x-nextcloud-talk-random": "rand",
"x-nextcloud-talk-backend": "backend",
}),
).toEqual({
signature: "sig",
random: "rand",
backend: "backend",
});
expect(
extractNextcloudTalkHeaders({
"X-Nextcloud-Talk-Signature": "sig",
}),
).toBeNull();
});
it("persists replay decisions across guard instances", async () => {
const stateDir = await makeTempDir();
const firstGuard = createNextcloudTalkReplayGuard({ stateDir });
const firstAttempt = await firstGuard.shouldProcessMessage({
accountId: "account-a",
roomToken: "room-1",
messageId: "msg-1",
});
const replayAttempt = await firstGuard.shouldProcessMessage({
accountId: "account-a",
roomToken: "room-1",
messageId: "msg-1",
});
const secondGuard = createNextcloudTalkReplayGuard({ stateDir });
const restartReplayAttempt = await secondGuard.shouldProcessMessage({
accountId: "account-a",
roomToken: "room-1",
messageId: "msg-1",
});
expect(firstAttempt).toBe(true);
expect(replayAttempt).toBe(false);
expect(restartReplayAttempt).toBe(false);
});
it("scopes replay state by account namespace", async () => {
const stateDir = await makeTempDir();
const guard = createNextcloudTalkReplayGuard({ stateDir });
const accountAFirst = await guard.shouldProcessMessage({
accountId: "account-a",
roomToken: "room-1",
messageId: "msg-9",
});
const accountBFirst = await guard.shouldProcessMessage({
accountId: "account-b",
roomToken: "room-1",
messageId: "msg-9",
});
expect(accountAFirst).toBe(true);
expect(accountBFirst).toBe(true);
});
it("normalizes trimmed DM allowlist prefixes to lowercase ids", () => {
const resolveDmPolicy = nextcloudTalkPlugin.security?.resolveDmPolicy;
if (!resolveDmPolicy) {
throw new Error("resolveDmPolicy unavailable");
}
const cfg = {
channels: {
"nextcloud-talk": {
baseUrl: "https://cloud.example.com",
botSecret: "secret",
dmPolicy: "allowlist",
allowFrom: [" nc:User-Id "],
},
},
} as CoreConfig;
const result = resolveDmPolicy({
cfg,
account: nextcloudTalkPlugin.config.resolveAccount(cfg, "default"),
});
if (!result) {
throw new Error("nextcloud-talk resolveDmPolicy returned null");
}
expect(result.policy).toBe("allowlist");
expect(result.allowFrom).toEqual([" nc:User-Id "]);
expect(result.normalizeEntry?.(" nc:User-Id ")).toBe("user-id");
expect(nextcloudTalkPlugin.pairing?.normalizeAllowEntry?.(" nextcloud-talk:User-Id ")).toBe(
"user-id",
);
});
it("resolves allowlist matches and group policy decisions", () => {
expect(
resolveNextcloudTalkAllowlistMatch({
allowFrom: ["*"],
senderId: "user-id",
}).allowed,
).toBe(true);
expect(
resolveNextcloudTalkAllowlistMatch({
allowFrom: ["nc:User-Id"],
senderId: "user-id",
}),
).toEqual({ allowed: true, matchKey: "user-id", matchSource: "id" });
expect(
resolveNextcloudTalkAllowlistMatch({
allowFrom: ["allowed"],
senderId: "other",
}).allowed,
).toBe(false);
expect(
resolveNextcloudTalkGroupAllow({
groupPolicy: "disabled",
outerAllowFrom: ["owner"],
innerAllowFrom: ["room-user"],
senderId: "owner",
}),
).toEqual({
allowed: false,
outerMatch: { allowed: false },
innerMatch: { allowed: false },
});
expect(
resolveNextcloudTalkGroupAllow({
groupPolicy: "open",
outerAllowFrom: [],
innerAllowFrom: [],
senderId: "owner",
}),
).toEqual({
allowed: true,
outerMatch: { allowed: true },
innerMatch: { allowed: true },
});
expect(
resolveNextcloudTalkGroupAllow({
groupPolicy: "allowlist",
outerAllowFrom: [],
innerAllowFrom: [],
senderId: "owner",
}),
).toEqual({
allowed: false,
outerMatch: { allowed: false },
innerMatch: { allowed: false },
});
expect(
resolveNextcloudTalkGroupAllow({
groupPolicy: "allowlist",
outerAllowFrom: [],
innerAllowFrom: ["room-user"],
senderId: "room-user",
}),
).toEqual({
allowed: true,
outerMatch: { allowed: false },
innerMatch: { allowed: true, matchKey: "room-user", matchSource: "id" },
});
expect(
resolveNextcloudTalkGroupAllow({
groupPolicy: "allowlist",
outerAllowFrom: ["team-owner"],
innerAllowFrom: ["room-user"],
senderId: "room-user",
}),
).toEqual({
allowed: false,
outerMatch: { allowed: false },
innerMatch: { allowed: true, matchKey: "room-user", matchSource: "id" },
});
expect(
resolveNextcloudTalkGroupAllow({
groupPolicy: "allowlist",
outerAllowFrom: ["team-owner"],
innerAllowFrom: ["room-user"],
senderId: "team-owner",
}),
).toEqual({
allowed: false,
outerMatch: { allowed: true, matchKey: "team-owner", matchSource: "id" },
innerMatch: { allowed: false },
});
expect(
resolveNextcloudTalkGroupAllow({
groupPolicy: "allowlist",
outerAllowFrom: ["shared-user"],
innerAllowFrom: ["shared-user"],
senderId: "shared-user",
}),
).toEqual({
allowed: true,
outerMatch: { allowed: true, matchKey: "shared-user", matchSource: "id" },
innerMatch: { allowed: true, matchKey: "shared-user", matchSource: "id" },
});
});
});

View File

@@ -1,28 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { startWebhookServer } from "./monitor.test-harness.js";
describe("createNextcloudTalkWebhookServer auth order", () => {
it("rejects missing signature headers before reading request body", async () => {
const readBody = vi.fn(async () => {
throw new Error("should not be called for missing signature headers");
});
const harness = await startWebhookServer({
path: "/nextcloud-auth-order",
maxBodyBytes: 128,
readBody,
onMessage: vi.fn(),
});
const response = await fetch(harness.webhookUrl, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: "{}",
});
expect(response.status).toBe(400);
expect(await response.json()).toEqual({ error: "Missing signature headers" });
expect(readBody).not.toHaveBeenCalled();
});
});

View File

@@ -1,27 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { createSignedCreateMessageRequest } from "./monitor.test-fixtures.js";
import { startWebhookServer } from "./monitor.test-harness.js";
describe("createNextcloudTalkWebhookServer backend allowlist", () => {
it("rejects requests from unexpected backend origins", async () => {
const onMessage = vi.fn(async () => {});
const harness = await startWebhookServer({
path: "/nextcloud-backend-check",
isBackendAllowed: (backend) => backend === "https://nextcloud.expected",
onMessage,
});
const { body, headers } = createSignedCreateMessageRequest({
backend: "https://nextcloud.unexpected",
});
const response = await fetch(harness.webhookUrl, {
method: "POST",
headers,
body,
});
expect(response.status).toBe(401);
expect(await response.json()).toEqual({ error: "Invalid backend" });
expect(onMessage).not.toHaveBeenCalled();
});
});

View File

@@ -1,16 +0,0 @@
import { describe, expect, it } from "vitest";
import { createMockIncomingRequest } from "../../../test/helpers/mock-incoming-request.js";
import { readNextcloudTalkWebhookBody } from "./monitor.js";
describe("readNextcloudTalkWebhookBody", () => {
it("reads valid body within max bytes", async () => {
const req = createMockIncomingRequest(['{"type":"Create"}']);
const body = await readNextcloudTalkWebhookBody(req, 1024);
expect(body).toBe('{"type":"Create"}');
});
it("rejects when payload exceeds max bytes", async () => {
const req = createMockIncomingRequest(["x".repeat(300)]);
await expect(readNextcloudTalkWebhookBody(req, 128)).rejects.toThrow("PayloadTooLarge");
});
});

View File

@@ -1,8 +1,73 @@
import { describe, expect, it, vi } from "vitest";
import { createMockIncomingRequest } from "../../../test/helpers/mock-incoming-request.js";
import { readNextcloudTalkWebhookBody } from "./monitor.js";
import { createSignedCreateMessageRequest } from "./monitor.test-fixtures.js";
import { startWebhookServer } from "./monitor.test-harness.js";
import type { NextcloudTalkInboundMessage } from "./types.js";
describe("readNextcloudTalkWebhookBody", () => {
it("reads valid body within max bytes", async () => {
const req = createMockIncomingRequest(['{"type":"Create"}']);
const body = await readNextcloudTalkWebhookBody(req, 1024);
expect(body).toBe('{"type":"Create"}');
});
it("rejects when payload exceeds max bytes", async () => {
const req = createMockIncomingRequest(["x".repeat(300)]);
await expect(readNextcloudTalkWebhookBody(req, 128)).rejects.toThrow("PayloadTooLarge");
});
});
describe("createNextcloudTalkWebhookServer auth order", () => {
it("rejects missing signature headers before reading request body", async () => {
const readBody = vi.fn(async () => {
throw new Error("should not be called for missing signature headers");
});
const harness = await startWebhookServer({
path: "/nextcloud-auth-order",
maxBodyBytes: 128,
readBody,
onMessage: vi.fn(),
});
const response = await fetch(harness.webhookUrl, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: "{}",
});
expect(response.status).toBe(400);
expect(await response.json()).toEqual({ error: "Missing signature headers" });
expect(readBody).not.toHaveBeenCalled();
});
});
describe("createNextcloudTalkWebhookServer backend allowlist", () => {
it("rejects requests from unexpected backend origins", async () => {
const onMessage = vi.fn(async () => {});
const harness = await startWebhookServer({
path: "/nextcloud-backend-check",
isBackendAllowed: (backend) => backend === "https://nextcloud.expected",
onMessage,
});
const { body, headers } = createSignedCreateMessageRequest({
backend: "https://nextcloud.unexpected",
});
const response = await fetch(harness.webhookUrl, {
method: "POST",
headers,
body,
});
expect(response.status).toBe(401);
expect(await response.json()).toEqual({ error: "Invalid backend" });
expect(onMessage).not.toHaveBeenCalled();
});
});
describe("createNextcloudTalkWebhookServer replay handling", () => {
it("acknowledges replayed requests and skips onMessage side effects", async () => {
const seen = new Set<string>();

View File

@@ -1,28 +0,0 @@
import { describe, expect, it } from "vitest";
import {
looksLikeNextcloudTalkTargetId,
normalizeNextcloudTalkMessagingTarget,
stripNextcloudTalkTargetPrefix,
} from "./normalize.js";
describe("nextcloud-talk target normalization", () => {
it("strips supported prefixes to a room token", () => {
expect(stripNextcloudTalkTargetPrefix(" room:abc123 ")).toBe("abc123");
expect(stripNextcloudTalkTargetPrefix("nextcloud-talk:room:AbC123")).toBe("AbC123");
expect(stripNextcloudTalkTargetPrefix("nc-talk:room:ops")).toBe("ops");
expect(stripNextcloudTalkTargetPrefix("nc:room:ops")).toBe("ops");
expect(stripNextcloudTalkTargetPrefix("room: ")).toBeUndefined();
});
it("normalizes messaging targets to lowercase channel ids", () => {
expect(normalizeNextcloudTalkMessagingTarget("room:AbC123")).toBe("nextcloud-talk:abc123");
expect(normalizeNextcloudTalkMessagingTarget("nc-talk:room:Ops")).toBe("nextcloud-talk:ops");
});
it("detects prefixed and bare room ids", () => {
expect(looksLikeNextcloudTalkTargetId("nextcloud-talk:room:abc12345")).toBe(true);
expect(looksLikeNextcloudTalkTargetId("nc:opsroom1")).toBe(true);
expect(looksLikeNextcloudTalkTargetId("abc12345")).toBe(true);
expect(looksLikeNextcloudTalkTargetId("")).toBe(false);
});
});

View File

@@ -1,138 +0,0 @@
import { describe, expect, it } from "vitest";
import { resolveNextcloudTalkAllowlistMatch, resolveNextcloudTalkGroupAllow } from "./policy.js";
describe("nextcloud-talk policy", () => {
describe("resolveNextcloudTalkAllowlistMatch", () => {
it("allows wildcard", () => {
expect(
resolveNextcloudTalkAllowlistMatch({
allowFrom: ["*"],
senderId: "user-id",
}).allowed,
).toBe(true);
});
it("allows sender id match with normalization", () => {
expect(
resolveNextcloudTalkAllowlistMatch({
allowFrom: ["nc:User-Id"],
senderId: "user-id",
}),
).toEqual({ allowed: true, matchKey: "user-id", matchSource: "id" });
});
it("blocks when sender id does not match", () => {
expect(
resolveNextcloudTalkAllowlistMatch({
allowFrom: ["allowed"],
senderId: "other",
}).allowed,
).toBe(false);
});
});
describe("resolveNextcloudTalkGroupAllow", () => {
it("blocks disabled policy", () => {
expect(
resolveNextcloudTalkGroupAllow({
groupPolicy: "disabled",
outerAllowFrom: ["owner"],
innerAllowFrom: ["room-user"],
senderId: "owner",
}),
).toEqual({
allowed: false,
outerMatch: { allowed: false },
innerMatch: { allowed: false },
});
});
it("allows open policy", () => {
expect(
resolveNextcloudTalkGroupAllow({
groupPolicy: "open",
outerAllowFrom: [],
innerAllowFrom: [],
senderId: "owner",
}),
).toEqual({
allowed: true,
outerMatch: { allowed: true },
innerMatch: { allowed: true },
});
});
it("blocks allowlist mode when both outer and inner allowlists are empty", () => {
expect(
resolveNextcloudTalkGroupAllow({
groupPolicy: "allowlist",
outerAllowFrom: [],
innerAllowFrom: [],
senderId: "owner",
}),
).toEqual({
allowed: false,
outerMatch: { allowed: false },
innerMatch: { allowed: false },
});
});
it("requires inner match when only room-specific allowlist is configured", () => {
expect(
resolveNextcloudTalkGroupAllow({
groupPolicy: "allowlist",
outerAllowFrom: [],
innerAllowFrom: ["room-user"],
senderId: "room-user",
}),
).toEqual({
allowed: true,
outerMatch: { allowed: false },
innerMatch: { allowed: true, matchKey: "room-user", matchSource: "id" },
});
});
it("blocks when outer allowlist misses even if inner allowlist matches", () => {
expect(
resolveNextcloudTalkGroupAllow({
groupPolicy: "allowlist",
outerAllowFrom: ["team-owner"],
innerAllowFrom: ["room-user"],
senderId: "room-user",
}),
).toEqual({
allowed: false,
outerMatch: { allowed: false },
innerMatch: { allowed: true, matchKey: "room-user", matchSource: "id" },
});
});
it("allows when both outer and inner allowlists match", () => {
expect(
resolveNextcloudTalkGroupAllow({
groupPolicy: "allowlist",
outerAllowFrom: ["team-owner"],
innerAllowFrom: ["room-user"],
senderId: "team-owner",
}),
).toEqual({
allowed: false,
outerMatch: { allowed: true, matchKey: "team-owner", matchSource: "id" },
innerMatch: { allowed: false },
});
expect(
resolveNextcloudTalkGroupAllow({
groupPolicy: "allowlist",
outerAllowFrom: ["shared-user"],
innerAllowFrom: ["shared-user"],
senderId: "shared-user",
}),
).toEqual({
allowed: true,
outerMatch: { allowed: true, matchKey: "shared-user", matchSource: "id" },
innerMatch: { allowed: true, matchKey: "shared-user", matchSource: "id" },
});
});
});
});

View File

@@ -1,70 +0,0 @@
import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createNextcloudTalkReplayGuard } from "./replay-guard.js";
const tempDirs: string[] = [];
afterEach(async () => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
await rm(dir, { recursive: true, force: true });
}
}
});
async function makeTempDir(): Promise<string> {
const dir = await mkdtemp(path.join(os.tmpdir(), "nextcloud-talk-replay-"));
tempDirs.push(dir);
return dir;
}
describe("createNextcloudTalkReplayGuard", () => {
it("persists replay decisions across guard instances", async () => {
const stateDir = await makeTempDir();
const firstGuard = createNextcloudTalkReplayGuard({ stateDir });
const firstAttempt = await firstGuard.shouldProcessMessage({
accountId: "account-a",
roomToken: "room-1",
messageId: "msg-1",
});
const replayAttempt = await firstGuard.shouldProcessMessage({
accountId: "account-a",
roomToken: "room-1",
messageId: "msg-1",
});
const secondGuard = createNextcloudTalkReplayGuard({ stateDir });
const restartReplayAttempt = await secondGuard.shouldProcessMessage({
accountId: "account-a",
roomToken: "room-1",
messageId: "msg-1",
});
expect(firstAttempt).toBe(true);
expect(replayAttempt).toBe(false);
expect(restartReplayAttempt).toBe(false);
});
it("scopes replay state by account namespace", async () => {
const stateDir = await makeTempDir();
const guard = createNextcloudTalkReplayGuard({ stateDir });
const accountAFirst = await guard.shouldProcessMessage({
accountId: "account-a",
roomToken: "room-1",
messageId: "msg-9",
});
const accountBFirst = await guard.shouldProcessMessage({
accountId: "account-b",
roomToken: "room-1",
messageId: "msg-9",
});
expect(accountAFirst).toBe(true);
expect(accountBFirst).toBe(true);
});
});

View File

@@ -1,66 +0,0 @@
import { describe, expect, it } from "vitest";
import {
extractNextcloudTalkHeaders,
generateNextcloudTalkSignature,
verifyNextcloudTalkSignature,
} from "./signature.js";
describe("nextcloud talk signature helpers", () => {
it("verifies generated signatures against the same body and secret", () => {
const body = JSON.stringify({ hello: "world" });
const generated = generateNextcloudTalkSignature({
body,
secret: "secret-123",
});
expect(generated.random).toMatch(/^[0-9a-f]{64}$/);
expect(generated.signature).toMatch(/^[0-9a-f]{64}$/);
expect(
verifyNextcloudTalkSignature({
signature: generated.signature,
random: generated.random,
body,
secret: "secret-123",
}),
).toBe(true);
});
it("rejects missing fields and mismatched signatures", () => {
expect(
verifyNextcloudTalkSignature({
signature: "",
random: "abc",
body: "body",
secret: "secret",
}),
).toBe(false);
expect(
verifyNextcloudTalkSignature({
signature: "deadbeef",
random: "abc",
body: "body",
secret: "secret",
}),
).toBe(false);
});
it("extracts normalized webhook headers", () => {
expect(
extractNextcloudTalkHeaders({
"x-nextcloud-talk-signature": "sig",
"x-nextcloud-talk-random": "rand",
"x-nextcloud-talk-backend": "backend",
}),
).toEqual({
signature: "sig",
random: "rand",
backend: "backend",
});
expect(
extractNextcloudTalkHeaders({
"X-Nextcloud-Talk-Signature": "sig",
}),
).toBeNull();
});
});