feat(server): wire Discord DM pairing notifications

This commit is contained in:
nav
2026-04-09 04:06:06 +00:00
parent b67166fd12
commit 2972c4750e
4 changed files with 257 additions and 43 deletions

107
tests/notifications.test.ts Normal file
View File

@@ -0,0 +1,107 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createDiscordNotificationService,
formatPairingMessage,
type DiscordFetch
} from "../plugin/notifications/discord.js";
const request = {
identifier: "client-a",
pairingCode: "PAIR-1234-CODE",
expiresAt: 1_710_000_300,
ttlSeconds: 300,
createdAt: 1_710_000_000
};
afterEach(() => {
vi.restoreAllMocks();
});
describe("Discord notification service", () => {
it("formats pairing requests as a DM-friendly message", () => {
const message = formatPairingMessage(request);
expect(message).toContain("Yonexus Pairing Request");
expect(message).toContain("`client-a`");
expect(message).toContain("`PAIR-1234-CODE`");
expect(message).toContain("TTL:** 300 seconds");
});
it("creates a DM channel and posts the pairing message", async () => {
const fetcher = vi
.fn<DiscordFetch>()
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ id: "dm-channel-1" })
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ id: "message-1" })
});
const service = createDiscordNotificationService(
{
botToken: "discord-bot-token",
adminUserId: "123456789012345678"
},
{ fetcher }
);
await expect(service.sendPairingNotification(request)).resolves.toBe(true);
expect(fetcher).toHaveBeenCalledTimes(2);
expect(fetcher).toHaveBeenNthCalledWith(
1,
"https://discord.com/api/v10/users/@me/channels",
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
Authorization: "Bot discord-bot-token"
}),
body: JSON.stringify({ recipient_id: "123456789012345678" })
})
);
expect(fetcher).toHaveBeenNthCalledWith(
2,
"https://discord.com/api/v10/channels/dm-channel-1/messages",
expect.objectContaining({
method: "POST"
})
);
});
it("returns false when Discord rejects DM channel creation", async () => {
const fetcher = vi.fn<DiscordFetch>().mockResolvedValueOnce({
ok: false,
status: 403,
json: async () => ({ message: "Missing Access" })
});
const service = createDiscordNotificationService(
{
botToken: "discord-bot-token",
adminUserId: "123456789012345678"
},
{ fetcher }
);
await expect(service.sendPairingNotification(request)).resolves.toBe(false);
expect(fetcher).toHaveBeenCalledTimes(1);
});
it("returns false when config is missing required Discord credentials", async () => {
const fetcher = vi.fn<DiscordFetch>();
const service = createDiscordNotificationService(
{
botToken: "",
adminUserId: ""
},
{ fetcher }
);
await expect(service.sendPairingNotification(request)).resolves.toBe(false);
expect(fetcher).not.toHaveBeenCalled();
});
});

View File

@@ -98,8 +98,27 @@ function createMockTransport() {
};
}
function stubDiscordFetchSuccess() {
vi.stubGlobal(
"fetch",
vi
.fn()
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ id: "dm-channel-1" })
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ id: "message-1" })
})
);
}
describe("Yonexus.Server runtime flow", () => {
it("runs hello -> pair_request for an unpaired client", async () => {
stubDiscordFetchSuccess();
const store = createMockStore();
const transportState = createMockTransport();
const runtime = createYonexusServerRuntime({
@@ -160,6 +179,7 @@ describe("Yonexus.Server runtime flow", () => {
});
it("completes pair_confirm -> auth_request -> heartbeat for a client", async () => {
stubDiscordFetchSuccess();
let now = 1_710_000_000;
const keyPair = await generateKeyPair();
const store = createMockStore();