132 lines
4.0 KiB
TypeScript
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",
|
|
});
|
|
});
|
|
});
|