feat(server): wire Discord DM pairing notifications
This commit is contained in:
107
tests/notifications.test.ts
Normal file
107
tests/notifications.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user