test: clear synology chat channel broad matchers

This commit is contained in:
Peter Steinberger
2026-05-10 15:42:05 +01:00
parent 2c4d9bea82
commit f297deeffc

View File

@@ -26,6 +26,14 @@ function makeSecurityAccount(
return { ...securityAccountDefaults, ...overrides };
}
function expectIncludesSubstring(values: readonly string[], expected: string): void {
expect(values.some((value) => value.includes(expected))).toBe(true);
}
function mockStringMessages(mock: { mock: { calls: unknown[][] } }): string[] {
return mock.mock.calls.map((call) => String(call[0] ?? ""));
}
const clientModule = await import("./client.js");
const gatewayRuntimeModule = await import("./gateway-runtime.js");
const mockSendMessage = vi.spyOn(clientModule, "sendMessage").mockResolvedValue(true);
@@ -112,16 +120,14 @@ describe("createSynologyChatPlugin", () => {
};
const plugin = createSynologyChatPlugin();
const account = plugin.config.resolveAccount(cfg, "office");
expect(account).toMatchObject({
accountId: "office",
token: "office-token",
incomingUrl: "https://nas/base",
nasHost: "nas-base",
allowedUserIds: ["base-user"],
rateLimitPerMinute: 45,
botName: "Base Bot",
allowInsecureSsl: true,
});
expect(account.accountId).toBe("office");
expect(account.token).toBe("office-token");
expect(account.incomingUrl).toBe("https://nas/base");
expect(account.nasHost).toBe("nas-base");
expect(account.allowedUserIds).toEqual(["base-user"]);
expect(account.rateLimitPerMinute).toBe(45);
expect(account.botName).toBe("Base Bot");
expect(account.allowInsecureSsl).toBe(true);
});
it("defaultAccountId returns 'default'", () => {
@@ -256,23 +262,21 @@ describe("createSynologyChatPlugin", () => {
const plugin = createSynologyChatPlugin();
const account = makeSecurityAccount({ token: "" });
const warnings = plugin.security.collectWarnings({ cfg: {}, account });
expect(warnings).toEqual(expect.arrayContaining([expect.stringContaining("token")]));
expectIncludesSubstring(warnings, "token");
});
it("warns when allowInsecureSsl is true", () => {
const plugin = createSynologyChatPlugin();
const account = makeSecurityAccount({ allowInsecureSsl: true });
const warnings = plugin.security.collectWarnings({ cfg: {}, account });
expect(warnings).toEqual(expect.arrayContaining([expect.stringContaining("SSL")]));
expectIncludesSubstring(warnings, "SSL");
});
it("warns when dangerous name matching is enabled", () => {
const plugin = createSynologyChatPlugin();
const account = makeSecurityAccount({ dangerouslyAllowNameMatching: true });
const warnings = plugin.security.collectWarnings({ cfg: {}, account });
expect(warnings).toEqual(
expect.arrayContaining([expect.stringContaining("dangerouslyAllowNameMatching")]),
);
expectIncludesSubstring(warnings, "dangerouslyAllowNameMatching");
});
it("warns when inherited shared webhookPath is dangerously re-enabled", () => {
@@ -283,36 +287,28 @@ describe("createSynologyChatPlugin", () => {
dangerouslyAllowInheritedWebhookPath: true,
});
const warnings = plugin.security.collectWarnings({ cfg: {}, account });
expect(warnings).toEqual(
expect.arrayContaining([
expect.stringContaining("dangerouslyAllowInheritedWebhookPath=true"),
]),
);
expectIncludesSubstring(warnings, "dangerouslyAllowInheritedWebhookPath=true");
});
it("warns when dmPolicy is open", () => {
const plugin = createSynologyChatPlugin();
const account = makeSecurityAccount({ dmPolicy: "open", allowedUserIds: ["*"] });
const warnings = plugin.security.collectWarnings({ cfg: {}, account });
expect(warnings).toEqual(expect.arrayContaining([expect.stringContaining("open")]));
expectIncludesSubstring(warnings, "open");
});
it("warns when dmPolicy is open and allowedUserIds is empty", () => {
const plugin = createSynologyChatPlugin();
const account = makeSecurityAccount({ dmPolicy: "open", allowedUserIds: [] });
const warnings = plugin.security.collectWarnings({ cfg: {}, account });
expect(warnings).toEqual(
expect.arrayContaining([expect.stringContaining("empty allowedUserIds")]),
);
expectIncludesSubstring(warnings, "empty allowedUserIds");
});
it("warns when dmPolicy is allowlist and allowedUserIds is empty", () => {
const plugin = createSynologyChatPlugin();
const account = makeSecurityAccount();
const warnings = plugin.security.collectWarnings({ cfg: {}, account });
expect(warnings).toEqual(
expect.arrayContaining([expect.stringContaining("empty allowedUserIds")]),
);
expectIncludesSubstring(warnings, "empty allowedUserIds");
});
it("warns when named multi-account routes inherit a shared webhookPath", () => {
@@ -320,9 +316,7 @@ describe("createSynologyChatPlugin", () => {
const cfg = makeSharedWebhookConfig();
const account = plugin.config.resolveAccount(cfg, "alerts");
const warnings = plugin.security.collectWarnings({ cfg, account });
expect(warnings).toEqual(
expect.arrayContaining([expect.stringContaining("must set an explicit webhookPath")]),
);
expectIncludesSubstring(warnings, "must set an explicit webhookPath");
});
it("warns when enabled accounts share the same exact webhookPath", () => {
@@ -342,9 +336,7 @@ describe("createSynologyChatPlugin", () => {
};
const account = plugin.config.resolveAccount(cfg, "alerts");
const warnings = plugin.security.collectWarnings({ cfg, account });
expect(warnings).toEqual(
expect.arrayContaining([expect.stringContaining("conflicts on webhookPath")]),
);
expectIncludesSubstring(warnings, "conflicts on webhookPath");
});
it("returns no warnings for fully configured account", () => {
@@ -410,42 +402,41 @@ describe("createSynologyChatPlugin", () => {
},
};
await expect(
verifyChannelMessageAdapterCapabilityProofs({
adapterName: "synology-chat",
adapter: plugin.message,
proofs: {
text: async () => {
const result = await plugin.message.send?.text?.({
cfg,
text: "hello",
to: "user1",
});
expect(result?.receipt.parts[0]?.kind).toBe("text");
expect(result?.receipt.platformMessageIds).toHaveLength(1);
},
media: async () => {
const result = await plugin.message.send?.media?.({
cfg,
text: "image",
mediaUrl: "https://example.com/img.png",
to: "user1",
});
expect(result?.receipt.parts[0]?.kind).toBe("media");
expect(result?.receipt.platformMessageIds).toHaveLength(1);
},
messageSendingHooks: () => {
expect(plugin.message.durableFinal?.capabilities?.messageSendingHooks).toBe(true);
},
const results = await verifyChannelMessageAdapterCapabilityProofs({
adapterName: "synology-chat",
adapter: plugin.message,
proofs: {
text: async () => {
const result = await plugin.message.send?.text?.({
cfg,
text: "hello",
to: "user1",
});
expect(result?.receipt.parts[0]?.kind).toBe("text");
expect(result?.receipt.platformMessageIds).toHaveLength(1);
},
}),
).resolves.toEqual(
expect.arrayContaining([
{ capability: "text", status: "verified" },
{ capability: "media", status: "verified" },
{ capability: "messageSendingHooks", status: "verified" },
]),
media: async () => {
const result = await plugin.message.send?.media?.({
cfg,
text: "image",
mediaUrl: "https://example.com/img.png",
to: "user1",
});
expect(result?.receipt.parts[0]?.kind).toBe("media");
expect(result?.receipt.platformMessageIds).toHaveLength(1);
},
messageSendingHooks: () => {
expect(plugin.message.durableFinal?.capabilities?.messageSendingHooks).toBe(true);
},
},
});
const statusByCapability = new Map(
results.map(({ capability, status }) => [capability, status]),
);
expect(statusByCapability.get("text")).toBe("verified");
expect(statusByCapability.get("media")).toBe("verified");
expect(statusByCapability.get("messageSendingHooks")).toBe("verified");
});
it("sendText throws when no incomingUrl", async () => {
@@ -479,10 +470,8 @@ describe("createSynologyChatPlugin", () => {
text: "hello",
to: "user1",
});
expect(result).toMatchObject({
channel: "synology-chat",
chatId: "user1",
});
expect(result.channel).toBe("synology-chat");
expect(result.chatId).toBe("user1");
expect(result.messageId).toMatch(/^sc-\d+$/);
expect(result.receipt.primaryPlatformMessageId).toBe(result.messageId);
expect(result.receipt.parts[0]?.kind).toBe("text");
@@ -605,7 +594,7 @@ describe("createSynologyChatPlugin", () => {
const result = plugin.gateway.startAccount(ctx);
await expectPendingStartAccountPromise(result, abortController);
expect(ctx.log.warn).toHaveBeenCalledWith(expect.stringContaining("empty allowedUserIds"));
expectIncludesSubstring(mockStringMessages(ctx.log.warn), "empty allowedUserIds");
expect(registerMock).not.toHaveBeenCalled();
});
@@ -623,8 +612,9 @@ describe("createSynologyChatPlugin", () => {
const result = plugin.gateway.startAccount(ctx);
await expectPendingStartAccountPromise(result, abortController);
expect(ctx.log.warn).toHaveBeenCalledWith(
expect.stringContaining("dmPolicy=open but empty allowedUserIds"),
expectIncludesSubstring(
mockStringMessages(ctx.log.warn),
"dmPolicy=open but empty allowedUserIds",
);
expect(registerMock).not.toHaveBeenCalled();
});
@@ -639,9 +629,7 @@ describe("createSynologyChatPlugin", () => {
const result = plugin.gateway.startAccount(ctx);
await expectPendingStartAccountPromise(result, abortController);
expect(ctx.log.warn).toHaveBeenCalledWith(
expect.stringContaining("must set an explicit webhookPath"),
);
expectIncludesSubstring(mockStringMessages(ctx.log.warn), "must set an explicit webhookPath");
expect(registerMock).not.toHaveBeenCalled();
});
@@ -656,9 +644,7 @@ describe("createSynologyChatPlugin", () => {
const result = plugin.gateway.startAccount(ctx);
await expectPendingStartAccountPromise(result, abortController);
expect(ctx.log.warn).toHaveBeenCalledWith(
expect.stringContaining("conflicts on webhookPath"),
);
expectIncludesSubstring(mockStringMessages(ctx.log.warn), "conflicts on webhookPath");
expect(registerMock).not.toHaveBeenCalled();
});