this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add unit tests

+649
+44
src/lib/avatars.test.ts
··· 1 + import { describe, expect, test } from "bun:test"; 2 + import { getAvatarForNick } from "./avatars"; 3 + 4 + describe("getAvatarForNick", () => { 5 + test("returns a valid URL", () => { 6 + const avatar = getAvatarForNick("testnick"); 7 + expect(avatar).toBeString(); 8 + expect(avatar).toStartWith("https://"); 9 + }); 10 + 11 + test("returns consistent avatar for same nick", () => { 12 + const avatar1 = getAvatarForNick("alice"); 13 + const avatar2 = getAvatarForNick("alice"); 14 + expect(avatar1).toBe(avatar2); 15 + }); 16 + 17 + test("returns different avatars for different nicks", () => { 18 + const avatar1 = getAvatarForNick("alice"); 19 + const avatar2 = getAvatarForNick("bob"); 20 + 21 + // They might occasionally be the same due to hash collisions, 22 + // but let's test they can be different 23 + expect(avatar1).toBeString(); 24 + expect(avatar2).toBeString(); 25 + }); 26 + 27 + test("handles empty string", () => { 28 + const avatar = getAvatarForNick(""); 29 + expect(avatar).toBeString(); 30 + expect(avatar).toStartWith("https://"); 31 + }); 32 + 33 + test("handles special characters", () => { 34 + const avatar = getAvatarForNick("user-123_test"); 35 + expect(avatar).toBeString(); 36 + expect(avatar).toStartWith("https://"); 37 + }); 38 + 39 + test("handles unicode characters", () => { 40 + const avatar = getAvatarForNick("用户名"); 41 + expect(avatar).toBeString(); 42 + expect(avatar).toStartWith("https://"); 43 + }); 44 + });
+160
src/lib/db.test.ts
··· 1 + import { afterEach, describe, expect, test } from "bun:test"; 2 + import { channelMappings, userMappings } from "./db"; 3 + 4 + describe("channelMappings", () => { 5 + const testSlackChannel = "C123TEST"; 6 + const testIrcChannel = "#test-channel"; 7 + 8 + afterEach(() => { 9 + // Cleanup test data 10 + try { 11 + channelMappings.delete(testSlackChannel); 12 + } catch { 13 + // Ignore if doesn't exist 14 + } 15 + }); 16 + 17 + test("creates a channel mapping", () => { 18 + channelMappings.create(testSlackChannel, testIrcChannel); 19 + const mapping = channelMappings.getBySlackChannel(testSlackChannel); 20 + 21 + expect(mapping).toBeDefined(); 22 + expect(mapping?.slack_channel_id).toBe(testSlackChannel); 23 + expect(mapping?.irc_channel).toBe(testIrcChannel); 24 + }); 25 + 26 + test("retrieves mapping by Slack channel ID", () => { 27 + channelMappings.create(testSlackChannel, testIrcChannel); 28 + const mapping = channelMappings.getBySlackChannel(testSlackChannel); 29 + 30 + expect(mapping).not.toBeNull(); 31 + expect(mapping?.irc_channel).toBe(testIrcChannel); 32 + }); 33 + 34 + test("retrieves mapping by IRC channel", () => { 35 + channelMappings.create(testSlackChannel, testIrcChannel); 36 + const mapping = channelMappings.getByIrcChannel(testIrcChannel); 37 + 38 + expect(mapping).not.toBeNull(); 39 + expect(mapping?.slack_channel_id).toBe(testSlackChannel); 40 + }); 41 + 42 + test("returns null for non-existent mapping", () => { 43 + const mapping = channelMappings.getBySlackChannel("C999NOTFOUND"); 44 + expect(mapping).toBeNull(); 45 + }); 46 + 47 + test("deletes a channel mapping", () => { 48 + channelMappings.create(testSlackChannel, testIrcChannel); 49 + channelMappings.delete(testSlackChannel); 50 + 51 + const mapping = channelMappings.getBySlackChannel(testSlackChannel); 52 + expect(mapping).toBeNull(); 53 + }); 54 + 55 + test("replaces existing mapping on create", () => { 56 + channelMappings.create(testSlackChannel, "#old-channel"); 57 + channelMappings.create(testSlackChannel, testIrcChannel); 58 + 59 + const mapping = channelMappings.getBySlackChannel(testSlackChannel); 60 + expect(mapping?.irc_channel).toBe(testIrcChannel); 61 + }); 62 + 63 + test("getAll returns all mappings", () => { 64 + const testChannel2 = "C456TEST"; 65 + const testIrc2 = "#another-channel"; 66 + 67 + channelMappings.create(testSlackChannel, testIrcChannel); 68 + channelMappings.create(testChannel2, testIrc2); 69 + 70 + const all = channelMappings.getAll(); 71 + const testMappings = all.filter( 72 + (m) => 73 + m.slack_channel_id === testSlackChannel || 74 + m.slack_channel_id === testChannel2, 75 + ); 76 + 77 + expect(testMappings.length).toBeGreaterThanOrEqual(2); 78 + 79 + // Cleanup 80 + channelMappings.delete(testChannel2); 81 + }); 82 + }); 83 + 84 + describe("userMappings", () => { 85 + const testSlackUser = "U123TEST"; 86 + const testIrcNick = "testnick"; 87 + 88 + afterEach(() => { 89 + // Cleanup test data 90 + try { 91 + userMappings.delete(testSlackUser); 92 + } catch { 93 + // Ignore if doesn't exist 94 + } 95 + }); 96 + 97 + test("creates a user mapping", () => { 98 + userMappings.create(testSlackUser, testIrcNick); 99 + const mapping = userMappings.getBySlackUser(testSlackUser); 100 + 101 + expect(mapping).toBeDefined(); 102 + expect(mapping?.slack_user_id).toBe(testSlackUser); 103 + expect(mapping?.irc_nick).toBe(testIrcNick); 104 + }); 105 + 106 + test("retrieves mapping by Slack user ID", () => { 107 + userMappings.create(testSlackUser, testIrcNick); 108 + const mapping = userMappings.getBySlackUser(testSlackUser); 109 + 110 + expect(mapping).not.toBeNull(); 111 + expect(mapping?.irc_nick).toBe(testIrcNick); 112 + }); 113 + 114 + test("retrieves mapping by IRC nick", () => { 115 + userMappings.create(testSlackUser, testIrcNick); 116 + const mapping = userMappings.getByIrcNick(testIrcNick); 117 + 118 + expect(mapping).not.toBeNull(); 119 + expect(mapping?.slack_user_id).toBe(testSlackUser); 120 + }); 121 + 122 + test("returns null for non-existent mapping", () => { 123 + const mapping = userMappings.getBySlackUser("U999NOTFOUND"); 124 + expect(mapping).toBeNull(); 125 + }); 126 + 127 + test("deletes a user mapping", () => { 128 + userMappings.create(testSlackUser, testIrcNick); 129 + userMappings.delete(testSlackUser); 130 + 131 + const mapping = userMappings.getBySlackUser(testSlackUser); 132 + expect(mapping).toBeNull(); 133 + }); 134 + 135 + test("replaces existing mapping on create", () => { 136 + userMappings.create(testSlackUser, "oldnick"); 137 + userMappings.create(testSlackUser, testIrcNick); 138 + 139 + const mapping = userMappings.getBySlackUser(testSlackUser); 140 + expect(mapping?.irc_nick).toBe(testIrcNick); 141 + }); 142 + 143 + test("getAll returns all mappings", () => { 144 + const testUser2 = "U456TEST"; 145 + const testNick2 = "anothernick"; 146 + 147 + userMappings.create(testSlackUser, testIrcNick); 148 + userMappings.create(testUser2, testNick2); 149 + 150 + const all = userMappings.getAll(); 151 + const testMappings = all.filter( 152 + (m) => m.slack_user_id === testSlackUser || m.slack_user_id === testUser2, 153 + ); 154 + 155 + expect(testMappings.length).toBeGreaterThanOrEqual(2); 156 + 157 + // Cleanup 158 + userMappings.delete(testUser2); 159 + }); 160 + });
+56
src/lib/mentions.test.ts
··· 1 + import { describe, expect, test } from "bun:test"; 2 + import { convertIrcMentionsToSlack } from "./mentions"; 3 + import { userMappings } from "./db"; 4 + 5 + describe("convertIrcMentionsToSlack", () => { 6 + test("converts @mention when user mapping exists", () => { 7 + // Setup test data 8 + userMappings.create("U123", "testuser"); 9 + 10 + const result = convertIrcMentionsToSlack("Hey @testuser how are you?"); 11 + expect(result).toBe("Hey <@U123> how are you?"); 12 + 13 + // Cleanup 14 + userMappings.delete("U123"); 15 + }); 16 + 17 + test("leaves @mention unchanged when no mapping exists", () => { 18 + const result = convertIrcMentionsToSlack("Hey @unknownuser"); 19 + expect(result).toBe("Hey @unknownuser"); 20 + }); 21 + 22 + test("converts nick: mention when user mapping exists", () => { 23 + userMappings.create("U456", "alice"); 24 + 25 + const result = convertIrcMentionsToSlack("alice: hello"); 26 + expect(result).toBe("<@U456>: hello"); 27 + 28 + userMappings.delete("U456"); 29 + }); 30 + 31 + test("leaves nick: unchanged when no mapping exists", () => { 32 + const result = convertIrcMentionsToSlack("bob: hello"); 33 + expect(result).toBe("bob: hello"); 34 + }); 35 + 36 + test("handles multiple mentions", () => { 37 + userMappings.create("U123", "alice"); 38 + userMappings.create("U456", "bob"); 39 + 40 + const result = convertIrcMentionsToSlack("@alice and bob: hello!"); 41 + expect(result).toBe("<@U123> and <@U456>: hello!"); 42 + 43 + userMappings.delete("U123"); 44 + userMappings.delete("U456"); 45 + }); 46 + 47 + test("handles mixed mapped and unmapped mentions", () => { 48 + userMappings.create("U123", "alice"); 49 + 50 + const result = convertIrcMentionsToSlack("@alice and @unknown user"); 51 + expect(result).toContain("<@U123>"); 52 + expect(result).toContain("@unknown"); 53 + 54 + userMappings.delete("U123"); 55 + }); 56 + });
+137
src/lib/parser.test.ts
··· 1 + import { describe, expect, test } from "bun:test"; 2 + import { parseIRCFormatting, parseSlackMarkdown } from "./parser"; 3 + 4 + describe("parseSlackMarkdown", () => { 5 + test("converts channel mentions with name", () => { 6 + const result = parseSlackMarkdown("Check out <#C123ABC|general>"); 7 + expect(result).toBe("Check out #general"); 8 + }); 9 + 10 + test("converts channel mentions without name", () => { 11 + const result = parseSlackMarkdown("Check out <#C123ABC>"); 12 + expect(result).toBe("Check out #channel"); 13 + }); 14 + 15 + test("converts links with text", () => { 16 + const result = parseSlackMarkdown( 17 + "Visit <https://example.com|Example Site>", 18 + ); 19 + expect(result).toBe("Visit Example Site (https://example.com)"); 20 + }); 21 + 22 + test("converts links without text", () => { 23 + const result = parseSlackMarkdown("Visit <https://example.com>"); 24 + expect(result).toBe("Visit https://example.com"); 25 + }); 26 + 27 + test("converts mailto links", () => { 28 + const result = parseSlackMarkdown( 29 + "Email <mailto:test@example.com|Support>", 30 + ); 31 + expect(result).toBe("Email Support <test@example.com>"); 32 + }); 33 + 34 + test("converts special mentions", () => { 35 + expect(parseSlackMarkdown("<!here> everyone")).toBe("@here everyone"); 36 + expect(parseSlackMarkdown("<!channel> announcement")).toBe( 37 + "@channel announcement", 38 + ); 39 + expect(parseSlackMarkdown("<!everyone> alert")).toBe("@everyone alert"); 40 + }); 41 + 42 + test("converts user group mentions", () => { 43 + const result = parseSlackMarkdown("Hey <!subteam^GROUP123|developers>"); 44 + expect(result).toBe("Hey @developers"); 45 + }); 46 + 47 + test("converts bold formatting", () => { 48 + const result = parseSlackMarkdown("This is *bold* text"); 49 + expect(result).toBe("This is \x02bold\x02 text"); 50 + }); 51 + 52 + test("converts italic formatting", () => { 53 + const result = parseSlackMarkdown("This is _italic_ text"); 54 + expect(result).toBe("This is \x1Ditalic\x1D text"); 55 + }); 56 + 57 + test("strips strikethrough formatting", () => { 58 + const result = parseSlackMarkdown("This is ~strikethrough~ text"); 59 + expect(result).toBe("This is strikethrough text"); 60 + }); 61 + 62 + test("strips code blocks", () => { 63 + const result = parseSlackMarkdown("Code: ```const x = 1;```"); 64 + expect(result).toBe("Code: const x = 1;"); 65 + }); 66 + 67 + test("strips inline code", () => { 68 + const result = parseSlackMarkdown("Run `npm install` to start"); 69 + expect(result).toBe("Run npm install to start"); 70 + }); 71 + 72 + test("unescapes HTML entities", () => { 73 + const result = parseSlackMarkdown("a &lt; b &amp;&amp; c &gt; d"); 74 + expect(result).toBe("a < b && c > d"); 75 + }); 76 + 77 + test("handles mixed formatting", () => { 78 + const result = parseSlackMarkdown( 79 + "*Bold* and _italic_ with <https://example.com|link>", 80 + ); 81 + expect(result).toBe( 82 + "\x02Bold\x02 and \x1Ditalic\x1D with link (https://example.com)", 83 + ); 84 + }); 85 + }); 86 + 87 + describe("parseIRCFormatting", () => { 88 + test("strips IRC color codes", () => { 89 + const result = parseIRCFormatting("\x0304red text\x03 normal"); 90 + expect(result).toBe("red text normal"); 91 + }); 92 + 93 + test("converts bold formatting", () => { 94 + const result = parseIRCFormatting("This is \x02bold\x02 text"); 95 + expect(result).toBe("This is *bold* text"); 96 + }); 97 + 98 + test("converts italic formatting", () => { 99 + const result = parseIRCFormatting("This is \x1Ditalic\x1D text"); 100 + expect(result).toBe("This is _italic_ text"); 101 + }); 102 + 103 + test("converts underline to italic", () => { 104 + const result = parseIRCFormatting("This is \x1Funderline\x1F text"); 105 + expect(result).toBe("This is _underline_ text"); 106 + }); 107 + 108 + test("strips reverse/inverse formatting", () => { 109 + const result = parseIRCFormatting("Normal \x16reversed\x16 normal"); 110 + expect(result).toBe("Normal reversed normal"); 111 + }); 112 + 113 + test("strips reset formatting", () => { 114 + const result = parseIRCFormatting("Text\x0F reset"); 115 + expect(result).toBe("Text reset"); 116 + }); 117 + 118 + test("escapes special Slack characters", () => { 119 + const result = parseIRCFormatting("a < b & c > d"); 120 + expect(result).toBe("a &lt; b &amp; c &gt; d"); 121 + }); 122 + 123 + test("handles mixed formatting", () => { 124 + const result = parseIRCFormatting("\x02Bold\x02 and \x1Ditalic\x1D"); 125 + expect(result).toBe("*Bold* and _italic_"); 126 + }); 127 + 128 + test("handles nested formatting codes", () => { 129 + const result = parseIRCFormatting("\x02\x1Dbold italic\x1D\x02"); 130 + expect(result).toBe("*_bold italic_*"); 131 + }); 132 + 133 + test("handles color codes with background", () => { 134 + const result = parseIRCFormatting("\x0304,08red on yellow\x03"); 135 + expect(result).toBe("red on yellow"); 136 + }); 137 + });
+106
src/lib/threads.test.ts
··· 1 + import { afterEach, beforeEach, describe, expect, test } from "bun:test"; 2 + import { 3 + generateThreadId, 4 + getThreadByThreadId, 5 + isFirstThreadMessage, 6 + updateThreadTimestamp, 7 + } from "./threads"; 8 + import { threadTimestamps } from "./db"; 9 + 10 + describe("threads", () => { 11 + const testChannelId = "C123TEST"; 12 + const testThreadTs = "1234567890.123456"; 13 + 14 + afterEach(() => { 15 + // Clean up test data 16 + const thread = threadTimestamps.get(testThreadTs); 17 + if (thread) { 18 + threadTimestamps.cleanup(Date.now() + 1000); 19 + } 20 + }); 21 + 22 + describe("generateThreadId", () => { 23 + test("generates a 5-character thread ID", () => { 24 + const threadId = generateThreadId(testThreadTs); 25 + expect(threadId).toBeString(); 26 + expect(threadId.length).toBe(5); 27 + }); 28 + 29 + test("generates consistent IDs for same input", () => { 30 + const id1 = generateThreadId(testThreadTs); 31 + const id2 = generateThreadId(testThreadTs); 32 + expect(id1).toBe(id2); 33 + }); 34 + 35 + test("generates different IDs for different inputs", () => { 36 + const id1 = generateThreadId("1234567890.123456"); 37 + const id2 = generateThreadId("9876543210.654321"); 38 + expect(id1).not.toBe(id2); 39 + }); 40 + 41 + test("generates alphanumeric IDs", () => { 42 + const threadId = generateThreadId(testThreadTs); 43 + expect(threadId).toMatch(/^[a-z0-9]{5}$/); 44 + }); 45 + }); 46 + 47 + describe("isFirstThreadMessage", () => { 48 + test("returns true for new thread", () => { 49 + const result = isFirstThreadMessage(testThreadTs); 50 + expect(result).toBe(true); 51 + }); 52 + 53 + test("returns false for existing thread", () => { 54 + updateThreadTimestamp(testThreadTs, testChannelId); 55 + const result = isFirstThreadMessage(testThreadTs); 56 + expect(result).toBe(false); 57 + }); 58 + }); 59 + 60 + describe("updateThreadTimestamp", () => { 61 + test("creates new thread entry", () => { 62 + const threadId = updateThreadTimestamp(testThreadTs, testChannelId); 63 + 64 + expect(threadId).toBeString(); 65 + expect(threadId.length).toBe(5); 66 + 67 + const thread = threadTimestamps.get(testThreadTs); 68 + expect(thread).toBeDefined(); 69 + expect(thread?.thread_id).toBe(threadId); 70 + expect(thread?.slack_channel_id).toBe(testChannelId); 71 + }); 72 + 73 + test("updates existing thread timestamp", () => { 74 + const threadId1 = updateThreadTimestamp(testThreadTs, testChannelId); 75 + const thread1 = threadTimestamps.get(testThreadTs); 76 + const timestamp1 = thread1?.last_message_time; 77 + 78 + // Wait a bit to ensure timestamp changes 79 + Bun.sleepSync(10); 80 + 81 + const threadId2 = updateThreadTimestamp(testThreadTs, testChannelId); 82 + const thread2 = threadTimestamps.get(testThreadTs); 83 + const timestamp2 = thread2?.last_message_time; 84 + 85 + expect(threadId1).toBe(threadId2); 86 + expect(timestamp2).toBeGreaterThan(timestamp1!); 87 + }); 88 + }); 89 + 90 + describe("getThreadByThreadId", () => { 91 + test("retrieves thread by thread ID", () => { 92 + const threadId = updateThreadTimestamp(testThreadTs, testChannelId); 93 + const thread = getThreadByThreadId(threadId); 94 + 95 + expect(thread).toBeDefined(); 96 + expect(thread?.thread_ts).toBe(testThreadTs); 97 + expect(thread?.thread_id).toBe(threadId); 98 + expect(thread?.slack_channel_id).toBe(testChannelId); 99 + }); 100 + 101 + test("returns null for non-existent thread ID", () => { 102 + const thread = getThreadByThreadId("xxxxx"); 103 + expect(thread).toBeNull(); 104 + }); 105 + }); 106 + });
+146
src/lib/user-cache.test.ts
··· 1 + import { afterEach, describe, expect, mock, test } from "bun:test"; 2 + import { cleanupUserCache, getUserInfo } from "./user-cache"; 3 + 4 + describe("user-cache", () => { 5 + const mockSlackClient = { 6 + users: { 7 + info: mock(async () => ({ 8 + user: { 9 + name: "testuser", 10 + real_name: "Test User", 11 + }, 12 + })), 13 + }, 14 + }; 15 + 16 + afterEach(() => { 17 + cleanupUserCache(); 18 + mockSlackClient.users.info.mockClear(); 19 + mockSlackClient.users.info.mockReset(); 20 + }); 21 + 22 + describe("getUserInfo", () => { 23 + test("fetches user info from Slack on cache miss", async () => { 24 + const client = { 25 + users: { 26 + info: mock(async () => ({ 27 + user: { 28 + name: "testuser", 29 + real_name: "Test User", 30 + }, 31 + })), 32 + }, 33 + }; 34 + 35 + const result = await getUserInfo("U123", client); 36 + 37 + expect(result).toEqual({ 38 + name: "testuser", 39 + realName: "Test User", 40 + }); 41 + expect(client.users.info).toHaveBeenCalledTimes(1); 42 + }); 43 + 44 + test("returns cached data on cache hit", async () => { 45 + const client = { 46 + users: { 47 + info: mock(async () => ({ 48 + user: { 49 + name: "testuser", 50 + real_name: "Test User", 51 + }, 52 + })), 53 + }, 54 + }; 55 + 56 + // First call - cache miss 57 + await getUserInfo("U124", client); 58 + expect(client.users.info).toHaveBeenCalledTimes(1); 59 + 60 + // Second call - cache hit 61 + const result = await getUserInfo("U124", client); 62 + expect(result).toEqual({ 63 + name: "testuser", 64 + realName: "Test User", 65 + }); 66 + expect(client.users.info).toHaveBeenCalledTimes(1); // Still 1 67 + }); 68 + 69 + test("uses name as fallback for real_name", async () => { 70 + const client = { 71 + users: { 72 + info: mock(async () => ({ 73 + user: { 74 + name: "testuser", 75 + }, 76 + })), 77 + }, 78 + }; 79 + 80 + const result = await getUserInfo("U456", client); 81 + expect(result).toEqual({ 82 + name: "testuser", 83 + realName: "testuser", 84 + }); 85 + }); 86 + 87 + test("handles missing user data gracefully", async () => { 88 + const client = { 89 + users: { 90 + info: mock(async () => ({})), 91 + }, 92 + }; 93 + 94 + const result = await getUserInfo("U789", client); 95 + expect(result).toEqual({ 96 + name: "Unknown", 97 + realName: "Unknown", 98 + }); 99 + }); 100 + 101 + test("handles Slack API errors", async () => { 102 + const client = { 103 + users: { 104 + info: mock(async () => { 105 + throw new Error("API Error"); 106 + }), 107 + }, 108 + }; 109 + 110 + const result = await getUserInfo("U999", client); 111 + expect(result).toBeNull(); 112 + }); 113 + 114 + test("caches different users separately", async () => { 115 + const client = { 116 + users: { 117 + info: mock(async ({ user }: { user: string }) => { 118 + if (user === "U111") { 119 + return { user: { name: "alice", real_name: "Alice" } }; 120 + } 121 + return { user: { name: "bob", real_name: "Bob" } }; 122 + }), 123 + }, 124 + }; 125 + 126 + const result1 = await getUserInfo("U111", client); 127 + const result2 = await getUserInfo("U222", client); 128 + 129 + expect(result1?.name).toBe("alice"); 130 + expect(result2?.name).toBe("bob"); 131 + expect(client.users.info).toHaveBeenCalledTimes(2); 132 + 133 + // Both should be cached now 134 + await getUserInfo("U111", client); 135 + await getUserInfo("U222", client); 136 + expect(client.users.info).toHaveBeenCalledTimes(2); // Still 2 137 + }); 138 + }); 139 + 140 + describe("cleanupUserCache", () => { 141 + test("cleanup runs without errors", () => { 142 + // Just test that cleanup doesn't throw 143 + expect(() => cleanupUserCache()).not.toThrow(); 144 + }); 145 + }); 146 + });