this repo has no description

feat: add unit tests

dunkirk.sh 67129005 7af20c9e

verified
+44
src/lib/avatars.test.ts
···
+
import { describe, expect, test } from "bun:test";
+
import { getAvatarForNick } from "./avatars";
+
+
describe("getAvatarForNick", () => {
+
test("returns a valid URL", () => {
+
const avatar = getAvatarForNick("testnick");
+
expect(avatar).toBeString();
+
expect(avatar).toStartWith("https://");
+
});
+
+
test("returns consistent avatar for same nick", () => {
+
const avatar1 = getAvatarForNick("alice");
+
const avatar2 = getAvatarForNick("alice");
+
expect(avatar1).toBe(avatar2);
+
});
+
+
test("returns different avatars for different nicks", () => {
+
const avatar1 = getAvatarForNick("alice");
+
const avatar2 = getAvatarForNick("bob");
+
+
// They might occasionally be the same due to hash collisions,
+
// but let's test they can be different
+
expect(avatar1).toBeString();
+
expect(avatar2).toBeString();
+
});
+
+
test("handles empty string", () => {
+
const avatar = getAvatarForNick("");
+
expect(avatar).toBeString();
+
expect(avatar).toStartWith("https://");
+
});
+
+
test("handles special characters", () => {
+
const avatar = getAvatarForNick("user-123_test");
+
expect(avatar).toBeString();
+
expect(avatar).toStartWith("https://");
+
});
+
+
test("handles unicode characters", () => {
+
const avatar = getAvatarForNick("用户名");
+
expect(avatar).toBeString();
+
expect(avatar).toStartWith("https://");
+
});
+
});
+160
src/lib/db.test.ts
···
+
import { afterEach, describe, expect, test } from "bun:test";
+
import { channelMappings, userMappings } from "./db";
+
+
describe("channelMappings", () => {
+
const testSlackChannel = "C123TEST";
+
const testIrcChannel = "#test-channel";
+
+
afterEach(() => {
+
// Cleanup test data
+
try {
+
channelMappings.delete(testSlackChannel);
+
} catch {
+
// Ignore if doesn't exist
+
}
+
});
+
+
test("creates a channel mapping", () => {
+
channelMappings.create(testSlackChannel, testIrcChannel);
+
const mapping = channelMappings.getBySlackChannel(testSlackChannel);
+
+
expect(mapping).toBeDefined();
+
expect(mapping?.slack_channel_id).toBe(testSlackChannel);
+
expect(mapping?.irc_channel).toBe(testIrcChannel);
+
});
+
+
test("retrieves mapping by Slack channel ID", () => {
+
channelMappings.create(testSlackChannel, testIrcChannel);
+
const mapping = channelMappings.getBySlackChannel(testSlackChannel);
+
+
expect(mapping).not.toBeNull();
+
expect(mapping?.irc_channel).toBe(testIrcChannel);
+
});
+
+
test("retrieves mapping by IRC channel", () => {
+
channelMappings.create(testSlackChannel, testIrcChannel);
+
const mapping = channelMappings.getByIrcChannel(testIrcChannel);
+
+
expect(mapping).not.toBeNull();
+
expect(mapping?.slack_channel_id).toBe(testSlackChannel);
+
});
+
+
test("returns null for non-existent mapping", () => {
+
const mapping = channelMappings.getBySlackChannel("C999NOTFOUND");
+
expect(mapping).toBeNull();
+
});
+
+
test("deletes a channel mapping", () => {
+
channelMappings.create(testSlackChannel, testIrcChannel);
+
channelMappings.delete(testSlackChannel);
+
+
const mapping = channelMappings.getBySlackChannel(testSlackChannel);
+
expect(mapping).toBeNull();
+
});
+
+
test("replaces existing mapping on create", () => {
+
channelMappings.create(testSlackChannel, "#old-channel");
+
channelMappings.create(testSlackChannel, testIrcChannel);
+
+
const mapping = channelMappings.getBySlackChannel(testSlackChannel);
+
expect(mapping?.irc_channel).toBe(testIrcChannel);
+
});
+
+
test("getAll returns all mappings", () => {
+
const testChannel2 = "C456TEST";
+
const testIrc2 = "#another-channel";
+
+
channelMappings.create(testSlackChannel, testIrcChannel);
+
channelMappings.create(testChannel2, testIrc2);
+
+
const all = channelMappings.getAll();
+
const testMappings = all.filter(
+
(m) =>
+
m.slack_channel_id === testSlackChannel ||
+
m.slack_channel_id === testChannel2,
+
);
+
+
expect(testMappings.length).toBeGreaterThanOrEqual(2);
+
+
// Cleanup
+
channelMappings.delete(testChannel2);
+
});
+
});
+
+
describe("userMappings", () => {
+
const testSlackUser = "U123TEST";
+
const testIrcNick = "testnick";
+
+
afterEach(() => {
+
// Cleanup test data
+
try {
+
userMappings.delete(testSlackUser);
+
} catch {
+
// Ignore if doesn't exist
+
}
+
});
+
+
test("creates a user mapping", () => {
+
userMappings.create(testSlackUser, testIrcNick);
+
const mapping = userMappings.getBySlackUser(testSlackUser);
+
+
expect(mapping).toBeDefined();
+
expect(mapping?.slack_user_id).toBe(testSlackUser);
+
expect(mapping?.irc_nick).toBe(testIrcNick);
+
});
+
+
test("retrieves mapping by Slack user ID", () => {
+
userMappings.create(testSlackUser, testIrcNick);
+
const mapping = userMappings.getBySlackUser(testSlackUser);
+
+
expect(mapping).not.toBeNull();
+
expect(mapping?.irc_nick).toBe(testIrcNick);
+
});
+
+
test("retrieves mapping by IRC nick", () => {
+
userMappings.create(testSlackUser, testIrcNick);
+
const mapping = userMappings.getByIrcNick(testIrcNick);
+
+
expect(mapping).not.toBeNull();
+
expect(mapping?.slack_user_id).toBe(testSlackUser);
+
});
+
+
test("returns null for non-existent mapping", () => {
+
const mapping = userMappings.getBySlackUser("U999NOTFOUND");
+
expect(mapping).toBeNull();
+
});
+
+
test("deletes a user mapping", () => {
+
userMappings.create(testSlackUser, testIrcNick);
+
userMappings.delete(testSlackUser);
+
+
const mapping = userMappings.getBySlackUser(testSlackUser);
+
expect(mapping).toBeNull();
+
});
+
+
test("replaces existing mapping on create", () => {
+
userMappings.create(testSlackUser, "oldnick");
+
userMappings.create(testSlackUser, testIrcNick);
+
+
const mapping = userMappings.getBySlackUser(testSlackUser);
+
expect(mapping?.irc_nick).toBe(testIrcNick);
+
});
+
+
test("getAll returns all mappings", () => {
+
const testUser2 = "U456TEST";
+
const testNick2 = "anothernick";
+
+
userMappings.create(testSlackUser, testIrcNick);
+
userMappings.create(testUser2, testNick2);
+
+
const all = userMappings.getAll();
+
const testMappings = all.filter(
+
(m) => m.slack_user_id === testSlackUser || m.slack_user_id === testUser2,
+
);
+
+
expect(testMappings.length).toBeGreaterThanOrEqual(2);
+
+
// Cleanup
+
userMappings.delete(testUser2);
+
});
+
});
+56
src/lib/mentions.test.ts
···
+
import { describe, expect, test } from "bun:test";
+
import { convertIrcMentionsToSlack } from "./mentions";
+
import { userMappings } from "./db";
+
+
describe("convertIrcMentionsToSlack", () => {
+
test("converts @mention when user mapping exists", () => {
+
// Setup test data
+
userMappings.create("U123", "testuser");
+
+
const result = convertIrcMentionsToSlack("Hey @testuser how are you?");
+
expect(result).toBe("Hey <@U123> how are you?");
+
+
// Cleanup
+
userMappings.delete("U123");
+
});
+
+
test("leaves @mention unchanged when no mapping exists", () => {
+
const result = convertIrcMentionsToSlack("Hey @unknownuser");
+
expect(result).toBe("Hey @unknownuser");
+
});
+
+
test("converts nick: mention when user mapping exists", () => {
+
userMappings.create("U456", "alice");
+
+
const result = convertIrcMentionsToSlack("alice: hello");
+
expect(result).toBe("<@U456>: hello");
+
+
userMappings.delete("U456");
+
});
+
+
test("leaves nick: unchanged when no mapping exists", () => {
+
const result = convertIrcMentionsToSlack("bob: hello");
+
expect(result).toBe("bob: hello");
+
});
+
+
test("handles multiple mentions", () => {
+
userMappings.create("U123", "alice");
+
userMappings.create("U456", "bob");
+
+
const result = convertIrcMentionsToSlack("@alice and bob: hello!");
+
expect(result).toBe("<@U123> and <@U456>: hello!");
+
+
userMappings.delete("U123");
+
userMappings.delete("U456");
+
});
+
+
test("handles mixed mapped and unmapped mentions", () => {
+
userMappings.create("U123", "alice");
+
+
const result = convertIrcMentionsToSlack("@alice and @unknown user");
+
expect(result).toContain("<@U123>");
+
expect(result).toContain("@unknown");
+
+
userMappings.delete("U123");
+
});
+
});
+137
src/lib/parser.test.ts
···
+
import { describe, expect, test } from "bun:test";
+
import { parseIRCFormatting, parseSlackMarkdown } from "./parser";
+
+
describe("parseSlackMarkdown", () => {
+
test("converts channel mentions with name", () => {
+
const result = parseSlackMarkdown("Check out <#C123ABC|general>");
+
expect(result).toBe("Check out #general");
+
});
+
+
test("converts channel mentions without name", () => {
+
const result = parseSlackMarkdown("Check out <#C123ABC>");
+
expect(result).toBe("Check out #channel");
+
});
+
+
test("converts links with text", () => {
+
const result = parseSlackMarkdown(
+
"Visit <https://example.com|Example Site>",
+
);
+
expect(result).toBe("Visit Example Site (https://example.com)");
+
});
+
+
test("converts links without text", () => {
+
const result = parseSlackMarkdown("Visit <https://example.com>");
+
expect(result).toBe("Visit https://example.com");
+
});
+
+
test("converts mailto links", () => {
+
const result = parseSlackMarkdown(
+
"Email <mailto:test@example.com|Support>",
+
);
+
expect(result).toBe("Email Support <test@example.com>");
+
});
+
+
test("converts special mentions", () => {
+
expect(parseSlackMarkdown("<!here> everyone")).toBe("@here everyone");
+
expect(parseSlackMarkdown("<!channel> announcement")).toBe(
+
"@channel announcement",
+
);
+
expect(parseSlackMarkdown("<!everyone> alert")).toBe("@everyone alert");
+
});
+
+
test("converts user group mentions", () => {
+
const result = parseSlackMarkdown("Hey <!subteam^GROUP123|developers>");
+
expect(result).toBe("Hey @developers");
+
});
+
+
test("converts bold formatting", () => {
+
const result = parseSlackMarkdown("This is *bold* text");
+
expect(result).toBe("This is \x02bold\x02 text");
+
});
+
+
test("converts italic formatting", () => {
+
const result = parseSlackMarkdown("This is _italic_ text");
+
expect(result).toBe("This is \x1Ditalic\x1D text");
+
});
+
+
test("strips strikethrough formatting", () => {
+
const result = parseSlackMarkdown("This is ~strikethrough~ text");
+
expect(result).toBe("This is strikethrough text");
+
});
+
+
test("strips code blocks", () => {
+
const result = parseSlackMarkdown("Code: ```const x = 1;```");
+
expect(result).toBe("Code: const x = 1;");
+
});
+
+
test("strips inline code", () => {
+
const result = parseSlackMarkdown("Run `npm install` to start");
+
expect(result).toBe("Run npm install to start");
+
});
+
+
test("unescapes HTML entities", () => {
+
const result = parseSlackMarkdown("a &lt; b &amp;&amp; c &gt; d");
+
expect(result).toBe("a < b && c > d");
+
});
+
+
test("handles mixed formatting", () => {
+
const result = parseSlackMarkdown(
+
"*Bold* and _italic_ with <https://example.com|link>",
+
);
+
expect(result).toBe(
+
"\x02Bold\x02 and \x1Ditalic\x1D with link (https://example.com)",
+
);
+
});
+
});
+
+
describe("parseIRCFormatting", () => {
+
test("strips IRC color codes", () => {
+
const result = parseIRCFormatting("\x0304red text\x03 normal");
+
expect(result).toBe("red text normal");
+
});
+
+
test("converts bold formatting", () => {
+
const result = parseIRCFormatting("This is \x02bold\x02 text");
+
expect(result).toBe("This is *bold* text");
+
});
+
+
test("converts italic formatting", () => {
+
const result = parseIRCFormatting("This is \x1Ditalic\x1D text");
+
expect(result).toBe("This is _italic_ text");
+
});
+
+
test("converts underline to italic", () => {
+
const result = parseIRCFormatting("This is \x1Funderline\x1F text");
+
expect(result).toBe("This is _underline_ text");
+
});
+
+
test("strips reverse/inverse formatting", () => {
+
const result = parseIRCFormatting("Normal \x16reversed\x16 normal");
+
expect(result).toBe("Normal reversed normal");
+
});
+
+
test("strips reset formatting", () => {
+
const result = parseIRCFormatting("Text\x0F reset");
+
expect(result).toBe("Text reset");
+
});
+
+
test("escapes special Slack characters", () => {
+
const result = parseIRCFormatting("a < b & c > d");
+
expect(result).toBe("a &lt; b &amp; c &gt; d");
+
});
+
+
test("handles mixed formatting", () => {
+
const result = parseIRCFormatting("\x02Bold\x02 and \x1Ditalic\x1D");
+
expect(result).toBe("*Bold* and _italic_");
+
});
+
+
test("handles nested formatting codes", () => {
+
const result = parseIRCFormatting("\x02\x1Dbold italic\x1D\x02");
+
expect(result).toBe("*_bold italic_*");
+
});
+
+
test("handles color codes with background", () => {
+
const result = parseIRCFormatting("\x0304,08red on yellow\x03");
+
expect(result).toBe("red on yellow");
+
});
+
});
+106
src/lib/threads.test.ts
···
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
+
import {
+
generateThreadId,
+
getThreadByThreadId,
+
isFirstThreadMessage,
+
updateThreadTimestamp,
+
} from "./threads";
+
import { threadTimestamps } from "./db";
+
+
describe("threads", () => {
+
const testChannelId = "C123TEST";
+
const testThreadTs = "1234567890.123456";
+
+
afterEach(() => {
+
// Clean up test data
+
const thread = threadTimestamps.get(testThreadTs);
+
if (thread) {
+
threadTimestamps.cleanup(Date.now() + 1000);
+
}
+
});
+
+
describe("generateThreadId", () => {
+
test("generates a 5-character thread ID", () => {
+
const threadId = generateThreadId(testThreadTs);
+
expect(threadId).toBeString();
+
expect(threadId.length).toBe(5);
+
});
+
+
test("generates consistent IDs for same input", () => {
+
const id1 = generateThreadId(testThreadTs);
+
const id2 = generateThreadId(testThreadTs);
+
expect(id1).toBe(id2);
+
});
+
+
test("generates different IDs for different inputs", () => {
+
const id1 = generateThreadId("1234567890.123456");
+
const id2 = generateThreadId("9876543210.654321");
+
expect(id1).not.toBe(id2);
+
});
+
+
test("generates alphanumeric IDs", () => {
+
const threadId = generateThreadId(testThreadTs);
+
expect(threadId).toMatch(/^[a-z0-9]{5}$/);
+
});
+
});
+
+
describe("isFirstThreadMessage", () => {
+
test("returns true for new thread", () => {
+
const result = isFirstThreadMessage(testThreadTs);
+
expect(result).toBe(true);
+
});
+
+
test("returns false for existing thread", () => {
+
updateThreadTimestamp(testThreadTs, testChannelId);
+
const result = isFirstThreadMessage(testThreadTs);
+
expect(result).toBe(false);
+
});
+
});
+
+
describe("updateThreadTimestamp", () => {
+
test("creates new thread entry", () => {
+
const threadId = updateThreadTimestamp(testThreadTs, testChannelId);
+
+
expect(threadId).toBeString();
+
expect(threadId.length).toBe(5);
+
+
const thread = threadTimestamps.get(testThreadTs);
+
expect(thread).toBeDefined();
+
expect(thread?.thread_id).toBe(threadId);
+
expect(thread?.slack_channel_id).toBe(testChannelId);
+
});
+
+
test("updates existing thread timestamp", () => {
+
const threadId1 = updateThreadTimestamp(testThreadTs, testChannelId);
+
const thread1 = threadTimestamps.get(testThreadTs);
+
const timestamp1 = thread1?.last_message_time;
+
+
// Wait a bit to ensure timestamp changes
+
Bun.sleepSync(10);
+
+
const threadId2 = updateThreadTimestamp(testThreadTs, testChannelId);
+
const thread2 = threadTimestamps.get(testThreadTs);
+
const timestamp2 = thread2?.last_message_time;
+
+
expect(threadId1).toBe(threadId2);
+
expect(timestamp2).toBeGreaterThan(timestamp1!);
+
});
+
});
+
+
describe("getThreadByThreadId", () => {
+
test("retrieves thread by thread ID", () => {
+
const threadId = updateThreadTimestamp(testThreadTs, testChannelId);
+
const thread = getThreadByThreadId(threadId);
+
+
expect(thread).toBeDefined();
+
expect(thread?.thread_ts).toBe(testThreadTs);
+
expect(thread?.thread_id).toBe(threadId);
+
expect(thread?.slack_channel_id).toBe(testChannelId);
+
});
+
+
test("returns null for non-existent thread ID", () => {
+
const thread = getThreadByThreadId("xxxxx");
+
expect(thread).toBeNull();
+
});
+
});
+
});
+146
src/lib/user-cache.test.ts
···
+
import { afterEach, describe, expect, mock, test } from "bun:test";
+
import { cleanupUserCache, getUserInfo } from "./user-cache";
+
+
describe("user-cache", () => {
+
const mockSlackClient = {
+
users: {
+
info: mock(async () => ({
+
user: {
+
name: "testuser",
+
real_name: "Test User",
+
},
+
})),
+
},
+
};
+
+
afterEach(() => {
+
cleanupUserCache();
+
mockSlackClient.users.info.mockClear();
+
mockSlackClient.users.info.mockReset();
+
});
+
+
describe("getUserInfo", () => {
+
test("fetches user info from Slack on cache miss", async () => {
+
const client = {
+
users: {
+
info: mock(async () => ({
+
user: {
+
name: "testuser",
+
real_name: "Test User",
+
},
+
})),
+
},
+
};
+
+
const result = await getUserInfo("U123", client);
+
+
expect(result).toEqual({
+
name: "testuser",
+
realName: "Test User",
+
});
+
expect(client.users.info).toHaveBeenCalledTimes(1);
+
});
+
+
test("returns cached data on cache hit", async () => {
+
const client = {
+
users: {
+
info: mock(async () => ({
+
user: {
+
name: "testuser",
+
real_name: "Test User",
+
},
+
})),
+
},
+
};
+
+
// First call - cache miss
+
await getUserInfo("U124", client);
+
expect(client.users.info).toHaveBeenCalledTimes(1);
+
+
// Second call - cache hit
+
const result = await getUserInfo("U124", client);
+
expect(result).toEqual({
+
name: "testuser",
+
realName: "Test User",
+
});
+
expect(client.users.info).toHaveBeenCalledTimes(1); // Still 1
+
});
+
+
test("uses name as fallback for real_name", async () => {
+
const client = {
+
users: {
+
info: mock(async () => ({
+
user: {
+
name: "testuser",
+
},
+
})),
+
},
+
};
+
+
const result = await getUserInfo("U456", client);
+
expect(result).toEqual({
+
name: "testuser",
+
realName: "testuser",
+
});
+
});
+
+
test("handles missing user data gracefully", async () => {
+
const client = {
+
users: {
+
info: mock(async () => ({})),
+
},
+
};
+
+
const result = await getUserInfo("U789", client);
+
expect(result).toEqual({
+
name: "Unknown",
+
realName: "Unknown",
+
});
+
});
+
+
test("handles Slack API errors", async () => {
+
const client = {
+
users: {
+
info: mock(async () => {
+
throw new Error("API Error");
+
}),
+
},
+
};
+
+
const result = await getUserInfo("U999", client);
+
expect(result).toBeNull();
+
});
+
+
test("caches different users separately", async () => {
+
const client = {
+
users: {
+
info: mock(async ({ user }: { user: string }) => {
+
if (user === "U111") {
+
return { user: { name: "alice", real_name: "Alice" } };
+
}
+
return { user: { name: "bob", real_name: "Bob" } };
+
}),
+
},
+
};
+
+
const result1 = await getUserInfo("U111", client);
+
const result2 = await getUserInfo("U222", client);
+
+
expect(result1?.name).toBe("alice");
+
expect(result2?.name).toBe("bob");
+
expect(client.users.info).toHaveBeenCalledTimes(2);
+
+
// Both should be cached now
+
await getUserInfo("U111", client);
+
await getUserInfo("U222", client);
+
expect(client.users.info).toHaveBeenCalledTimes(2); // Still 2
+
});
+
});
+
+
describe("cleanupUserCache", () => {
+
test("cleanup runs without errors", () => {
+
// Just test that cleanup doesn't throw
+
expect(() => cleanupUserCache()).not.toThrow();
+
});
+
});
+
});