diff --git a/internal/client/client.go b/internal/client/client.go index e23961c..c9534dd 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -223,14 +223,22 @@ type User struct { } `json:"profile"` } +// Reaction represents an emoji reaction on a Slack message +type Reaction struct { + Name string `json:"name"` + Count int `json:"count"` + Users []string `json:"users"` +} + // Message represents a Slack message type Message struct { - Type string `json:"type"` - User string `json:"user"` - Text string `json:"text"` - TS string `json:"ts"` - ThreadTS string `json:"thread_ts,omitempty"` - ReplyCount int `json:"reply_count,omitempty"` + Type string `json:"type"` + User string `json:"user"` + Text string `json:"text"` + TS string `json:"ts"` + ThreadTS string `json:"thread_ts,omitempty"` + ReplyCount int `json:"reply_count,omitempty"` + Reactions []Reaction `json:"reactions,omitempty"` } // Team represents workspace info diff --git a/internal/client/client_test.go b/internal/client/client_test.go index 76eb910..e416d4f 100644 --- a/internal/client/client_test.go +++ b/internal/client/client_test.go @@ -483,6 +483,107 @@ func TestClient_GetThreadReplies_Success(t *testing.T) { } } +func TestClient_GetThreadReplies_WithReactions(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + resp := map[string]interface{}{ + "ok": true, + "messages": []map[string]interface{}{ + { + "ts": "1234567890.123456", + "text": "Original", + "user": "U123", + "reactions": []map[string]interface{}{ + {"name": "thumbsup", "count": 2, "users": []string{"U123", "U456"}}, + {"name": "heart", "count": 1, "users": []string{"U789"}}, + }, + }, + { + "ts": "1234567890.123457", + "text": "Reply without reactions", + "user": "U456", + }, + }, + "response_metadata": map[string]string{"next_cursor": ""}, + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + client := NewWithConfig(server.URL, "test-token", nil) + messages, err := client.GetThreadReplies("C123", "1234567890.123456", 100) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(messages) != 2 { + t.Fatalf("expected 2 messages, got %d", len(messages)) + } + + // First message should have reactions + if len(messages[0].Reactions) != 2 { + t.Fatalf("expected 2 reactions on first message, got %d", len(messages[0].Reactions)) + } + if messages[0].Reactions[0].Name != "thumbsup" { + t.Errorf("expected reaction name 'thumbsup', got %s", messages[0].Reactions[0].Name) + } + if messages[0].Reactions[0].Count != 2 { + t.Errorf("expected reaction count 2, got %d", messages[0].Reactions[0].Count) + } + if len(messages[0].Reactions[0].Users) != 2 { + t.Errorf("expected 2 users on thumbsup reaction, got %d", len(messages[0].Reactions[0].Users)) + } + if messages[0].Reactions[1].Name != "heart" { + t.Errorf("expected reaction name 'heart', got %s", messages[0].Reactions[1].Name) + } + + // Second message should have no reactions + if len(messages[1].Reactions) != 0 { + t.Errorf("expected 0 reactions on second message, got %d", len(messages[1].Reactions)) + } +} + +func TestClient_GetChannelHistory_WithReactions(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + resp := map[string]interface{}{ + "ok": true, + "messages": []map[string]interface{}{ + { + "ts": "1234567890.123456", + "text": "Hello", + "user": "U123", + "reactions": []map[string]interface{}{ + {"name": "wave", "count": 3, "users": []string{"U1", "U2", "U3"}}, + }, + }, + }, + "response_metadata": map[string]string{"next_cursor": ""}, + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + client := NewWithConfig(server.URL, "test-token", nil) + messages, err := client.GetChannelHistory("C123", 20, "", "") + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(messages) != 1 { + t.Fatalf("expected 1 message, got %d", len(messages)) + } + if len(messages[0].Reactions) != 1 { + t.Fatalf("expected 1 reaction, got %d", len(messages[0].Reactions)) + } + if messages[0].Reactions[0].Name != "wave" { + t.Errorf("expected reaction name 'wave', got %s", messages[0].Reactions[0].Name) + } + if messages[0].Reactions[0].Count != 3 { + t.Errorf("expected reaction count 3, got %d", messages[0].Reactions[0].Count) + } +} + func TestClient_AddReaction_Success(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.Contains(r.URL.Path, "reactions.add") { diff --git a/internal/cmd/messages/messages_test.go b/internal/cmd/messages/messages_test.go index d6c51b3..68e4f1d 100644 --- a/internal/cmd/messages/messages_test.go +++ b/internal/cmd/messages/messages_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" "github.com/open-cli-collective/slack-chat-api/internal/client" + "github.com/open-cli-collective/slack-chat-api/internal/output" ) func TestFormatTimestamp(t *testing.T) { @@ -483,6 +484,115 @@ func TestRunThread_Success(t *testing.T) { require.NoError(t, err) } +func TestRunThread_JSONIncludesReactions(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/conversations.replies": + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "ok": true, + "messages": []map[string]interface{}{ + { + "ts": "1234567890.123456", + "user": "U001", + "text": "Original", + "reactions": []map[string]interface{}{ + {"name": "thumbsup", "count": 2, "users": []string{"U001", "U002"}}, + }, + }, + { + "ts": "1234567890.123457", + "user": "U002", + "text": "Reply without reactions", + }, + }, + }) + case "/users.info": + mockUserInfoHandler(w, r) + } + })) + defer server.Close() + + c := client.NewWithConfig(server.URL, "test-token", nil) + opts := &threadOptions{limit: 100} + + // Capture JSON output + output.OutputFormat = output.FormatJSON + defer func() { output.OutputFormat = output.FormatText }() + + var buf strings.Builder + output.Writer = &buf + defer func() { output.Writer = os.Stdout }() + + err := runThread("C123", "1234567890.123456", opts, c) + require.NoError(t, err) + + // Parse the JSON output + var messages []client.Message + err = json.Unmarshal([]byte(buf.String()), &messages) + require.NoError(t, err) + + require.Len(t, messages, 2) + + // First message should have reactions + require.Len(t, messages[0].Reactions, 1) + assert.Equal(t, "thumbsup", messages[0].Reactions[0].Name) + assert.Equal(t, 2, messages[0].Reactions[0].Count) + assert.Equal(t, []string{"U001", "U002"}, messages[0].Reactions[0].Users) + + // Second message should have no reactions + assert.Empty(t, messages[1].Reactions) +} + +func TestRunHistory_JSONIncludesReactions(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/conversations.history": + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "ok": true, + "messages": []map[string]interface{}{ + { + "ts": "1234567890.123456", + "user": "U001", + "text": "Hello", + "reactions": []map[string]interface{}{ + {"name": "wave", "count": 1, "users": []string{"U002"}}, + {"name": "heart", "count": 3, "users": []string{"U001", "U002", "U003"}}, + }, + }, + }, + }) + case "/users.info": + mockUserInfoHandler(w, r) + } + })) + defer server.Close() + + c := client.NewWithConfig(server.URL, "test-token", nil) + opts := &historyOptions{limit: 20} + + // Capture JSON output + output.OutputFormat = output.FormatJSON + defer func() { output.OutputFormat = output.FormatText }() + + var buf strings.Builder + output.Writer = &buf + defer func() { output.Writer = os.Stdout }() + + err := runHistory("C123", opts, c) + require.NoError(t, err) + + // Parse the JSON output + var messages []client.Message + err = json.Unmarshal([]byte(buf.String()), &messages) + require.NoError(t, err) + + require.Len(t, messages, 1) + require.Len(t, messages[0].Reactions, 2) + assert.Equal(t, "wave", messages[0].Reactions[0].Name) + assert.Equal(t, "heart", messages[0].Reactions[1].Name) + assert.Equal(t, 3, messages[0].Reactions[1].Count) +} + func TestRunReact_Success(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/reactions.add", r.URL.Path)