Co-authored-by: twilsher <273817+twilsher@users.noreply.github.com> Co-authored-by: Alex <1514933+alexuser@users.noreply.github.com> Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: pavelzak <2132599+pavelzak@users.noreply.github.com>
491 lines
15 KiB
Go
491 lines
15 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"google.golang.org/api/chat/v1"
|
|
"google.golang.org/api/option"
|
|
)
|
|
|
|
var errUnexpectedChatServiceCall = errors.New("unexpected chat service call")
|
|
|
|
func useFakeChatService(t *testing.T, handler http.HandlerFunc) {
|
|
t.Helper()
|
|
|
|
origNew := newChatService
|
|
t.Cleanup(func() { newChatService = origNew })
|
|
|
|
srv := httptest.NewServer(handler)
|
|
t.Cleanup(srv.Close)
|
|
|
|
svc, err := chat.NewService(context.Background(),
|
|
option.WithoutAuthentication(),
|
|
option.WithHTTPClient(srv.Client()),
|
|
option.WithEndpoint(srv.URL+"/"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewService: %v", err)
|
|
}
|
|
newChatService = func(context.Context, string) (*chat.Service, error) { return svc, nil }
|
|
}
|
|
|
|
func TestChatSpaceDisplayNameMatches(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
displayName string
|
|
query string
|
|
exact bool
|
|
want bool
|
|
}{
|
|
{name: "substring case insensitive", displayName: "My Project Team", query: "project", want: true},
|
|
{name: "substring miss", displayName: "Random Channel", query: "project", want: false},
|
|
{name: "exact case insensitive", displayName: "Project Alpha", query: "project alpha", exact: true, want: true},
|
|
{name: "exact does not substring", displayName: "Project Alpha", query: "project", exact: true, want: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := chatSpaceDisplayNameMatches(tt.displayName, tt.query, tt.exact)
|
|
if got != tt.want {
|
|
t.Fatalf("match = %t, want %t", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecute_ChatSpacesList_Text(t *testing.T) {
|
|
useFakeChatService(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if !(r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/spaces")) {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"spaces": []map[string]any{
|
|
{"name": "spaces/aaa", "displayName": "Engineering", "spaceType": "SPACE"},
|
|
{"name": "spaces/bbb", "displayName": "", "spaceType": "DIRECT_MESSAGE"},
|
|
},
|
|
"nextPageToken": "npt",
|
|
})
|
|
})
|
|
|
|
out := captureStdout(t, func() {
|
|
errOut := captureStderr(t, func() {
|
|
if err := Execute([]string{"--account", "a@b.com", "chat", "spaces", "list", "--max", "2"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
if !strings.Contains(errOut, "# Next page: --page npt") {
|
|
t.Fatalf("unexpected stderr=%q", errOut)
|
|
}
|
|
})
|
|
if !strings.Contains(out, "RESOURCE") || !strings.Contains(out, "spaces/aaa") || !strings.Contains(out, "Engineering") {
|
|
t.Fatalf("unexpected out=%q", out)
|
|
}
|
|
}
|
|
|
|
func TestExecute_ChatSpacesList_ConsumerBlocked(t *testing.T) {
|
|
origNew := newChatService
|
|
t.Cleanup(func() { newChatService = origNew })
|
|
newChatService = func(context.Context, string) (*chat.Service, error) {
|
|
t.Fatalf("unexpected chat service call")
|
|
return nil, errUnexpectedChatServiceCall
|
|
}
|
|
|
|
err := Execute([]string{"--account", "user@gmail.com", "chat", "spaces", "list"})
|
|
if err == nil {
|
|
t.Fatalf("expected error")
|
|
}
|
|
if !strings.Contains(err.Error(), "Workspace") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestExecute_ChatSpacesFind_JSON(t *testing.T) {
|
|
useFakeChatService(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if !(r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/spaces")) {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
token := r.URL.Query().Get("pageToken")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if token == "" {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"spaces": []map[string]any{
|
|
{"name": "spaces/aaa", "displayName": "Engineering", "spaceType": "SPACE"},
|
|
},
|
|
"nextPageToken": "next",
|
|
})
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"spaces": []map[string]any{
|
|
{"name": "spaces/bbb", "displayName": "Other", "spaceType": "SPACE"},
|
|
},
|
|
})
|
|
})
|
|
|
|
out := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "--account", "a@b.com", "chat", "spaces", "find", "Engineering"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
var parsed struct {
|
|
Spaces []struct {
|
|
Resource string `json:"resource"`
|
|
Name string `json:"name"`
|
|
} `json:"spaces"`
|
|
}
|
|
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if len(parsed.Spaces) != 1 || parsed.Spaces[0].Resource != "spaces/aaa" {
|
|
t.Fatalf("unexpected spaces: %#v", parsed.Spaces)
|
|
}
|
|
}
|
|
|
|
func TestExecute_ChatSpacesFind_Substring(t *testing.T) {
|
|
useFakeChatService(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if !(r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/spaces")) {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"spaces": []map[string]any{
|
|
{"name": "spaces/aaa", "displayName": "My Project Team", "spaceType": "SPACE"},
|
|
{"name": "spaces/bbb", "displayName": "Project Alpha", "spaceType": "SPACE"},
|
|
{"name": "spaces/ccc", "displayName": "Random Channel", "spaceType": "SPACE"},
|
|
{"name": "spaces/ddd", "displayName": "Old Project Archive", "spaceType": "SPACE"},
|
|
},
|
|
})
|
|
})
|
|
|
|
// Default behavior: substring, case-insensitive. "project" must match all
|
|
// three entries whose DisplayName contains "Project", and must exclude the
|
|
// unrelated "Random Channel".
|
|
out := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "--account", "a@b.com", "chat", "spaces", "find", "project"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
var parsed struct {
|
|
Spaces []struct {
|
|
Resource string `json:"resource"`
|
|
Name string `json:"name"`
|
|
} `json:"spaces"`
|
|
}
|
|
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
got := make(map[string]bool, len(parsed.Spaces))
|
|
for _, s := range parsed.Spaces {
|
|
got[s.Resource] = true
|
|
}
|
|
if len(got) != 3 || !got["spaces/aaa"] || !got["spaces/bbb"] || !got["spaces/ddd"] {
|
|
t.Fatalf("substring search must match all three 'Project' spaces, got %#v", parsed.Spaces)
|
|
}
|
|
if got["spaces/ccc"] {
|
|
t.Fatalf("substring search must not match 'Random Channel', got %#v", parsed.Spaces)
|
|
}
|
|
}
|
|
|
|
func TestExecute_ChatSpacesFind_Exact(t *testing.T) {
|
|
useFakeChatService(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if !(r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/spaces")) {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"spaces": []map[string]any{
|
|
{"name": "spaces/aaa", "displayName": "My Project Team", "spaceType": "SPACE"},
|
|
{"name": "spaces/bbb", "displayName": "Project Alpha", "spaceType": "SPACE"},
|
|
},
|
|
})
|
|
})
|
|
|
|
// --exact must restore the legacy case-insensitive equality behavior: only
|
|
// the space whose DisplayName equals "project alpha" (ignoring case)
|
|
// is returned.
|
|
out := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "--account", "a@b.com", "chat", "spaces", "find", "--exact", "project alpha"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
var parsed struct {
|
|
Spaces []struct {
|
|
Resource string `json:"resource"`
|
|
} `json:"spaces"`
|
|
}
|
|
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if len(parsed.Spaces) != 1 || parsed.Spaces[0].Resource != "spaces/bbb" {
|
|
t.Fatalf("--exact must return only 'Project Alpha', got %#v", parsed.Spaces)
|
|
}
|
|
}
|
|
|
|
func TestExecute_ChatSpacesCreate_JSON(t *testing.T) {
|
|
var mu sync.Mutex
|
|
var gotType string
|
|
var gotMembers int
|
|
|
|
useFakeChatService(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if !(r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/spaces:setup")) {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
var body map[string]any
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
space := body["space"].(map[string]any)
|
|
members := body["memberships"].([]any)
|
|
mu.Lock()
|
|
gotType, _ = space["spaceType"].(string)
|
|
gotMembers = len(members)
|
|
mu.Unlock()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"name": "spaces/new",
|
|
"displayName": "Engineering",
|
|
"spaceType": "SPACE",
|
|
})
|
|
})
|
|
|
|
out := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "--account", "a@b.com", "chat", "spaces", "create", "Engineering", "--member", "a@b.com", "--member", "b@b.com"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
var parsed struct {
|
|
Space struct {
|
|
Name string `json:"name"`
|
|
} `json:"space"`
|
|
}
|
|
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if parsed.Space.Name != "spaces/new" {
|
|
t.Fatalf("unexpected space: %#v", parsed.Space)
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if gotType != "SPACE" || gotMembers != 2 {
|
|
t.Fatalf("unexpected setup: type=%q members=%d", gotType, gotMembers)
|
|
}
|
|
}
|
|
|
|
func TestExecute_ChatMessagesList_Text_Unread(t *testing.T) {
|
|
var gotFilter string
|
|
|
|
useFakeChatService(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/spaceReadState") && r.Method == http.MethodGet:
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"lastReadTime": "2025-01-01T00:00:00Z"})
|
|
case strings.Contains(r.URL.Path, "/messages") && r.Method == http.MethodGet:
|
|
gotFilter = r.URL.Query().Get("filter")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"messages": []map[string]any{{
|
|
"name": "spaces/aaa/messages/msg1",
|
|
"text": "hello",
|
|
"createTime": "2025-01-02T00:00:00Z",
|
|
"sender": map[string]any{
|
|
"displayName": "Ada",
|
|
},
|
|
"thread": map[string]any{
|
|
"name": "spaces/aaa/threads/t1",
|
|
},
|
|
}},
|
|
"nextPageToken": "npt",
|
|
})
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
|
|
out := captureStdout(t, func() {
|
|
errOut := captureStderr(t, func() {
|
|
if err := Execute([]string{"--account", "a@b.com", "chat", "messages", "list", "spaces/aaa", "--unread", "--thread", "t1"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
if !strings.Contains(errOut, "# Next page: --page npt") {
|
|
t.Fatalf("unexpected stderr=%q", errOut)
|
|
}
|
|
})
|
|
if !strings.Contains(out, "RESOURCE") || !strings.Contains(out, "messages/msg1") || !strings.Contains(out, "hello") {
|
|
t.Fatalf("unexpected out=%q", out)
|
|
}
|
|
if !strings.Contains(gotFilter, "createTime > \"2025-01-01T00:00:00Z\"") {
|
|
t.Fatalf("unexpected filter: %q", gotFilter)
|
|
}
|
|
if !strings.Contains(gotFilter, "thread.name = \"spaces/aaa/threads/t1\"") {
|
|
t.Fatalf("unexpected thread filter: %q", gotFilter)
|
|
}
|
|
}
|
|
|
|
func TestExecute_ChatMessagesSend_JSON(t *testing.T) {
|
|
var gotText string
|
|
var gotThread string
|
|
|
|
useFakeChatService(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if !(r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/messages")) {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
var body map[string]any
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
gotText, _ = body["text"].(string)
|
|
if thread, ok := body["thread"].(map[string]any); ok {
|
|
gotThread, _ = thread["name"].(string)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"name": "spaces/aaa/messages/msg2",
|
|
})
|
|
})
|
|
|
|
out := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "--account", "a@b.com", "chat", "messages", "send", "spaces/aaa", "--text", "hello", "--thread", "t1"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
if gotText != "hello" {
|
|
t.Fatalf("unexpected text: %q", gotText)
|
|
}
|
|
if gotThread != "spaces/aaa/threads/t1" {
|
|
t.Fatalf("unexpected thread: %q", gotThread)
|
|
}
|
|
if !strings.Contains(out, "spaces/aaa/messages/msg2") {
|
|
t.Fatalf("unexpected out=%q", out)
|
|
}
|
|
}
|
|
|
|
func TestExecute_ChatThreadsList_Text(t *testing.T) {
|
|
useFakeChatService(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if !(r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/messages")) {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"messages": []map[string]any{
|
|
{"name": "spaces/aaa/messages/m1", "thread": map[string]any{"name": "spaces/aaa/threads/t1"}, "text": "t1"},
|
|
{"name": "spaces/aaa/messages/m2", "thread": map[string]any{"name": "spaces/aaa/threads/t1"}, "text": "t1 again"},
|
|
{"name": "spaces/aaa/messages/m3", "thread": map[string]any{"name": "spaces/aaa/threads/t2"}, "text": "t2"},
|
|
},
|
|
})
|
|
})
|
|
|
|
out := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--account", "a@b.com", "chat", "threads", "list", "spaces/aaa"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
if strings.Count(out, "threads/t1") != 1 || !strings.Contains(out, "threads/t2") {
|
|
t.Fatalf("unexpected out=%q", out)
|
|
}
|
|
}
|
|
|
|
func TestExecute_ChatDMSpace_JSON(t *testing.T) {
|
|
var gotMember string
|
|
|
|
useFakeChatService(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if !(r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/spaces:setup")) {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
var body map[string]any
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
members := body["memberships"].([]any)
|
|
member := members[0].(map[string]any)["member"].(map[string]any)
|
|
gotMember, _ = member["name"].(string)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"name": "spaces/dm1",
|
|
"spaceType": "DIRECT_MESSAGE",
|
|
})
|
|
})
|
|
|
|
out := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "--account", "a@b.com", "chat", "dm", "space", "user@example.com"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
if gotMember != "users/user@example.com" {
|
|
t.Fatalf("unexpected member: %q", gotMember)
|
|
}
|
|
if !strings.Contains(out, "spaces/dm1") {
|
|
t.Fatalf("unexpected out=%q", out)
|
|
}
|
|
}
|
|
|
|
func TestExecute_ChatDMSend_JSON(t *testing.T) {
|
|
var gotText string
|
|
|
|
useFakeChatService(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/spaces:setup"):
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"name": "spaces/dm1",
|
|
})
|
|
case r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/spaces/dm1/messages"):
|
|
var body map[string]any
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
gotText, _ = body["text"].(string)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"name": "spaces/dm1/messages/m1",
|
|
})
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
|
|
out := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "--account", "a@b.com", "chat", "dm", "send", "user@example.com", "--text", "ping"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
if gotText != "ping" {
|
|
t.Fatalf("unexpected text: %q", gotText)
|
|
}
|
|
if !strings.Contains(out, "spaces/dm1/messages/m1") {
|
|
t.Fatalf("unexpected out=%q", out)
|
|
}
|
|
}
|