From 1e66b7d6980ae5ba83697fc22eb4ada524d44a03 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 24 Apr 2026 19:23:20 +0100 Subject: [PATCH] test: increase DM and wiretap coverage --- CHANGELOG.md | 1 + internal/cli/cli_test.go | 11 ++ internal/discorddesktop/import_test.go | 37 +++++ internal/store/direct_messages_test.go | 222 +++++++++++++++++++++++++ 4 files changed, 271 insertions(+) create mode 100644 internal/store/direct_messages_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 43c68ea..ff84a7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to `discrawl` will be documented in this file. ### Tests - Added regression coverage for DM channel-name inference from cached profile data when Discord Desktop cache lacks explicit channel recipient metadata. +- Added coverage for local DM conversation listing/filtering, DM cleanup paths, and Discord Desktop import helper edge cases. ## 0.5.1 - 2026-04-24 diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index b8b80c8..0854153 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -1680,6 +1680,17 @@ func TestRunMentionsValidation(t *testing.T) { rt := &runtime{stderr: &bytes.Buffer{}} rt.now = func() time.Time { return time.Date(2026, 3, 8, 12, 0, 0, 0, time.UTC) } + require.Equal(t, 2, ExitCode(rt.runDirectMessages([]string{"extra"}))) + require.Equal(t, 2, ExitCode(rt.runDirectMessages([]string{"--hours", "-1"}))) + require.Equal(t, 2, ExitCode(rt.runDirectMessages([]string{"--days", "-1"}))) + require.Equal(t, 2, ExitCode(rt.runDirectMessages([]string{"--hours", "1", "--days", "1"}))) + require.Equal(t, 2, ExitCode(rt.runDirectMessages([]string{"--hours", "1", "--since", "2026-03-01T00:00:00Z"}))) + require.Equal(t, 2, ExitCode(rt.runDirectMessages([]string{"--limit", "-1"}))) + require.Equal(t, 2, ExitCode(rt.runDirectMessages([]string{"--last", "-1"}))) + require.Equal(t, 2, ExitCode(rt.runDirectMessages([]string{"--all", "--last", "1"}))) + require.Equal(t, 2, ExitCode(rt.runDirectMessages([]string{"--limit", "1", "--last", "1"}))) + require.Equal(t, 2, ExitCode(rt.runDirectMessages([]string{"--since", "bad"}))) + require.Equal(t, 2, ExitCode(rt.runDirectMessages([]string{"--before", "bad"}))) require.Equal(t, 2, ExitCode(rt.runMessages([]string{"--hours", "-1", "--channel", "general"}))) require.Equal(t, 2, ExitCode(rt.runMessages([]string{"--hours", "1", "--days", "1", "--channel", "general"}))) require.Equal(t, 2, ExitCode(rt.runMessages([]string{"--hours", "1", "--since", "2026-03-01T00:00:00Z", "--channel", "general"}))) diff --git a/internal/discorddesktop/import_test.go b/internal/discorddesktop/import_test.go index 739a23b..17b5943 100644 --- a/internal/discorddesktop/import_test.go +++ b/internal/discorddesktop/import_test.go @@ -6,6 +6,7 @@ import ( "context" "os" "path/filepath" + "runtime" "testing" "time" @@ -14,6 +15,42 @@ import ( "github.com/steipete/discrawl/internal/store" ) +func TestDesktopPathAndImportHelpers(t *testing.T) { + home := t.TempDir() + switch runtime.GOOS { + case "windows": + t.Setenv("USERPROFILE", home) + require.Equal(t, filepath.Join(home, "AppData", "Roaming", "discord"), DefaultPath()) + case "darwin": + t.Setenv("HOME", home) + require.Equal(t, filepath.Join(home, "Library", "Application Support", "discord"), DefaultPath()) + default: + xdg := filepath.Join(home, "xdg") + t.Setenv("XDG_CONFIG_HOME", xdg) + require.Equal(t, filepath.Join(xdg, "discord"), DefaultPath()) + } + + require.Equal(t, "dm", kindForChannelType(1, true)) + require.Equal(t, "group_dm", kindForChannelType(3, true)) + require.Equal(t, "text", kindForChannelType(0, false)) + require.Equal(t, "announcement", kindForChannelType(5, false)) + require.Equal(t, "thread_announcement", kindForChannelType(10, false)) + require.Equal(t, "thread_public", kindForChannelType(11, false)) + require.Equal(t, "thread_private", kindForChannelType(12, false)) + require.Equal(t, "forum", kindForChannelType(15, false)) + require.Equal(t, "desktop", kindForChannelType(99, false)) + embedParts := embedText(map[string]any{"embeds": []any{ + map[string]any{"title": " title ", "description": "body"}, + }}) + require.Equal(t, []string{"title", "body"}, embedParts) + require.Equal(t, time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC), snowflakeTime("0")) + require.True(t, snowflakeTime("not-a-snowflake").IsZero()) + require.Equal(t, time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC), parseDiscordTime("2026-04-24T12:00:00Z")) + require.True(t, parseDiscordTime("bad").IsZero()) + require.Empty(t, formatOptionalTime(time.Time{})) + require.NotEmpty(t, formatOptionalTime(time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC))) +} + func TestImportExtractsDirectMessageFromDesktopCache(t *testing.T) { ctx := context.Background() dir := t.TempDir() diff --git a/internal/store/direct_messages_test.go b/internal/store/direct_messages_test.go new file mode 100644 index 0000000..565d36b --- /dev/null +++ b/internal/store/direct_messages_test.go @@ -0,0 +1,222 @@ +package store + +import ( + "context" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestDirectMessageConversations(t *testing.T) { + t.Parallel() + + ctx := context.Background() + s, err := Open(ctx, filepath.Join(t.TempDir(), "discrawl.db")) + require.NoError(t, err) + defer func() { _ = s.Close() }() + + base := time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC) + require.NoError(t, s.UpsertChannel(ctx, ChannelRecord{ID: "dm1", GuildID: DirectMessageGuildID, Kind: "dm", Name: "Vincent K", RawJSON: `{}`})) + require.NoError(t, s.UpsertChannel(ctx, ChannelRecord{ID: "dm2", GuildID: DirectMessageGuildID, Kind: "dm", Name: "Alice", RawJSON: `{}`})) + require.NoError(t, s.UpsertChannel(ctx, ChannelRecord{ID: "dm-empty", GuildID: DirectMessageGuildID, Kind: "dm", Name: "Empty", RawJSON: `{}`})) + require.NoError(t, s.UpsertChannel(ctx, ChannelRecord{ID: "c1", GuildID: "g1", Kind: "text", Name: "general", RawJSON: `{}`})) + for _, message := range []MessageRecord{ + { + ID: "m1", + GuildID: DirectMessageGuildID, + ChannelID: "dm1", + ChannelName: "Vincent K", + AuthorID: "u1", + AuthorName: "Vincent K", + CreatedAt: base.Format(time.RFC3339Nano), + Content: "hello", + NormalizedContent: "hello", + RawJSON: `{"author":{"username":"vincentkoc","global_name":"Vincent K"}}`, + }, + { + ID: "m2", + GuildID: DirectMessageGuildID, + ChannelID: "dm1", + ChannelName: "Vincent K", + AuthorID: "self", + AuthorName: "Peter", + CreatedAt: base.Add(time.Minute).Format(time.RFC3339Nano), + Content: "reply", + NormalizedContent: "reply", + RawJSON: `{"author":{"username":"steipete","global_name":"Peter"}}`, + }, + { + ID: "m3", + GuildID: DirectMessageGuildID, + ChannelID: "dm2", + ChannelName: "Alice", + AuthorID: "u2", + AuthorName: "Alice", + CreatedAt: base.Add(2 * time.Minute).Format(time.RFC3339Nano), + Content: "newer", + NormalizedContent: "newer", + RawJSON: `{"author":{"username":"alice","global_name":"Alice Example"}}`, + }, + { + ID: "m4", + GuildID: "g1", + ChannelID: "c1", + ChannelName: "general", + AuthorID: "u2", + AuthorName: "Alice", + CreatedAt: base.Add(3 * time.Minute).Format(time.RFC3339Nano), + Content: "guild message", + NormalizedContent: "guild message", + RawJSON: `{"author":{"username":"alice"}}`, + }, + } { + require.NoError(t, s.UpsertMessage(ctx, message)) + } + + rows, err := s.DirectMessageConversations(ctx, DirectMessageConversationOptions{}) + require.NoError(t, err) + require.Len(t, rows, 3) + require.Equal(t, "dm2", rows[0].ChannelID) + require.Equal(t, "Alice", rows[0].Name) + require.Equal(t, 1, rows[0].MessageCount) + require.Equal(t, 1, rows[0].AuthorCount) + require.Equal(t, base.Add(2*time.Minute), rows[0].FirstMessageAt) + require.Equal(t, base.Add(2*time.Minute), rows[0].LastMessageAt) + require.Equal(t, "dm1", rows[1].ChannelID) + require.Equal(t, 2, rows[1].MessageCount) + require.Equal(t, 2, rows[1].AuthorCount) + require.Equal(t, "dm-empty", rows[2].ChannelID) + require.Zero(t, rows[2].MessageCount) + require.True(t, rows[2].FirstMessageAt.IsZero()) + require.True(t, rows[2].LastMessageAt.IsZero()) + + rows, err = s.DirectMessageConversations(ctx, DirectMessageConversationOptions{With: "vincent"}) + require.NoError(t, err) + require.Len(t, rows, 1) + require.Equal(t, "dm1", rows[0].ChannelID) + + rows, err = s.DirectMessageConversations(ctx, DirectMessageConversationOptions{With: "Alice Example"}) + require.NoError(t, err) + require.Len(t, rows, 1) + require.Equal(t, "dm2", rows[0].ChannelID) + + rows, err = s.DirectMessageConversations(ctx, DirectMessageConversationOptions{With: "u1"}) + require.NoError(t, err) + require.Len(t, rows, 1) + require.Equal(t, "dm1", rows[0].ChannelID) + + rows, err = s.DirectMessageConversations(ctx, DirectMessageConversationOptions{Limit: 1}) + require.NoError(t, err) + require.Len(t, rows, 1) + require.Equal(t, "dm2", rows[0].ChannelID) +} + +func TestDeleteGuildDataAndOrphanChannels(t *testing.T) { + t.Parallel() + + ctx := context.Background() + s, err := Open(ctx, filepath.Join(t.TempDir(), "discrawl.db")) + require.NoError(t, err) + defer func() { _ = s.Close() }() + + now := time.Date(2026, 4, 24, 13, 0, 0, 0, time.UTC).Format(time.RFC3339Nano) + require.NoError(t, s.UpsertGuild(ctx, GuildRecord{ID: "g1", Name: "Delete Me", RawJSON: `{}`})) + require.NoError(t, s.UpsertGuild(ctx, GuildRecord{ID: "g2", Name: "Keep Me", RawJSON: `{}`})) + require.NoError(t, s.UpsertChannel(ctx, ChannelRecord{ID: "c1", GuildID: "g1", Kind: "text", Name: "general", RawJSON: `{}`})) + require.NoError(t, s.UpsertChannel(ctx, ChannelRecord{ID: "c2", GuildID: "g2", Kind: "text", Name: "general", RawJSON: `{}`})) + require.NoError(t, s.UpsertMember(ctx, MemberRecord{GuildID: "g1", UserID: "u1", Username: "gone", RoleIDsJSON: `[]`, RawJSON: `{}`})) + require.NoError(t, s.UpsertMember(ctx, MemberRecord{GuildID: "g2", UserID: "u2", Username: "kept", RoleIDsJSON: `[]`, RawJSON: `{}`})) + require.NoError(t, s.UpsertMessages(ctx, []MessageMutation{ + { + Record: MessageRecord{ + ID: "m1", + GuildID: "g1", + ChannelID: "c1", + ChannelName: "general", + AuthorID: "u1", + AuthorName: "Gone", + CreatedAt: now, + Content: "remove me", + NormalizedContent: "remove me", + HasAttachments: true, + RawJSON: `{"author":{"username":"gone"}}`, + }, + EventType: "wiretap", + PayloadJSON: `{}`, + Options: WriteOptions{AppendEvent: true, EnqueueEmbedding: true}, + Attachments: []AttachmentRecord{{ + AttachmentID: "a1", + MessageID: "m1", + GuildID: "g1", + ChannelID: "c1", + AuthorID: "u1", + Filename: "gone.txt", + }}, + Mentions: []MentionEventRecord{{ + MessageID: "m1", + GuildID: "g1", + ChannelID: "c1", + AuthorID: "u1", + TargetType: "user", + TargetID: "u2", + TargetName: "Kept", + EventAt: now, + }}, + }, + { + Record: MessageRecord{ + ID: "m2", + GuildID: "g2", + ChannelID: "c2", + ChannelName: "general", + AuthorID: "u2", + AuthorName: "Kept", + CreatedAt: now, + Content: "keep me", + NormalizedContent: "keep me", + RawJSON: `{"author":{"username":"kept"}}`, + }, + }, + })) + + require.NoError(t, s.DeleteGuildData(ctx, "g1")) + for _, query := range []string{ + "select count(*) from guilds where id = 'g1'", + "select count(*) from channels where guild_id = 'g1'", + "select count(*) from members where guild_id = 'g1'", + "select count(*) from messages where guild_id = 'g1'", + "select count(*) from message_fts where guild_id = 'g1'", + "select count(*) from message_events where guild_id = 'g1'", + "select count(*) from message_attachments where guild_id = 'g1'", + "select count(*) from mention_events where guild_id = 'g1'", + "select count(*) from embedding_jobs where message_id = 'm1'", + } { + _, rows, err := s.ReadOnlyQuery(ctx, query) + require.NoError(t, err, query) + require.Equal(t, "0", rows[0][0], query) + } + _, rows, err := s.ReadOnlyQuery(ctx, "select count(*) from messages where guild_id = 'g2'") + require.NoError(t, err) + require.Equal(t, "1", rows[0][0]) + + require.NoError(t, s.UpsertChannel(ctx, ChannelRecord{ID: "dm-keep", GuildID: DirectMessageGuildID, Kind: "dm", Name: "Keep", RawJSON: `{}`})) + require.NoError(t, s.UpsertChannel(ctx, ChannelRecord{ID: "dm-orphan", GuildID: DirectMessageGuildID, Kind: "dm", Name: "Remove", RawJSON: `{}`})) + require.NoError(t, s.UpsertMessage(ctx, MessageRecord{ + ID: "dm-m1", + GuildID: DirectMessageGuildID, + ChannelID: "dm-keep", + ChannelName: "Keep", + AuthorID: "u3", + AuthorName: "Keep", + CreatedAt: now, + Content: "keep dm", + NormalizedContent: "keep dm", + RawJSON: `{}`, + })) + require.NoError(t, s.DeleteOrphanChannels(ctx, DirectMessageGuildID)) + _, rows, err = s.ReadOnlyQuery(ctx, "select id from channels where guild_id = '@me' order by id") + require.NoError(t, err) + require.Equal(t, [][]string{{"dm-keep"}}, rows) +}