Files
erika/lib/auth/discord-user-core.test.ts
T

132 lines
4.0 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import {
reconcileDiscordUser,
type DiscordIdentity,
type DiscordUserOperations,
} from "@/lib/auth/discord-user-core";
type MemoryUser = {
id: string;
discordId: string;
name: string;
image: string | null;
email: string | null;
};
function createMemoryStore() {
const users = new Map<string, MemoryUser>();
const userIdByDiscord = new Map<string, string>();
const accounts = new Map<string, { userId: string; fields: Record<string, unknown> }>();
let nextUserId = 1;
const operations: DiscordUserOperations<MemoryUser> = {
async findLinkedUserId(discordId) {
return accounts.get(discordId)?.userId ?? null;
},
async upsertUser(identity) {
let userId = userIdByDiscord.get(identity.discordId);
if (!userId) {
userId = `user-${nextUserId++}`;
userIdByDiscord.set(identity.discordId, userId);
users.set(userId, {
id: userId,
discordId: identity.discordId,
name: identity.displayName,
image: identity.avatarUrl,
email: identity.email ?? null,
});
}
return userId;
},
async insertAccount(userId, discordId, account) {
if (!accounts.has(discordId)) {
accounts.set(discordId, {
userId,
fields: { type: account?.type ?? "oauth" },
});
}
},
async updateUser(userId, identity) {
const user = users.get(userId);
if (!user) return null;
const updated = {
...user,
discordId: identity.discordId,
name: identity.displayName,
image: identity.avatarUrl,
...(identity.email !== undefined ? { email: identity.email } : {}),
};
users.set(userId, updated);
return updated;
},
async updateAccount(discordId, update) {
const account = accounts.get(discordId);
if (account) account.fields = { ...account.fields, ...update };
},
};
return { users, accounts, operations };
}
const botIdentity: DiscordIdentity = {
discordId: "123456789012345678",
displayName: "Bot-created user",
avatarUrl: "https://cdn.discordapp.com/avatar.png",
};
describe("Discord account reconciliation", () => {
test("creates one user and one account for a new bot sender", async () => {
const store = createMemoryStore();
const user = await reconcileDiscordUser(botIdentity, store.operations);
expect(user.id).toBe("user-1");
expect(store.users.size).toBe(1);
expect(store.accounts.size).toBe(1);
expect(store.accounts.get(botIdentity.discordId)?.userId).toBe(user.id);
});
test("concurrent first uploads resolve to the same database winner", async () => {
const store = createMemoryStore();
const [first, second] = await Promise.all([
reconcileDiscordUser(botIdentity, store.operations),
reconcileDiscordUser(botIdentity, store.operations),
]);
expect(first.id).toBe(second.id);
expect(store.users.size).toBe(1);
expect(store.accounts.size).toBe(1);
});
test("OAuth reuses a bot-created user and bot refreshes preserve email", async () => {
const store = createMemoryStore();
const botUser = await reconcileDiscordUser(botIdentity, store.operations);
const oauthUser = await reconcileDiscordUser(
{
...botIdentity,
displayName: "OAuth name",
email: "[email protected]",
account: {
type: "oauth",
access_token: "access-token",
refresh_token: "refresh-token",
},
},
store.operations,
);
const refreshedByBot = await reconcileDiscordUser(
{ ...botIdentity, displayName: "Latest Discord name" },
store.operations,
);
expect(oauthUser.id).toBe(botUser.id);
expect(refreshedByBot.id).toBe(botUser.id);
expect(refreshedByBot.email).toBe("[email protected]");
expect(store.users.size).toBe(1);
expect(store.accounts.size).toBe(1);
expect(store.accounts.get(botIdentity.discordId)?.fields).toMatchObject({
access_token: "access-token",
refresh_token: "refresh-token",
});
});
});