From 2f441b3c7d63461025752b8270335d8a92a6da93 Mon Sep 17 00:00:00 2001 From: badkaktus Date: Sat, 3 Feb 2024 21:54:40 +0200 Subject: [PATCH] write tests, fix some bugs --- .github/workflows/test.yaml | 25 ++ README.md | 21 +- authentication.go | 4 +- authentication_test.go | 81 +++++++ autotranslate.go | 1 + autotranslate_test.go | 29 +++ channels.go | 40 ++-- channels_test.go | 406 +++++++++++++++++++++++++++++++++ chat.go | 17 +- chat_test.go | 176 +++++++++++++++ go.mod | 5 +- go.sum | 23 ++ gorocket.go | 3 +- gorocket_logo.png | Bin 0 -> 93130 bytes gorocket_test.go | 86 +++++++ groups.go | 76 ++++--- groups_test.go | 440 ++++++++++++++++++++++++++++++++++++ helper.go | 26 +++ hooks.go | 3 - hooks_test.go | 34 +++ info.go | 28 ++- info_test.go | 201 ++++++++++++++++ users.go | 61 ++--- users_test.go | 279 +++++++++++++++++++++++ 24 files changed, 1948 insertions(+), 117 deletions(-) create mode 100644 .github/workflows/test.yaml create mode 100644 authentication_test.go create mode 100644 autotranslate_test.go create mode 100644 channels_test.go create mode 100644 chat_test.go create mode 100644 go.sum create mode 100644 gorocket_logo.png create mode 100644 gorocket_test.go create mode 100644 groups_test.go create mode 100644 helper.go create mode 100644 hooks_test.go create mode 100644 info_test.go create mode 100644 users_test.go diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..463b341 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,25 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Go + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: Test + run: go test -v -cover ./... \ No newline at end of file diff --git a/README.md b/README.md index 4cb5a6b..b8bf3d4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![gorocket logo](gorocket_logo.png) + # Golang Rocket Chat REST API client Use this simple client if you need to connect to Rocket Chat @@ -14,6 +16,15 @@ import ( ``` Create client +```go +client := gorocket.NewWithOptions("https://your-rocket-chat.com", + gorocket.WithUserID("my-user-id"), + gorocket.WithToken("my-bot-token"), + gorocket.WithTimeout(1 * time.Second), +) +``` +or + ```go client := gorocket.NewClient("https://your-rocket-chat.com") @@ -31,16 +42,6 @@ if err != nil { fmt.Printf("I'm %s", lg.Data.Me.Username) ``` -or - -```go -client := gorocket.NewWithOptions("https://your-rocket-chat.com", - gorocket.WithUserID("my-user-id"), - gorocket.WithToken("my-bot-token"), - gorocket.WithTimeout(1 * time.Second), -) -``` - ## Manage user ```go str := gorocket.NewUser{ diff --git a/authentication.go b/authentication.go index 5412356..743d1fd 100644 --- a/authentication.go +++ b/authentication.go @@ -135,8 +135,8 @@ type MeResponse struct { Success bool `json:"success"` } +// Login login the user with the given credentials. func (c *Client) Login(login *LoginPayload) (*LoginResponse, error) { - opt, _ := json.Marshal(login) url := fmt.Sprintf("%s/%s/login", c.baseURL, c.apiVersion) @@ -162,6 +162,7 @@ func (c *Client) Login(login *LoginPayload) (*LoginResponse, error) { return &res, nil } +// Logout logout the user. func (c *Client) Logout() (*LogoutResponse, error) { req, err := http.NewRequest("POST", fmt.Sprintf("%s/%s/logout", c.baseURL, c.apiVersion), nil) if err != nil { @@ -176,6 +177,7 @@ func (c *Client) Logout() (*LogoutResponse, error) { return &res, nil } +// Me get the user information. func (c *Client) Me() (*MeResponse, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s/me", c.baseURL, c.apiVersion), nil) if err != nil { diff --git a/authentication_test.go b/authentication_test.go new file mode 100644 index 0000000..9caec16 --- /dev/null +++ b/authentication_test.go @@ -0,0 +1,81 @@ +package gorocket + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLogin(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"status":"success","data":{"authToken":"9HqLlyZOugoStsXCUfD_0YdwnNnunAJF8V47U3QHXSq","userId":"aobEdbYhXfu5hkeqG","me":{"_id":"aYjNnig8BEAWeQzMh","name":"Rocket Cat","emails":[{"address":"rocket.cat@rocket.chat","verified":false}],"status":"offline","statusConnection":"offline","username":"rocket.cat","utcOffset":-3,"active":true,"roles":["admin"],"settings":{"preferences":{}},"avatarUrl":"http://localhost:3000/avatar/test"}}}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + payload := LoginPayload{ + User: "username", + Password: "password", + } + resp, err := client.Login(&payload) + require.NoError(t, err) + + require.Equal(t, "success", resp.Status) + require.Equal(t, "9HqLlyZOugoStsXCUfD_0YdwnNnunAJF8V47U3QHXSq", resp.Data.AuthToken) + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.Data.UserID) + require.Equal(t, "aYjNnig8BEAWeQzMh", resp.Data.Me.ID) + require.Equal(t, "Rocket Cat", resp.Data.Me.Name) + require.Equal(t, "rocket.cat@rocket.chat", resp.Data.Me.Emails[0].Address) + require.Equal(t, false, resp.Data.Me.Emails[0].Verified) + require.Equal(t, "offline", resp.Data.Me.Status) + require.Equal(t, "offline", resp.Data.Me.StatusConnection) + require.Equal(t, "rocket.cat", resp.Data.Me.Username) + require.Equal(t, -3.0, resp.Data.Me.UtcOffset) + require.Equal(t, true, resp.Data.Me.Active) + require.Equal(t, "admin", resp.Data.Me.Roles[0]) + require.IsType(t, Settings{}, resp.Data.Me.Settings) + require.IsType(t, Preferences{}, resp.Data.Me.Settings.Preferences) + require.Equal(t, "http://localhost:3000/avatar/test", resp.Data.Me.AvatarURL) +} + +func TestMe(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"_id":"aobEdbYhXfu5hkeqG","name":"Example User","emails":[{"address":"example@example.com","verified":true}],"status":"offline","statusConnection":"offline","username":"example","utcOffset":0,"active":true,"roles":["user","admin"],"settings":{"preferences":{"enableAutoAway":false,"idleTimeoutLimit":300,"desktopNotificationDuration":0,"audioNotifications":"mentions","desktopNotifications":"mentions","mobileNotifications":"mentions","unreadAlert":true,"useEmojis":true,"convertAsciiEmoji":true,"autoImageLoad":true,"saveMobileBandwidth":true,"collapseMediaByDefault":false,"hideUsernames":false,"hideRoles":false,"hideFlexTab":false,"hideAvatars":false,"roomsListExhibitionMode":"category","sidebarViewMode":"medium","sidebarHideAvatar":false,"sidebarShowUnread":false,"sidebarShowFavorites":true,"sendOnEnter":"normal","messageViewMode":0,"emailNotificationMode":"all","roomCounterSidebar":false,"newRoomNotification":"door","newMessageNotification":"chime","muteFocusedConversations":true,"notificationsSoundVolume":100}},"customFields":{"twitter":"@userstwi"},"avatarUrl":"http://localhost:3000/avatar/test","success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.Me() + require.NoError(t, err) + + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.ID) + require.Equal(t, "Example User", resp.Name) + require.Equal(t, "example@example.com", resp.Emails[0].Address) + require.True(t, resp.Emails[0].Verified) + require.Equal(t, "offline", resp.Status) + require.Equal(t, "offline", resp.StatusConnection) + require.Equal(t, "example", resp.Username) + require.Equal(t, 0.0, resp.UtcOffset) + require.True(t, resp.Active) + require.Equal(t, "user", resp.Roles[0]) + require.Equal(t, "admin", resp.Roles[1]) + require.IsType(t, Settings{}, resp.Settings) + require.IsType(t, Preferences{}, resp.Settings.Preferences) + require.Equal(t, "http://localhost:3000/avatar/test", resp.AvatarURL) + require.True(t, resp.Success) +} + +func TestLogout(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"status":"success","data":{"message":"You've been logged out!"}}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.Logout() + require.NoError(t, err) + + require.Equal(t, "success", resp.Status) + require.Equal(t, "You've been logged out!", resp.Data.Message) +} diff --git a/autotranslate.go b/autotranslate.go index 482f0f3..47cb0bf 100644 --- a/autotranslate.go +++ b/autotranslate.go @@ -15,6 +15,7 @@ type language struct { Name string `json:"name"` } +// GetSupportedLanguage returns a list of supported languages by the autotranslate func (c *Client) GetSupportedLanguage(query string) (*SupportedLanguageResp, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s/autotranslate.getSupportedLanguages", c.baseURL, c.apiVersion), nil) if err != nil { diff --git a/autotranslate_test.go b/autotranslate_test.go new file mode 100644 index 0000000..50037b2 --- /dev/null +++ b/autotranslate_test.go @@ -0,0 +1,29 @@ +package gorocket + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetSupportedLanguage(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"languages":[{"language":"af","name":"Africâner"},{"language":"sq","name":"Albanês"},{"language":"de","name":"Alemão"},{"language":"am","name":"Amárico"}],"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.GetSupportedLanguage("") + require.NoError(t, err) + + require.Equal(t, "af", resp.Languages[0].Language) + require.Equal(t, "Africâner", resp.Languages[0].Name) + require.Equal(t, "sq", resp.Languages[1].Language) + require.Equal(t, "Albanês", resp.Languages[1].Name) + require.Equal(t, "de", resp.Languages[2].Language) + require.Equal(t, "Alemão", resp.Languages[2].Name) + require.Equal(t, "am", resp.Languages[3].Language) + require.Equal(t, "Amárico", resp.Languages[3].Name) + require.True(t, resp.Success) +} diff --git a/channels.go b/channels.go index b356fec..fd59630 100644 --- a/channels.go +++ b/channels.go @@ -194,7 +194,7 @@ type SetTopicResponse struct { Success bool `json:"success"` } -// Adds all of the users on the server to a channel. +// AddAllToChannel adds all of the users on the server to a channel. func (c *Client) AddAllToChannel(params *AddAllRequest) (*AddAllResponse, error) { opt, _ := json.Marshal(params) @@ -215,7 +215,7 @@ func (c *Client) AddAllToChannel(params *AddAllRequest) (*AddAllResponse, error) return &res, nil } -// Archives a channel. +// ArchiveChannel archives a channel. func (c *Client) ArchiveChannel(param *SimpleChannelId) (*SimpleSuccessResponse, error) { opt, _ := json.Marshal(param) @@ -236,7 +236,7 @@ func (c *Client) ArchiveChannel(param *SimpleChannelId) (*SimpleSuccessResponse, return &res, nil } -// Removes the channel from the user's list of channels. +// CloseChannel removes the channel from the user's list of channels. func (c *Client) CloseChannel(param *SimpleChannelId) (*SimpleSuccessResponse, error) { opt, _ := json.Marshal(param) @@ -257,7 +257,7 @@ func (c *Client) CloseChannel(param *SimpleChannelId) (*SimpleSuccessResponse, e return &res, nil } -// Gets channel counters. +// ChannelCounters gets channel counters. func (c *Client) ChannelCounters(param *ChannelCountersRequest) (*ChannelCountersResponse, error) { req, err := http.NewRequest("GET", @@ -265,7 +265,7 @@ func (c *Client) ChannelCounters(param *ChannelCountersRequest) (*ChannelCounter nil) if param.RoomName == "" && param.RoomId == "" { - return nil, fmt.Errorf("False parameters") + return nil, fmt.Errorf("false parameters") } url := req.URL.Query() @@ -290,7 +290,7 @@ func (c *Client) ChannelCounters(param *ChannelCountersRequest) (*ChannelCounter return &res, nil } -// Creates a new channel. +// CreateChannel creates a new channel. func (c *Client) CreateChannel(param *CreateChannelRequest) (*CreateChannelResponse, error) { opt, _ := json.Marshal(param) @@ -311,7 +311,7 @@ func (c *Client) CreateChannel(param *CreateChannelRequest) (*CreateChannelRespo return &res, nil } -// Delete channel. +// DeleteChannel delete channel. func (c *Client) DeleteChannel(param *SimpleChannelRequest) (*SimpleSuccessResponse, error) { opt, _ := json.Marshal(param) @@ -332,7 +332,7 @@ func (c *Client) DeleteChannel(param *SimpleChannelRequest) (*SimpleSuccessRespo return &res, nil } -// Get channel info. +// ChannelInfo get channel info. func (c *Client) ChannelInfo(param *SimpleChannelRequest) (*ChannelInfoResponse, error) { req, err := http.NewRequest("GET", @@ -340,7 +340,7 @@ func (c *Client) ChannelInfo(param *SimpleChannelRequest) (*ChannelInfoResponse, nil) if param.RoomName == "" && param.RoomId == "" { - return nil, fmt.Errorf("False parameters") + return nil, fmt.Errorf("false parameters") } url := req.URL.Query() @@ -365,7 +365,7 @@ func (c *Client) ChannelInfo(param *SimpleChannelRequest) (*ChannelInfoResponse, return &res, nil } -// Adds a user to the channel. +// ChannelInvite adds a user to the channel. func (c *Client) ChannelInvite(param *InviteChannelRequest) (*InviteChannelResponse, error) { opt, _ := json.Marshal(param) @@ -386,7 +386,7 @@ func (c *Client) ChannelInvite(param *InviteChannelRequest) (*InviteChannelRespo return &res, nil } -// Kick a user from the channel. +// ChannelKick kick a user from the channel. func (c *Client) ChannelKick(param *InviteChannelRequest) (*InviteChannelResponse, error) { opt, _ := json.Marshal(param) @@ -407,7 +407,7 @@ func (c *Client) ChannelKick(param *InviteChannelRequest) (*InviteChannelRespons return &res, nil } -// Get channels list +// ChannelList get channels list func (c *Client) ChannelList() (*ChannelListResponse, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s/channels.list", c.baseURL, c.apiVersion), @@ -426,7 +426,7 @@ func (c *Client) ChannelList() (*ChannelListResponse, error) { return &res, nil } -// Gets channel members +// ChannelMembers gets channel members func (c *Client) ChannelMembers(param *SimpleChannelRequest) (*ChannelMembersResponse, error) { req, err := http.NewRequest("GET", @@ -434,7 +434,7 @@ func (c *Client) ChannelMembers(param *SimpleChannelRequest) (*ChannelMembersRes nil) if param.RoomName == "" && param.RoomId == "" { - return nil, fmt.Errorf("False parameters") + return nil, fmt.Errorf("false parameters") } url := req.URL.Query() @@ -459,7 +459,7 @@ func (c *Client) ChannelMembers(param *SimpleChannelRequest) (*ChannelMembersRes return &res, nil } -// Adds the channel back to the user's list of channels. +// OpenChannel adds the channel back to the user's list of channels. func (c *Client) OpenChannel(param *SimpleChannelId) (*SimpleSuccessResponse, error) { opt, _ := json.Marshal(param) @@ -480,7 +480,7 @@ func (c *Client) OpenChannel(param *SimpleChannelId) (*SimpleSuccessResponse, er return &res, nil } -// Changes a channel's name. +// RenameChannel changes a channel's name. func (c *Client) RenameChannel(param *RenameChannelRequest) (*RenameChannelResponse, error) { opt, _ := json.Marshal(param) @@ -501,7 +501,7 @@ func (c *Client) RenameChannel(param *RenameChannelRequest) (*RenameChannelRespo return &res, nil } -// Sets the announcement for the channel. +// SetAnnouncementChannel sets the announcement for the channel. func (c *Client) SetAnnouncementChannel(param *SetAnnouncementRequest) (*SetAnnouncementResponse, error) { opt, _ := json.Marshal(param) @@ -522,7 +522,7 @@ func (c *Client) SetAnnouncementChannel(param *SetAnnouncementRequest) (*SetAnno return &res, nil } -// Sets the Description for the channel. +// SetDescriptionChannel sets the Description for the channel. func (c *Client) SetDescriptionChannel(param *SetDescriptionRequest) (*SetDescriptionResponse, error) { opt, _ := json.Marshal(param) @@ -543,7 +543,7 @@ func (c *Client) SetDescriptionChannel(param *SetDescriptionRequest) (*SetDescri return &res, nil } -// Sets the topic for the channel. +// SetTopicChannel sets the topic for the channel. func (c *Client) SetTopicChannel(param *SetTopicRequest) (*SetTopicResponse, error) { opt, _ := json.Marshal(param) @@ -564,7 +564,7 @@ func (c *Client) SetTopicChannel(param *SetTopicRequest) (*SetTopicResponse, err return &res, nil } -// Unarchive a channel. +// UnarchiveChannel unarchive a channel. func (c *Client) UnarchiveChannel(param *SimpleChannelId) (*SimpleSuccessResponse, error) { opt, _ := json.Marshal(param) diff --git a/channels_test.go b/channels_test.go new file mode 100644 index 0000000..5a7df11 --- /dev/null +++ b/channels_test.go @@ -0,0 +1,406 @@ +package gorocket + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAddAllToChannel(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"channel":{"_id":"ByehQjC44FwMeiLbX","name":"channelname","t":"c","usernames":["example","rocket.cat"],"msgs":0,"u":{"_id":"aobEdbYhXfu5hkeqG","username":"example"},"ts":"2016-05-30T13:42:25.304Z"},"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := AddAllRequest{ + RoomId: "ByehQjC44FwMeiLbX", + } + resp, err := client.AddAllToChannel(&req) + require.NoError(t, err) + + require.Equal(t, "ByehQjC44FwMeiLbX", resp.Channel.ID) + require.Equal(t, "channelname", resp.Channel.Name) + require.Equal(t, "c", resp.Channel.T) + require.Equal(t, "example", resp.Channel.Usernames[0]) + require.Equal(t, "rocket.cat", resp.Channel.Usernames[1]) + require.Equal(t, 0, resp.Channel.Msgs) + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.Channel.U.ID) + require.Equal(t, "example", resp.Channel.U.Username) + require.Equal(t, "2016-05-30T13:42:25.304Z", resp.Channel.Ts.Format("2006-01-02T15:04:05.999Z")) + require.True(t, resp.Success) +} + +func TestArchiveChannel(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := SimpleChannelId{ + RoomId: "ByehQjC44FwMeiLbX", + } + resp, err := client.ArchiveChannel(&req) + require.NoError(t, err) + + require.True(t, resp.Success) +} + +func TestCloseChannel(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := SimpleChannelId{ + RoomId: "ByehQjC44FwMeiLbX", + } + resp, err := client.CloseChannel(&req) + require.NoError(t, err) + + require.True(t, resp.Success) +} + +func TestChannelCounters(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"joined":true,"members":78,"unreads":2,"unreadsFrom":"2018-02-23T17:15:51.907Z","msgs":304,"latest":"2018-02-23T17:17:03.110Z","userMentions":0,"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := ChannelCountersRequest{ + RoomId: "ByehQjC44FwMeiLbX", + } + resp, err := client.ChannelCounters(&req) + require.NoError(t, err) + + require.True(t, resp.Success) + require.True(t, resp.Joined) + require.Equal(t, 78, resp.Members) + require.Equal(t, 2, resp.Unreads) + require.Equal(t, "2018-02-23T17:15:51.907Z", resp.UnreadsFrom.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, 304, resp.Msgs) + require.Equal(t, "2018-02-23T17:17:03.11Z", resp.Latest.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, 0, resp.UserMentions) +} + +func TestCreateChannel(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"channel":{"_id":"ByehQjC44FwMeiLbX","name":"channelname","t":"c","usernames":["example"],"msgs":0,"u":{"_id":"aobEdbYhXfu5hkeqG","username":"example"},"ts":"2016-05-30T13:42:25.304Z"},"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := CreateChannelRequest{ + Name: "channelname", + Members: []string{"example"}, + } + resp, err := client.CreateChannel(&req) + require.NoError(t, err) + + require.Equal(t, "ByehQjC44FwMeiLbX", resp.Channel.ID) + require.Equal(t, "channelname", resp.Channel.Name) + require.Equal(t, "c", resp.Channel.T) + require.Equal(t, "example", resp.Channel.Usernames[0]) + require.Equal(t, 0, resp.Channel.Msgs) + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.Channel.U.ID) + require.Equal(t, "example", resp.Channel.U.Username) + require.Equal(t, "2016-05-30T13:42:25.304Z", resp.Channel.Ts.Format("2006-01-02T15:04:05.999Z")) + require.True(t, resp.Success) +} + +func TestDeleteChannel(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := SimpleChannelRequest{ + RoomId: "ByehQjC44FwMeiLbX", + } + resp, err := client.DeleteChannel(&req) + require.NoError(t, err) + + require.True(t, resp.Success) +} + +func TestGetChannelInfo(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"channel":{"_id":"ByehQjC44FwMeiLbX","name":"testing","fname":"testing","t":"c","msgs":0,"usersCount":2,"u":{"_id":"HKKPmF8rZh45GMHWH","username":"marcos.defendi"},"customFields":{},"broadcast":false,"encrypted":false,"ts":"2020-05-21T13:14:07.070Z","ro":false,"default":false,"sysMes":true,"_updatedAt":"2020-05-21T13:14:07.096Z"},"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := SimpleChannelRequest{ + RoomId: "ByehQjC44FwMeiLbX", + } + resp, err := client.ChannelInfo(&req) + require.NoError(t, err) + + require.Equal(t, "ByehQjC44FwMeiLbX", resp.Channel.ID) + require.Equal(t, "testing", resp.Channel.Name) + require.Equal(t, "testing", resp.Channel.Fname) + require.Equal(t, "c", resp.Channel.T) + require.Equal(t, 0, resp.Channel.Msgs) + require.Equal(t, 2, resp.Channel.UsersCount) + require.Equal(t, "HKKPmF8rZh45GMHWH", resp.Channel.U.ID) + require.Equal(t, "marcos.defendi", resp.Channel.U.Username) + require.Equal(t, false, resp.Channel.Broadcast) + require.Equal(t, false, resp.Channel.Encrypted) + require.Equal(t, "2020-05-21T13:14:07.07Z", resp.Channel.Ts.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, false, resp.Channel.Ro) + require.Equal(t, false, resp.Channel.Default) + require.Equal(t, true, resp.Channel.SysMes) + require.Equal(t, "2020-05-21T13:14:07.096Z", resp.Channel.UpdatedAt.Format("2006-01-02T15:04:05.999Z")) + require.True(t, resp.Success) +} + +func TestChannelInvite(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"channel":{"_id":"ByehQjC44FwMeiLbX","ts":"2016-11-30T21:23:04.737Z","t":"c","name":"testing","usernames":["testing"],"msgs":1,"_updatedAt":"2016-12-09T12:50:51.575Z","lm":"2016-12-09T12:50:51.555Z"},"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := InviteChannelRequest{ + RoomId: "ByehQjC44FwMeiLbX", + } + resp, err := client.ChannelInvite(&req) + require.NoError(t, err) + + require.Equal(t, "ByehQjC44FwMeiLbX", resp.Channel.ID) + require.Equal(t, "2016-11-30T21:23:04.737Z", resp.Channel.Ts.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "c", resp.Channel.T) + require.Equal(t, "testing", resp.Channel.Name) + require.Equal(t, "testing", resp.Channel.Usernames[0]) + require.Equal(t, 1, resp.Channel.Msgs) + require.Equal(t, "2016-12-09T12:50:51.575Z", resp.Channel.UpdatedAt.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "2016-12-09T12:50:51.555Z", resp.Channel.Lm.Format("2006-01-02T15:04:05.999Z")) + require.True(t, resp.Success) +} + +func TestChannelKick(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"channel":{"_id":"ByehQjC44FwMeiLbX","name":"invite-me","t":"c","usernames":["testing1"],"msgs":0,"u":{"_id":"aobEdbYhXfu5hkeqG","username":"testing1"},"ts":"2016-12-09T15:08:58.042Z","ro":false,"sysMes":true,"_updatedAt":"2016-12-09T15:22:40.656Z"},"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := InviteChannelRequest{ + RoomId: "ByehQjC44FwMeiLbX", + UserId: "testing1", + } + resp, err := client.ChannelKick(&req) + require.NoError(t, err) + + require.Equal(t, "ByehQjC44FwMeiLbX", resp.Channel.ID) + require.Equal(t, "invite-me", resp.Channel.Name) + require.Equal(t, "c", resp.Channel.T) + require.Equal(t, "testing1", resp.Channel.Usernames[0]) + require.Equal(t, 0, resp.Channel.Msgs) + require.Equal(t, "2016-12-09T15:08:58.042Z", resp.Channel.Ts.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "2016-12-09T15:22:40.656Z", resp.Channel.UpdatedAt.Format("2006-01-02T15:04:05.999Z")) + require.True(t, resp.Success) +} + +func TestChannelList(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"channels":[{"_id":"ByehQjC44FwMeiLbX","name":"test-test","t":"c","usernames":["testing1"],"msgs":0,"u":{"_id":"aobEdbYhXfu5hkeqG","username":"testing1"},"ts":"2016-12-09T15:08:58.042Z","ro":false,"sysMes":true,"_updatedAt":"2016-12-09T15:22:40.656Z"},{"_id":"t7qapfhZjANMRAi5w","name":"testing","t":"c","usernames":["testing2"],"msgs":0,"u":{"_id":"y65tAmHs93aDChMWu","username":"testing2"},"ts":"2016-12-01T15:08:58.042Z","ro":false,"sysMes":true,"_updatedAt":"2016-12-09T15:22:40.656Z"}],"offset":0,"count":1,"total":1,"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + resp, err := client.Count(2).ChannelList() + require.NoError(t, err) + + require.Equal(t, 2, len(resp.Channels)) + require.Equal(t, "ByehQjC44FwMeiLbX", resp.Channels[0].ID) + require.Equal(t, "test-test", resp.Channels[0].Name) + require.Equal(t, "c", resp.Channels[0].T) + require.Equal(t, "testing1", resp.Channels[0].Usernames[0]) + require.Equal(t, 0, resp.Channels[0].Msgs) + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.Channels[0].U.ID) + require.Equal(t, "testing1", resp.Channels[0].U.Username) + require.Equal(t, "2016-12-09T15:08:58.042Z", resp.Channels[0].Ts.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, false, resp.Channels[0].Ro) + require.Equal(t, true, resp.Channels[0].SysMes) + require.Equal(t, "2016-12-09T15:22:40.656Z", resp.Channels[0].UpdatedAt.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "t7qapfhZjANMRAi5w", resp.Channels[1].ID) + require.Equal(t, "testing", resp.Channels[1].Name) + require.Equal(t, "c", resp.Channels[1].T) + require.Equal(t, "testing2", resp.Channels[1].Usernames[0]) + require.Equal(t, 0, resp.Channels[1].Msgs) + require.Equal(t, "y65tAmHs93aDChMWu", resp.Channels[1].U.ID) + require.Equal(t, "testing2", resp.Channels[1].U.Username) + require.Equal(t, "2016-12-01T15:08:58.042Z", resp.Channels[1].Ts.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, false, resp.Channels[1].Ro) + require.Equal(t, true, resp.Channels[1].SysMes) + require.Equal(t, "2016-12-09T15:22:40.656Z", resp.Channels[1].UpdatedAt.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, 0, resp.Offset) + require.Equal(t, 1, resp.Count) + require.Equal(t, 1, resp.Total) + require.True(t, resp.Success) +} + +func TestChannelMembers(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"members":[{"_id":"Loz7qh9ChSqHMPymx","username":"customField_apiuser.test.1529436896005","name":"customField_apiuser.test.1529436896005","status":"offline"},{"_id":"Zc3Y3cRW7ZtS7Y8Hk","username":"customField_apiuser.test.1529436997563","name":"customField_apiuser.test.1529436997563","status":"offline"}],"count":2,"offset":0,"total":35,"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := SimpleChannelRequest{ + RoomId: "ByehQjC44FwMeiLbX", + } + resp, err := client.Offset(10).ChannelMembers(&req) + require.NoError(t, err) + + require.Equal(t, 2, len(resp.Members)) + require.Equal(t, "Loz7qh9ChSqHMPymx", resp.Members[0].ID) + require.Equal(t, "customField_apiuser.test.1529436896005", resp.Members[0].Username) + require.Equal(t, "customField_apiuser.test.1529436896005", resp.Members[0].Name) + require.Equal(t, "offline", resp.Members[0].Status) + require.Equal(t, "Zc3Y3cRW7ZtS7Y8Hk", resp.Members[1].ID) + require.Equal(t, "customField_apiuser.test.1529436997563", resp.Members[1].Username) + require.Equal(t, "customField_apiuser.test.1529436997563", resp.Members[1].Name) + require.Equal(t, "offline", resp.Members[1].Status) + require.Equal(t, 2, resp.Count) + require.Equal(t, 0, resp.Offset) + require.Equal(t, 35, resp.Total) + require.True(t, resp.Success) +} + +func TestOpenChannel(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := SimpleChannelId{ + RoomId: "ByehQjC44FwMeiLbX", + } + resp, err := client.OpenChannel(&req) + require.NoError(t, err) + + require.True(t, resp.Success) +} + +func TestRenameChannel(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"channel":{"_id":"ByehQjC44FwMeiLbX","name":"new-name","t":"c","usernames":["testing1"],"msgs":4,"u":{"_id":"aobEdbYhXfu5hkeqG","username":"testing1"},"ts":"2016-12-09T15:08:58.042Z","ro":false,"sysMes":true,"_updatedAt":"2016-12-09T15:57:44.686Z"},"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := RenameChannelRequest{ + RoomId: "ByehQjC44FwMeiLbX", + NewName: "new-name", + } + resp, err := client.RenameChannel(&req) + require.NoError(t, err) + + require.Equal(t, "ByehQjC44FwMeiLbX", resp.Channel.ID) + require.Equal(t, "new-name", resp.Channel.Name) + require.Equal(t, "c", resp.Channel.T) + require.Equal(t, "testing1", resp.Channel.Usernames[0]) + require.Equal(t, 4, resp.Channel.Msgs) + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.Channel.U.ID) + require.Equal(t, "testing1", resp.Channel.U.Username) + require.Equal(t, "2016-12-09T15:08:58.042Z", resp.Channel.Ts.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, false, resp.Channel.Ro) + require.Equal(t, true, resp.Channel.SysMes) + require.Equal(t, "2016-12-09T15:57:44.686Z", resp.Channel.UpdatedAt.Format("2006-01-02T15:04:05.999Z")) + require.True(t, resp.Success) +} + +func TestSetAnnouncementChannel(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"announcement":"Test out everything","success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := SetAnnouncementRequest{ + RoomId: "ByehQjC44FwMeiLbX", + Announcement: "Test out everything", + } + resp, err := client.SetAnnouncementChannel(&req) + require.NoError(t, err) + + require.Equal(t, "Test out everything", resp.Announcement) + require.True(t, resp.Success) +} + +func TestSetDescriptionChannel(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"description":"Test out everything","success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := SetDescriptionRequest{ + RoomId: "ByehQjC44FwMeiLbX", + Description: "Test out everything", + } + resp, err := client.SetDescriptionChannel(&req) + require.NoError(t, err) + + require.Equal(t, "Test out everything", resp.Description) + require.True(t, resp.Success) +} + +func TestSetTopicChannel(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"topic":"Test out everything","success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := SetTopicRequest{ + RoomId: "ByehQjC44FwMeiLbX", + Topic: "Test out everything", + } + resp, err := client.SetTopicChannel(&req) + require.NoError(t, err) + + require.Equal(t, "Test out everything", resp.Topic) + require.True(t, resp.Success) +} + +func TestUnarchiveChannel(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := SimpleChannelId{ + RoomId: "ByehQjC44FwMeiLbX", + } + resp, err := client.UnarchiveChannel(&req) + require.NoError(t, err) + + require.True(t, resp.Success) +} diff --git a/chat.go b/chat.go index fc655db..2ba729c 100644 --- a/chat.go +++ b/chat.go @@ -96,6 +96,7 @@ type DeleteMessageRequest struct { AsUser bool `json:"asUser,omitempty"` } +// todo add the remaining fields type DeleteMessageResponse struct { ID string `json:"_id"` Ts int64 `json:"ts"` @@ -160,7 +161,7 @@ type PinMessageResponse struct { Success bool `json:"success"` } -// Posts a new chat message. +// PostMessage posts a new chat message. func (c *Client) PostMessage(msg *Message) (*RespPostMessage, error) { opt, _ := json.Marshal(msg) @@ -182,7 +183,7 @@ func (c *Client) PostMessage(msg *Message) (*RespPostMessage, error) { return &res, nil } -// Retrieves a single chat message by the provided id. +// GetMessage retrieves a single chat message by the provided id. // Callee must have permission to access the room where the message resides. func (c *Client) GetMessage(param *SingleMessageId) (*GetMessageResponse, error) { req, err := http.NewRequest("GET", @@ -190,7 +191,7 @@ func (c *Client) GetMessage(param *SingleMessageId) (*GetMessageResponse, error) nil) if param.MessageId == "" { - return nil, fmt.Errorf("False parameters") + return nil, fmt.Errorf("false parameters") } url := req.URL.Query() @@ -212,7 +213,7 @@ func (c *Client) GetMessage(param *SingleMessageId) (*GetMessageResponse, error) return &res, nil } -// Chat Message Delete +// DeleteMessage deletes an existing chat message. func (c *Client) DeleteMessage(param *DeleteMessageRequest) (*DeleteMessageResponse, error) { opt, _ := json.Marshal(param) @@ -233,14 +234,14 @@ func (c *Client) DeleteMessage(param *DeleteMessageRequest) (*DeleteMessageRespo return &res, nil } -// Callee must have permission to access the room where the message resides. +// GetPinnedMessages retrieve pinned messages from a room. func (c *Client) GetPinnedMessages(param *GetPinnedMsgRequest) (*GetPinnedMsgResponse, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s/chat.getPinnedMessages", c.baseURL, c.apiVersion), nil) if param.RoomId == "" { - return nil, fmt.Errorf("False parameters") + return nil, fmt.Errorf("false parameters") } url := req.URL.Query() @@ -268,7 +269,7 @@ func (c *Client) GetPinnedMessages(param *GetPinnedMsgRequest) (*GetPinnedMsgRes return &res, nil } -// Pins a chat message to the message's channel. +// PinMessage pins a chat message to the message's channel. func (c *Client) PinMessage(param *SingleMessageId) (*PinMessageResponse, error) { opt, _ := json.Marshal(param) @@ -289,7 +290,7 @@ func (c *Client) PinMessage(param *SingleMessageId) (*PinMessageResponse, error) return &res, nil } -// Unpins a chat message to the message's channel. +// UnpinMessage unpins a chat message to the message's channel. func (c *Client) UnpinMessage(param *SingleMessageId) (*SimpleSuccessResponse, error) { opt, _ := json.Marshal(param) diff --git a/chat_test.go b/chat_test.go new file mode 100644 index 0000000..9fc0efc --- /dev/null +++ b/chat_test.go @@ -0,0 +1,176 @@ +package gorocket + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPostMessage(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"message":{"rid":"GENERAL","msg":"123456789","ts":"2018-03-01T18:02:26.825Z","u":{"_id":"i5FdM4ssFgAcQP62k","username":"rocket.cat","name":"test"},"unread":true,"mentions":[],"channels":[],"_updatedAt":"2018-03-01T18:02:26.828Z","_id":"LnCSJxxNkCy6K9X8X"},"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + payload := Message{ + Text: "Hello", + } + resp, err := client.PostMessage(&payload) + require.NoError(t, err) + + require.Equal(t, "GENERAL", resp.Message.Rid) + require.Equal(t, "123456789", resp.Message.Msg) + require.Equal(t, "2018-03-01T18:02:26.825Z", resp.Message.Ts.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "i5FdM4ssFgAcQP62k", resp.Message.U.ID) + require.Equal(t, "rocket.cat", resp.Message.U.Username) + require.Equal(t, "2018-03-01T18:02:26.828Z", resp.Message.UpdatedAt.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "LnCSJxxNkCy6K9X8X", resp.Message.ID) + require.True(t, resp.Success) +} + +func TestGetMessage(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"message":{"_id":"7aDSXtjMA3KPLxLjt","rid":"GENERAL","msg":"This is a test!","ts":"2016-12-14T20:56:05.117Z","u":{"_id":"y65tAmHs93aDChMWu","username":"graywolf336"}},"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := SingleMessageId{ + MessageId: "7aDSXtjMA3KPLxLjt", + } + + resp, err := client.GetMessage(&req) + require.NoError(t, err) + + require.Equal(t, "7aDSXtjMA3KPLxLjt", resp.Message.ID) + require.Equal(t, "GENERAL", resp.Message.Rid) + require.Equal(t, "This is a test!", resp.Message.Msg) + require.Equal(t, "2016-12-14T20:56:05.117Z", resp.Message.Ts.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "y65tAmHs93aDChMWu", resp.Message.U.ID) + require.Equal(t, "graywolf336", resp.Message.U.Username) + require.True(t, resp.Success) +} + +func TestDeleteMessage(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"_id":"jEnjsxuoDJamGjbH2","ts":1696533809813,"message":{"_id":"jEnjsxuoDJamGjbH2","rid":"6GFJ3tbmHiyHbahmC","u":{"_id":"5fRTXMt7DMJbpPJfh","username":"test.funke","name":"TestFunke"}},"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := DeleteMessageRequest{ + RoomID: "6GFJ3tbmHiyHbahmC", + MsgID: "jEnjsxuoDJamGjbH2", + } + + resp, err := client.DeleteMessage(&req) + require.NoError(t, err) + + require.Equal(t, "jEnjsxuoDJamGjbH2", resp.ID) + require.Equal(t, int64(1696533809813), resp.Ts) + require.True(t, resp.Success) +} + +func TestGetPinnedMessages(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"messages":[{"_id":"Srhca3mgthgjkEisJ","rid":"ByehQjC44FwMeiLbX","msg":"I pinned this message","ts":"2019-03-23T00:53:24.388Z","u":{"_id":"aobEdbYhXfu5hkeqG","username":"user","name":"User"},"mentions":[],"channels":[],"_updatedAt":"2019-03-23T00:53:28.813Z","pinned":true,"pinnedAt":"2019-03-23T00:53:28.813Z","pinnedBy":{"_id":"aobEdbYhXfu5hkeqG","username":"user"}},{"_id":"m3AZcKrvayKEZSKJN","rid":"GENERAL","msg":"Ola","ts":"2019-03-23T00:53:50.974Z","u":{"_id":"aobEdbYhXfu5hkeqG","username":"user","name":"user"},"mentions":[],"channels":[],"_updatedAt":"2019-03-23T00:53:53.649Z","pinned":true,"pinnedAt":"2019-03-23T00:53:53.649Z","pinnedBy":{"_id":"aobEdbYhXfu5hkeqG","username":"user"}}],"count":2,"offset":0,"total":2,"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := GetPinnedMsgRequest{ + RoomId: "GENERAL", + Count: 2, + Offset: 0, + } + + resp, err := client.Sort(map[string]int{"ts": -1}).GetPinnedMessages(&req) + require.NoError(t, err) + + require.Equal(t, 2, resp.Count) + require.Equal(t, 0, resp.Offset) + require.Equal(t, 2, resp.Total) + require.True(t, resp.Success) + require.Equal(t, "Srhca3mgthgjkEisJ", resp.Messages[0].ID) + require.Equal(t, "ByehQjC44FwMeiLbX", resp.Messages[0].Rid) + require.Equal(t, "I pinned this message", resp.Messages[0].Msg) + require.Equal(t, "2019-03-23T00:53:24.388Z", resp.Messages[0].Ts.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.Messages[0].U.ID) + require.Equal(t, "user", resp.Messages[0].U.Username) + require.Equal(t, "User", resp.Messages[0].U.Name) + require.Equal(t, 0, len(resp.Messages[0].Mentions)) + require.Equal(t, 0, len(resp.Messages[0].Channels)) + require.Equal(t, "2019-03-23T00:53:28.813Z", resp.Messages[0].UpdatedAt.Format("2006-01-02T15:04:05.999Z")) + require.True(t, resp.Messages[0].Pinned) + require.Equal(t, "2019-03-23T00:53:28.813Z", resp.Messages[0].PinnedAt.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.Messages[0].PinnedBy.ID) + require.Equal(t, "user", resp.Messages[0].PinnedBy.Username) + require.Equal(t, "m3AZcKrvayKEZSKJN", resp.Messages[1].ID) + require.Equal(t, "GENERAL", resp.Messages[1].Rid) + require.Equal(t, "Ola", resp.Messages[1].Msg) + require.Equal(t, "2019-03-23T00:53:50.974Z", resp.Messages[1].Ts.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.Messages[1].U.ID) + require.Equal(t, "user", resp.Messages[1].U.Username) + require.Equal(t, "user", resp.Messages[1].U.Name) + require.Equal(t, 0, len(resp.Messages[1].Mentions)) + require.Equal(t, 0, len(resp.Messages[1].Channels)) + require.Equal(t, "2019-03-23T00:53:53.649Z", resp.Messages[1].UpdatedAt.Format("2006-01-02T15:04:05.999Z")) + require.True(t, resp.Messages[1].Pinned) + require.Equal(t, "2019-03-23T00:53:53.649Z", resp.Messages[1].PinnedAt.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.Messages[1].PinnedBy.ID) + require.Equal(t, "user", resp.Messages[1].PinnedBy.Username) +} + +func TestPinMessage(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"message":{"t":"message_pinned","rid":"GENERAL","ts":"2017-09-27T20:39:57.921Z","msg":"","u":{"_id":"Z3cpiYN6CNK2oXWKv","username":"graywolf336"},"groupable":false,"attachments":[{"text":"Hello","author_name":"graywolf336","author_icon":"/avatar/graywolf336?_dc=0","ts":"2017-09-27T19:36:01.683Z"}],"_updatedAt":"2017-09-27T20:39:57.921Z","_id":"hmzxXKSWmMkoQyiAd"},"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := SingleMessageId{ + MessageId: "jEnjsxuoDJamGjbH2", + } + + resp, err := client.PinMessage(&req) + require.NoError(t, err) + + require.Equal(t, "message_pinned", resp.Message.T) + require.Equal(t, "GENERAL", resp.Message.Rid) + require.Equal(t, "2017-09-27T20:39:57.921Z", resp.Message.Ts.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "", resp.Message.Msg) + require.Equal(t, "Z3cpiYN6CNK2oXWKv", resp.Message.U.ID) + require.Equal(t, "graywolf336", resp.Message.U.Username) + require.False(t, resp.Message.Groupable) + require.Equal(t, "Hello", resp.Message.Attachments[0].Text) + require.Equal(t, "graywolf336", resp.Message.Attachments[0].AuthorName) + require.Equal(t, "/avatar/graywolf336?_dc=0", resp.Message.Attachments[0].AuthorIcon) + require.Equal(t, "2017-09-27T19:36:01.683Z", resp.Message.Attachments[0].Ts.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "2017-09-27T20:39:57.921Z", resp.Message.UpdatedAt.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "hmzxXKSWmMkoQyiAd", resp.Message.ID) + require.True(t, resp.Success) +} + +func TestUnpinMessage(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + + req := SingleMessageId{ + MessageId: "jEnjsxuoDJamGjbH2", + } + + resp, err := client.UnpinMessage(&req) + require.NoError(t, err) + + require.True(t, resp.Success) +} diff --git a/go.mod b/go.mod index 8cf77ed..bc53a26 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/badkaktus/gorocket go 1.13 -require github.com/google/go-querystring v1.1.0 +require ( + github.com/google/go-querystring v1.1.0 + github.com/stretchr/testify v1.8.4 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e5bf923 --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gorocket.go b/gorocket.go index 6b22238..358c49d 100644 --- a/gorocket.go +++ b/gorocket.go @@ -3,10 +3,11 @@ package gorocket import ( "context" "encoding/json" - "github.com/google/go-querystring/query" "log" "net/http" "time" + + "github.com/google/go-querystring/query" ) type Client struct { diff --git a/gorocket_logo.png b/gorocket_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c05e0d3ab87ff50ee963ddd3a7636ba58431a264 GIT binary patch literal 93130 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

MSuTb9*a}NeASONK- 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 literal 0 HcmV?d00001 diff --git a/gorocket_test.go b/gorocket_test.go new file mode 100644 index 0000000..40346cc --- /dev/null +++ b/gorocket_test.go @@ -0,0 +1,86 @@ +package gorocket + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func NewTestServer(responseHandler http.HandlerFunc) *httptest.Server { + return httptest.NewServer(responseHandler) +} + +func NewTestClientWithCustomHandler(t *testing.T, server *httptest.Server) *Client { + client := NewClient(server.URL) + + if client.HTTPClient.Timeout != 5*time.Minute { + t.Errorf("Expected timeout to be 5 minutes, got %v", client.HTTPClient.Timeout) + } + + if client.baseURL != server.URL { + t.Errorf("Expected base URL to be %s, got %s", server.URL, client.baseURL) + } + + if client.apiVersion != "api/v1" { + t.Errorf("Expected API version to be api/v1, got %s", client.apiVersion) + } + + return client +} + +func NewTestClient(t *testing.T) *Client { + responseHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + server := NewTestServer(responseHandler) + + defer server.Close() + + client := NewClient(server.URL) + + if client.HTTPClient.Timeout != 5*time.Minute { + t.Errorf("Expected timeout to be 5 minutes, got %v", client.HTTPClient.Timeout) + } + + if client.baseURL != server.URL { + t.Errorf("Expected base URL to be %s, got %s", server.URL, client.baseURL) + } + + if client.apiVersion != "api/v1" { + t.Errorf("Expected API version to be api/v1, got %s", client.apiVersion) + } + + return client +} + +func TestNewClient(t *testing.T) { + NewTestClient(t) +} + +func TestClientWithOptions(t *testing.T) { + server := NewTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + client := NewWithOptions( + server.URL, + WithTimeout(5*time.Minute), + WithUserID("user"), + WithXToken("token"), + ) + + if client.HTTPClient.Timeout != 5*time.Minute { + t.Errorf("Expected timeout to be 5 minutes, got %v", client.HTTPClient.Timeout) + } + + if client.baseURL != server.URL { + t.Errorf("Expected base URL to be %s, got %s", server.URL, client.baseURL) + } + + if client.apiVersion != "api/v1" { + t.Errorf("Expected API version to be api/v1, got %s", client.apiVersion) + } +} diff --git a/groups.go b/groups.go index eccca20..d88fdc1 100644 --- a/groups.go +++ b/groups.go @@ -80,7 +80,9 @@ type InviteGroupResponse struct { Ts time.Time `json:"ts"` T string `json:"t"` Name string `json:"name"` - Usernames []string `json:"usernames"` + Usernames []string `json:"usernames,omitempty"` + U UChat `json:"u"` + Username string `json:"username,omitempty"` Msgs int `json:"msgs"` UpdatedAt time.Time `json:"_updatedAt"` Lm time.Time `json:"lm"` @@ -118,16 +120,17 @@ type GroupMembersResponse struct { } type GroupMessage struct { - ID string `json:"_id"` - Rid string `json:"rid"` - Msg string `json:"msg"` - Ts time.Time `json:"ts"` - U U `json:"u"` - UpdatedAt time.Time `json:"_updatedAt"` - Reactions []interface{} `json:"reactions"` - Mentions []U `json:"mentions"` - Channels []interface{} `json:"channels"` - Starred []interface{} `json:"starred"` + ID string `json:"_id"` + Rid string `json:"rid"` + Msg string `json:"msg"` + Ts time.Time `json:"ts"` + U U `json:"u"` + UpdatedAt time.Time `json:"_updatedAt"` + // todo create struct for these and uncomment in test + // Reactions []interface{} `json:"reactions"` + // Mentions []U `json:"mentions"` + // Channels []interface{} `json:"channels"` + // Starred []interface{} `json:"starred"` } type GroupMessagesResponse struct { @@ -153,7 +156,7 @@ type AddGroupPermissionRequest struct { UserId string `json:"userId"` } -// Archives a group. +// ArchiveGroup archives a group. func (c *Client) ArchiveGroup(param *SimpleGroupId) (*SimpleSuccessResponse, error) { opt, _ := json.Marshal(param) @@ -174,7 +177,7 @@ func (c *Client) ArchiveGroup(param *SimpleGroupId) (*SimpleSuccessResponse, err return &res, nil } -// Removes the group from the user's list of groups. +// CloseGroup closes a group. func (c *Client) CloseGroup(param *SimpleGroupId) (*SimpleSuccessResponse, error) { opt, _ := json.Marshal(param) @@ -195,7 +198,7 @@ func (c *Client) CloseGroup(param *SimpleGroupId) (*SimpleSuccessResponse, error return &res, nil } -// Gets group counters. +// GroupCounters gets counters of the group. func (c *Client) GroupCounters(param *GroupCountersRequest) (*GroupCountersResponse, error) { req, err := http.NewRequest("GET", @@ -203,7 +206,7 @@ func (c *Client) GroupCounters(param *GroupCountersRequest) (*GroupCountersRespo nil) if param.RoomName == "" && param.RoomId == "" { - return nil, fmt.Errorf("False parameters") + return nil, fmt.Errorf("false parameters") } url := req.URL.Query() @@ -228,7 +231,7 @@ func (c *Client) GroupCounters(param *GroupCountersRequest) (*GroupCountersRespo return &res, nil } -// Creates a new group. +// CreateGroup creates a group. func (c *Client) CreateGroup(param *CreateGroupRequest) (*CreateGroupResponse, error) { opt, _ := json.Marshal(param) @@ -249,7 +252,7 @@ func (c *Client) CreateGroup(param *CreateGroupRequest) (*CreateGroupResponse, e return &res, nil } -// Delete group. +// DeleteGroup deletes a group. func (c *Client) DeleteGroup(param *SimpleGroupId) (*SimpleSuccessResponse, error) { opt, _ := json.Marshal(param) @@ -270,7 +273,7 @@ func (c *Client) DeleteGroup(param *SimpleGroupId) (*SimpleSuccessResponse, erro return &res, nil } -// Get group info. +// GroupInfo gets group information. func (c *Client) GroupInfo(param *SimpleGroupRequest) (*GroupInfoResponse, error) { req, err := http.NewRequest("GET", @@ -278,7 +281,7 @@ func (c *Client) GroupInfo(param *SimpleGroupRequest) (*GroupInfoResponse, error nil) if param.RoomName == "" && param.RoomId == "" { - return nil, fmt.Errorf("False parameters") + return nil, fmt.Errorf("false parameters") } url := req.URL.Query() @@ -303,7 +306,7 @@ func (c *Client) GroupInfo(param *SimpleGroupRequest) (*GroupInfoResponse, error return &res, nil } -// Adds a user to the group. +// GroupInvite invites a user to the group. func (c *Client) GroupInvite(param *InviteGroupRequest) (*InviteGroupResponse, error) { opt, _ := json.Marshal(param) @@ -324,7 +327,7 @@ func (c *Client) GroupInvite(param *InviteGroupRequest) (*InviteGroupResponse, e return &res, nil } -// Kick a user from the group. +// GroupKick removes a user from the group. func (c *Client) GroupKick(param *InviteGroupRequest) (*InviteGroupResponse, error) { opt, _ := json.Marshal(param) @@ -345,7 +348,7 @@ func (c *Client) GroupKick(param *InviteGroupRequest) (*InviteGroupResponse, err return &res, nil } -// Get groups list +// GroupList gets the list of groups the caller is part of. func (c *Client) GroupList() (*GroupListResponse, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s/groups.list", c.baseURL, c.apiVersion), @@ -364,7 +367,7 @@ func (c *Client) GroupList() (*GroupListResponse, error) { return &res, nil } -// Gets group members +// GroupMembers gets the list of members of a group. func (c *Client) GroupMembers(param *SimpleGroupRequest) (*GroupMembersResponse, error) { req, err := http.NewRequest("GET", @@ -372,7 +375,7 @@ func (c *Client) GroupMembers(param *SimpleGroupRequest) (*GroupMembersResponse, nil) if param.RoomName == "" && param.RoomId == "" { - return nil, fmt.Errorf("False parameters") + return nil, fmt.Errorf("false parameters") } url := req.URL.Query() @@ -397,15 +400,14 @@ func (c *Client) GroupMembers(param *SimpleGroupRequest) (*GroupMembersResponse, return &res, nil } -// Gets group messages +// GroupMessages gets the messages from a group. func (c *Client) GroupMessages(param *SimpleGroupRequest) (*GroupMessagesResponse, error) { - req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s/groups.messages", c.baseURL, c.apiVersion), nil) if param.RoomName == "" && param.RoomId == "" { - return nil, fmt.Errorf("False parameters") + return nil, fmt.Errorf("false parameters") } url := req.URL.Query() @@ -430,7 +432,7 @@ func (c *Client) GroupMessages(param *SimpleGroupRequest) (*GroupMessagesRespons return &res, nil } -// Adds the group back to the user's list of groups. +// OpenGroup opens a group. func (c *Client) OpenGroup(param *SimpleGroupId) (*SimpleSuccessResponse, error) { opt, _ := json.Marshal(param) @@ -451,7 +453,7 @@ func (c *Client) OpenGroup(param *SimpleGroupId) (*SimpleSuccessResponse, error) return &res, nil } -// Changes a group's name. +// RenameGroup renames a group. func (c *Client) RenameGroup(param *RenameGroupRequest) (*RenameGroupResponse, error) { opt, _ := json.Marshal(param) @@ -472,10 +474,10 @@ func (c *Client) RenameGroup(param *RenameGroupRequest) (*RenameGroupResponse, e return &res, nil } -// Add leader for the group. +// AddLeaderGroup adds a leader for the group. func (c *Client) AddLeaderGroup(param *AddGroupPermissionRequest) (*SimpleSuccessResponse, error) { if param.UserId == "" && param.RoomId == "" { - return nil, fmt.Errorf("False parameters") + return nil, fmt.Errorf("false parameters") } opt, _ := json.Marshal(param) @@ -497,10 +499,10 @@ func (c *Client) AddLeaderGroup(param *AddGroupPermissionRequest) (*SimpleSucces return &res, nil } -// Add owner for the group. +// AddOwnerGroup adds an owner for the group. func (c *Client) AddOwnerGroup(param *AddGroupPermissionRequest) (*SimpleSuccessResponse, error) { if param.UserId == "" && param.RoomId == "" { - return nil, fmt.Errorf("False parameters") + return nil, fmt.Errorf("false parameters") } opt, _ := json.Marshal(param) @@ -522,7 +524,7 @@ func (c *Client) AddOwnerGroup(param *AddGroupPermissionRequest) (*SimpleSuccess return &res, nil } -// Sets the announcement for the group. +// SetAnnouncementGroup sets the announcement for the group. func (c *Client) SetAnnouncementGroup(param *SetAnnouncementRequest) (*SetAnnouncementResponse, error) { opt, _ := json.Marshal(param) @@ -543,7 +545,7 @@ func (c *Client) SetAnnouncementGroup(param *SetAnnouncementRequest) (*SetAnnoun return &res, nil } -// Sets the Description for the group. +// SetDescriptionGroup sets the description for the group. func (c *Client) SetDescriptionGroup(param *SetDescriptionRequest) (*SetDescriptionResponse, error) { opt, _ := json.Marshal(param) @@ -564,7 +566,7 @@ func (c *Client) SetDescriptionGroup(param *SetDescriptionRequest) (*SetDescript return &res, nil } -// Sets the topic for the group. +// SetTopicGroup sets the topic for the group. func (c *Client) SetTopicGroup(param *SetTopicRequest) (*SetTopicResponse, error) { opt, _ := json.Marshal(param) @@ -585,7 +587,7 @@ func (c *Client) SetTopicGroup(param *SetTopicRequest) (*SetTopicResponse, error return &res, nil } -// Unarchive a group. +// UnarchiveGroup unarchives a group. func (c *Client) UnarchiveGroup(param *SimpleGroupId) (*SimpleSuccessResponse, error) { opt, _ := json.Marshal(param) diff --git a/groups_test.go b/groups_test.go new file mode 100644 index 0000000..97debd1 --- /dev/null +++ b/groups_test.go @@ -0,0 +1,440 @@ +package gorocket + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestArchiveGroup(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + req := SimpleGroupId{ + RoomId: "GENERAL", + } + resp, err := client.ArchiveGroup(&req) + require.NoError(t, err) + + require.True(t, resp.Success) +} + +func TestCloseGroup(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + req := SimpleGroupId{ + RoomId: "GENERAL", + } + resp, err := client.CloseGroup(&req) + require.NoError(t, err) + + require.True(t, resp.Success) +} + +func TestGroupCounters(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"joined":true,"members":1,"unreads":1,"unreadsFrom":"2018-02-18T21:51:20.091Z","msgs":1,"latest":"2018-02-23T17:20:17.345Z","userMentions":0,"success":true}`, + })) + defer server.Close() + + req := GroupCountersRequest{ + RoomId: "GENERAL", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.GroupCounters(&req) + require.NoError(t, err) + + require.True(t, resp.Success) + require.True(t, resp.Joined) + require.Equal(t, 1, resp.Members) + require.Equal(t, 1, resp.Unreads) + require.Equal(t, "2018-02-18T21:51:20.091Z", resp.UnreadsFrom.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, 1, resp.Msgs) + require.Equal(t, "2018-02-23T17:20:17.345Z", resp.Latest.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, 0, resp.UserMentions) +} + +func TestCreateGroup(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"group":{"_id":"NtR6RQ7NvzA9ejecX","name":"testing","t":"p","msgs":0,"u":{"_id":"aobEdbYhXfu5hkeqG","username":"tester"},"ts":"2016-12-09T16:53:06.761Z","ro":false,"sysMes":true,"_updatedAt":"2016-12-09T16:53:06.761Z"},"success":true}`, + })) + defer server.Close() + + req := CreateGroupRequest{ + Name: "GENERAL", + ReadOnly: false, + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.CreateGroup(&req) + require.NoError(t, err) + + require.True(t, resp.Success) + require.Equal(t, "NtR6RQ7NvzA9ejecX", resp.Group.ID) + require.Equal(t, "testing", resp.Group.Name) + require.Equal(t, "p", resp.Group.T) + require.Equal(t, 0, resp.Group.Msgs) + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.Group.U.ID) + require.Equal(t, "tester", resp.Group.U.Username) + require.Equal(t, "2016-12-09T16:53:06.761Z", resp.Group.Ts.Format("2006-01-02T15:04:05.000Z")) +} + +func TestDeleteGroup(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + req := SimpleGroupId{ + RoomId: "GENERAL", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.DeleteGroup(&req) + require.NoError(t, err) + + require.True(t, resp.Success) +} + +func TestGroupInfo(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"group":{"_id":"ByehQjC44FwMeiLbX","name":"testing","fname":"testing","t":"p","msgs":0,"usersCount":2,"u":{"_id":"HKKPmF8rZh45GMHWH","username":"marcos.defendi"},"customFields":{},"broadcast":false,"encrypted":false,"ts":"2020-05-21T13:16:24.749Z","ro":false,"default":false,"sysMes":true,"_updatedAt":"2020-05-21T13:16:24.772Z"},"success":true}`, + })) + defer server.Close() + + req := SimpleGroupRequest{ + RoomId: "GENERAL", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.GroupInfo(&req) + require.NoError(t, err) + + require.True(t, resp.Success) + require.Equal(t, "ByehQjC44FwMeiLbX", resp.Group.ID) + require.Equal(t, "testing", resp.Group.Name) + require.Equal(t, "testing", resp.Group.Fname) + require.Equal(t, "p", resp.Group.T) + require.Equal(t, 0, resp.Group.Msgs) + require.Equal(t, 2, resp.Group.UsersCount) + require.Equal(t, "HKKPmF8rZh45GMHWH", resp.Group.U.ID) + require.Equal(t, "marcos.defendi", resp.Group.U.Username) + require.False(t, resp.Group.Broadcast) + require.False(t, resp.Group.Encrypted) + require.Equal(t, "2020-05-21T13:16:24.749Z", resp.Group.Ts.Format("2006-01-02T15:04:05.000Z")) + require.False(t, resp.Group.Ro) + require.False(t, resp.Group.Default) + require.True(t, resp.Group.SysMes) + require.Equal(t, "2020-05-21T13:16:24.772Z", resp.Group.UpdatedAt.Format("2006-01-02T15:04:05.000Z")) +} + +func TestGroupInvite(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"group":{"_id":"ByehQjC44FwMeiLbX","ts":"2016-11-30T21:23:04.737Z","t":"p","name":"testing","username":"testing","u":{"_id":"aobEdbYhXfu5hkeqG","username":"testing1"},"msgs":1,"_updatedAt":"2016-12-09T12:50:51.575Z","lm":"2016-12-09T12:50:51.555Z"},"success":true}`, + })) + defer server.Close() + + req := InviteGroupRequest{ + RoomId: "GENERAL", + UserId: "1234", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.GroupInvite(&req) + require.NoError(t, err) + + require.True(t, resp.Success) + require.Equal(t, "ByehQjC44FwMeiLbX", resp.Group.ID) + require.Equal(t, "2016-11-30T21:23:04.737Z", resp.Group.Ts.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "p", resp.Group.T) + require.Equal(t, "testing", resp.Group.Name) + require.Equal(t, "testing", resp.Group.Username) + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.Group.U.ID) + require.Equal(t, "testing1", resp.Group.U.Username) + require.Equal(t, 1, resp.Group.Msgs) + require.Equal(t, "2016-12-09T12:50:51.575Z", resp.Group.UpdatedAt.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "2016-12-09T12:50:51.555Z", resp.Group.Lm.Format("2006-01-02T15:04:05.000Z")) +} + +func TestGroupKick(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"group":{"_id":"ByehQjC44FwMeiLbX","ts":"2016-11-30T21:23:04.737Z","t":"p","name":"testing","username":"testing","u":{"_id":"aobEdbYhXfu5hkeqG","username":"testing1"},"msgs":1,"_updatedAt":"2016-12-09T12:50:51.575Z","lm":"2016-12-09T12:50:51.555Z"},"success":true}`, + })) + defer server.Close() + + req := InviteGroupRequest{ + RoomId: "GENERAL", + UserId: "1234", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.GroupKick(&req) + require.NoError(t, err) + + require.True(t, resp.Success) + require.Equal(t, "ByehQjC44FwMeiLbX", resp.Group.ID) + require.Equal(t, "2016-11-30T21:23:04.737Z", resp.Group.Ts.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "p", resp.Group.T) + require.Equal(t, "testing", resp.Group.Name) + require.Equal(t, "testing", resp.Group.Username) + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.Group.U.ID) + require.Equal(t, "testing1", resp.Group.U.Username) + require.Equal(t, 1, resp.Group.Msgs) + require.Equal(t, "2016-12-09T12:50:51.575Z", resp.Group.UpdatedAt.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "2016-12-09T12:50:51.555Z", resp.Group.Lm.Format("2006-01-02T15:04:05.000Z")) +} + +func TestGroupList(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"groups":[{"_id":"ByehQjC44FwMeiLbX","name":"test-test","t":"p","msgs":0,"u":{"_id":"aobEdbYhXfu5hkeqG","username":"testing1"},"ts":"2016-12-09T15:08:58.042Z","ro":false,"sysMes":true,"_updatedAt":"2016-12-09T15:22:40.656Z"},{"_id":"t7qapfhZjANMRAi5w","name":"testing","t":"p","msgs":0,"u":{"_id":"y65tAmHs93aDChMWu","username":"testing2"},"ts":"2016-12-01T15:08:58.042Z","ro":false,"sysMes":true,"_updatedAt":"2016-12-09T15:22:40.656Z"}],"offset":0,"count":1,"total":1,"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.GroupList() + require.NoError(t, err) + + require.True(t, resp.Success) + require.Equal(t, 0, resp.Offset) + require.Equal(t, 1, resp.Count) + require.Equal(t, 1, resp.Total) + require.Equal(t, 2, len(resp.Groups)) + require.Equal(t, "ByehQjC44FwMeiLbX", resp.Groups[0].ID) + require.Equal(t, "test-test", resp.Groups[0].Name) + require.Equal(t, "p", resp.Groups[0].T) + require.Equal(t, 0, resp.Groups[0].Msgs) + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.Groups[0].U.ID) + require.Equal(t, "testing1", resp.Groups[0].U.Username) + require.Equal(t, "2016-12-09T15:08:58.042Z", resp.Groups[0].Ts.Format("2006-01-02T15:04:05.000Z")) + require.False(t, resp.Groups[0].Ro) + require.True(t, resp.Groups[0].SysMes) + require.Equal(t, "2016-12-09T15:22:40.656Z", resp.Groups[0].UpdatedAt.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "t7qapfhZjANMRAi5w", resp.Groups[1].ID) + require.Equal(t, "testing", resp.Groups[1].Name) + require.Equal(t, "p", resp.Groups[1].T) + require.Equal(t, 0, resp.Groups[1].Msgs) + require.Equal(t, "y65tAmHs93aDChMWu", resp.Groups[1].U.ID) + require.Equal(t, "testing2", resp.Groups[1].U.Username) + require.Equal(t, "2016-12-01T15:08:58.042Z", resp.Groups[1].Ts.Format("2006-01-02T15:04:05.000Z")) + require.False(t, resp.Groups[1].Ro) + require.True(t, resp.Groups[1].SysMes) + require.Equal(t, "2016-12-09T15:22:40.656Z", resp.Groups[1].UpdatedAt.Format("2006-01-02T15:04:05.000Z")) +} + +func TestGroupMembers(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"members":[{"_id":"Q4GkX6RMepGDdQ7YJ","status":"online","name":"Marcos Defendi","utcOffset":-3,"username":"marcos.defendi"},{"_id":"rocket.cat","name":"Rocket.Cat","username":"rocket.cat","status":"online","utcOffset":0}],"count":2,"offset":0,"total":2,"success":true}`, + })) + defer server.Close() + + req := SimpleGroupRequest{ + RoomId: "GENERAL", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.GroupMembers(&req) + require.NoError(t, err) + + require.True(t, resp.Success) + require.Equal(t, 2, resp.Count) + require.Equal(t, 0, resp.Offset) + require.Equal(t, 2, resp.Total) + require.Equal(t, 2, len(resp.Members)) + require.Equal(t, "Q4GkX6RMepGDdQ7YJ", resp.Members[0].ID) + require.Equal(t, "online", resp.Members[0].Status) + require.Equal(t, "Marcos Defendi", resp.Members[0].Name) + require.Equal(t, "marcos.defendi", resp.Members[0].Username) + require.Equal(t, "rocket.cat", resp.Members[1].ID) + require.Equal(t, "Rocket.Cat", resp.Members[1].Name) + require.Equal(t, "rocket.cat", resp.Members[1].Username) + require.Equal(t, "online", resp.Members[1].Status) +} + +func TestGroupMessages(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"messages":[{"_id":"CeXwh5eBbdrtvnqG6","rid":"agh2Sucgb54RQ8dDo","msg":"s","ts":"2018-10-05T13:48:21.616Z","u":{"_id":"KPkEYwKKBKZnEEPpt","username":"marcos.defendi","name":"Marcos Defendi"},"_updatedAt":"2018-10-05T13:48:49.535Z","reactions":{":frowning2:":{"usernames":["marcos.defendi"]}},"mentions":[],"channels":[],"starred":{"_id":"KPkEYwKKBKZnEEPpt"}},{"_id":"MrAeupRiF9TvhMesK","t":"room_changed_privacy","rid":"agh2Sucgb54RQ8dDo","ts":"2018-10-05T00:11:16.998Z","msg":"Private Group","u":{"_id":"rocketchat.internal.admin.test","username":"rocketchat.internal.admin.test"},"groupable":false,"_updatedAt":"2018-10-05T00:11:16.998Z"}],"count":2,"offset":0,"total":2,"success":true}`, + })) + defer server.Close() + + req := SimpleGroupRequest{ + RoomId: "GENERAL", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.GroupMessages(&req) + require.NoError(t, err) + + require.True(t, resp.Success) + require.Equal(t, 2, resp.Count) + require.Equal(t, 0, resp.Offset) + require.Equal(t, 2, resp.Total) + require.Equal(t, 2, len(resp.Messages)) + require.Equal(t, "CeXwh5eBbdrtvnqG6", resp.Messages[0].ID) + require.Equal(t, "agh2Sucgb54RQ8dDo", resp.Messages[0].Rid) + require.Equal(t, "s", resp.Messages[0].Msg) + require.Equal(t, "2018-10-05T13:48:21.616Z", resp.Messages[0].Ts.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "KPkEYwKKBKZnEEPpt", resp.Messages[0].U.ID) + require.Equal(t, "marcos.defendi", resp.Messages[0].U.Username) + require.Equal(t, "Marcos Defendi", resp.Messages[0].U.Name) + require.Equal(t, "2018-10-05T13:48:49.535Z", resp.Messages[0].UpdatedAt.Format("2006-01-02T15:04:05.000Z")) + // require.Equal(t, 1, len(resp.Messages[0].Reactions)) + // require.Equal(t, 0, len(resp.Messages[0].Mentions)) + // require.Equal(t, 0, len(resp.Messages[0].Channels)) + require.Equal(t, "MrAeupRiF9TvhMesK", resp.Messages[1].ID) + require.Equal(t, "agh2Sucgb54RQ8dDo", resp.Messages[1].Rid) + require.Equal(t, "2018-10-05T00:11:16.998Z", resp.Messages[1].Ts.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "rocketchat.internal.admin.test", resp.Messages[1].U.ID) + require.Equal(t, "rocketchat.internal.admin.test", resp.Messages[1].U.Username) + require.Equal(t, "Private Group", resp.Messages[1].Msg) + require.Equal(t, "2018-10-05T00:11:16.998Z", resp.Messages[1].UpdatedAt.Format("2006-01-02T15:04:05.000Z")) +} + +func TestOpenGroup(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + req := SimpleGroupId{ + RoomId: "GENERAL", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.OpenGroup(&req) + require.NoError(t, err) + + require.True(t, resp.Success) +} + +func TestRenameGroup(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"group":{"_id":"ByehQjC44FwMeiLbX","name":"new-name","t":"p","usernames":["testing1"],"msgs":4,"u":{"_id":"aobEdbYhXfu5hkeqG","username":"testing1"},"ts":"2016-12-09T15:08:58.042Z","ro":false,"sysMes":true,"_updatedAt":"2016-12-09T15:57:44.686Z"},"success":true}`, + })) + defer server.Close() + + req := RenameGroupRequest{ + RoomId: "GENERAL", + NewName: "new-name", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.RenameGroup(&req) + require.NoError(t, err) + + require.True(t, resp.Success) + require.Equal(t, "ByehQjC44FwMeiLbX", resp.Group.ID) + require.Equal(t, "new-name", resp.Group.Name) + require.Equal(t, "p", resp.Group.T) + require.Equal(t, 1, len(resp.Group.Usernames)) + require.Equal(t, "testing1", resp.Group.Usernames[0]) + require.Equal(t, 4, resp.Group.Msgs) + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.Group.U.ID) + require.Equal(t, "testing1", resp.Group.U.Username) + require.Equal(t, "2016-12-09T15:08:58.042Z", resp.Group.Ts.Format("2006-01-02T15:04:05.000Z")) + require.False(t, resp.Group.Ro) + require.True(t, resp.Group.SysMes) + require.Equal(t, "2016-12-09T15:57:44.686Z", resp.Group.UpdatedAt.Format("2006-01-02T15:04:05.000Z")) +} + +func TestAddLeaderGroup(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + req := AddGroupPermissionRequest{ + RoomId: "GENERAL", + UserId: "1234", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.AddLeaderGroup(&req) + require.NoError(t, err) + + require.True(t, resp.Success) +} + +func TestAddOwnerGroup(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + req := AddGroupPermissionRequest{ + RoomId: "GENERAL", + UserId: "1234", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.AddOwnerGroup(&req) + require.NoError(t, err) + + require.True(t, resp.Success) +} + +func TestSetAnnouncementGroup(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"announcement": "Test out everything","success":true}`, + })) + defer server.Close() + + req := SetAnnouncementRequest{ + RoomId: "GENERAL", + Announcement: "Test out everything", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.SetAnnouncementGroup(&req) + require.NoError(t, err) + + require.True(t, resp.Success) + require.Equal(t, "Test out everything", resp.Announcement) +} + +func TestSetDescriptionGroup(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"description": "Test out everything","success":true}`, + })) + defer server.Close() + + req := SetDescriptionRequest{ + RoomId: "GENERAL", + Description: "Test out everything", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.SetDescriptionGroup(&req) + require.NoError(t, err) + + require.True(t, resp.Success) + require.Equal(t, "Test out everything", resp.Description) +} + +func TestSetTopicGroup(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"topic": "Test out everything","success":true}`, + })) + defer server.Close() + + req := SetTopicRequest{ + RoomId: "GENERAL", + Topic: "Test out everything", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.SetTopicGroup(&req) + require.NoError(t, err) + + require.True(t, resp.Success) + require.Equal(t, "Test out everything", resp.Topic) +} + +func TestUnarchiveGroup(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + req := SimpleGroupId{ + RoomId: "GENERAL", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.UnarchiveGroup(&req) + require.NoError(t, err) + + require.True(t, resp.Success) +} diff --git a/helper.go b/helper.go new file mode 100644 index 0000000..5d0971b --- /dev/null +++ b/helper.go @@ -0,0 +1,26 @@ +package gorocket + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +type HandlerHelper struct { + Code int + ResponseBody string +} + +func getHandler(t *testing.T, param *HandlerHelper) http.HandlerFunc { + httpStatus := param.Code + if httpStatus == 0 { + httpStatus = http.StatusOK + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(httpStatus) + _, err := w.Write([]byte(param.ResponseBody)) + require.NoError(t, err) + }) +} diff --git a/hooks.go b/hooks.go index 0d1b549..f548522 100644 --- a/hooks.go +++ b/hooks.go @@ -34,8 +34,6 @@ func (c *Client) Hooks(msg *HookMessage, token string) (*HookResponse, error) { url, bytes.NewBuffer(opt)) - fmt.Println(url) - req.Header.Set("Accept", "application/json; charset=utf-8") req.Header.Set("Content-Type", "application/json; charset=utf-8") @@ -45,7 +43,6 @@ func (c *Client) Hooks(msg *HookMessage, token string) (*HookResponse, error) { } res, err := c.HTTPClient.Do(req) - defer res.Body.Close() if err != nil { diff --git a/hooks_test.go b/hooks_test.go new file mode 100644 index 0000000..41a407f --- /dev/null +++ b/hooks_test.go @@ -0,0 +1,34 @@ +package gorocket + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestHooks(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + msg := HookMessage{ + Text: "Hello", + Attachments: []HookAttachment{ + { + Title: "Title", + TitleLink: "http://example.com", + Text: "Text", + ImageURL: "http://example.com/image.png", + Color: "#ff0000", + }, + }, + } + + resp, err := client.Hooks(&msg, "token") + require.NoError(t, err) + + require.True(t, resp.Success) +} diff --git a/info.go b/info.go index 5faee4c..62daeef 100644 --- a/info.go +++ b/info.go @@ -7,7 +7,8 @@ import ( ) type RespInfo struct { - Info struct { + Version string `json:"version"` + Info struct { Version string `json:"version"` Build Build `json:"build"` Commit Commit `json:"commit"` @@ -37,11 +38,20 @@ type Commit struct { } type RespDirectory struct { - Result []Result `json:"result"` - Count int `json:"count"` - Offset int `json:"offset"` - Total int `json:"total"` - Success bool `json:"success"` + Result []DirectoryResult `json:"result"` + Count int `json:"count"` + Offset int `json:"offset"` + Total int `json:"total"` + Success bool `json:"success"` +} + +type DirectoryResult struct { + ID string `json:"_id"` + CreatedAt time.Time `json:"createdAt"` + Emails []Email `json:"emails"` + Name string `json:"name"` + Username string `json:"username"` + AvatarETag string `json:"avatarETag"` } type Result struct { @@ -88,6 +98,7 @@ type UsersInfo struct { Name string `json:"name"` Username string `json:"username"` StatusText string `json:"statusText"` + AvatarETag string `json:"avatarETag,omitempty"` } type RoomsInfo struct { @@ -298,6 +309,7 @@ type RespStatisticsList struct { Success bool `json:"success"` } +// Info returns information about the server func (c *Client) Info() (*RespInfo, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/info", c.baseURL), nil) if err != nil { @@ -312,6 +324,7 @@ func (c *Client) Info() (*RespInfo, error) { return &res, nil } +// Directory returns a list of channels func (c *Client) Directory() (*RespDirectory, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s/directory", c.baseURL, c.apiVersion), nil) if err != nil { @@ -326,6 +339,7 @@ func (c *Client) Directory() (*RespDirectory, error) { return &res, nil } +// Spotlight returns a list of users and rooms that match the provided query func (c *Client) Spotlight(query string) (*RespSpotlight, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s/spotlight?query=%s", c.baseURL, c.apiVersion, query), nil) @@ -341,6 +355,7 @@ func (c *Client) Spotlight(query string) (*RespSpotlight, error) { return &res, nil } +// Statistics returns statistics about the server func (c *Client) Statistics() (*RespStatistics, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s/statistics", c.baseURL, c.apiVersion), nil) if err != nil { @@ -355,6 +370,7 @@ func (c *Client) Statistics() (*RespStatistics, error) { return &res, nil } +// StatisticsList returns a list of statistics func (c *Client) StatisticsList() (*RespStatisticsList, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s/statistics.list", c.baseURL, c.apiVersion), nil) if err != nil { diff --git a/info_test.go b/info_test.go new file mode 100644 index 0000000..720b44e --- /dev/null +++ b/info_test.go @@ -0,0 +1,201 @@ +package gorocket + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestInfo(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"version":"6.5.2","info":{"version":"6.5.2","build":{"date":"2024-01-03T03:33:06.277Z","nodeVersion":"v14.21.4","arch":"x64","platform":"linux","osRelease":"5.15.0-1053-azure","totalMemory":16768364544,"freeMemory":812339200,"cpus":4},"marketplaceApiVersion":"1.41.0","commit":{"hash":"3ebc8e0868c859d2d8e636787645c29a89dea1e5","date":"Tue Jan 2 23:38:47 2024 -0300","author":"Diego Sampaio","subject":"chore: fix dep version","tag":"6.5.2","branch":"HEAD"}},"minimumClientVersions":{"desktop":"3.9.6","mobile":"4.39.0"},"supportedVersions":{"signed":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"},"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.Info() + require.NoError(t, err) + + require.Equal(t, "6.5.2", resp.Version) + require.Equal(t, "6.5.2", resp.Info.Version) + require.Equal(t, "2024-01-03T03:33:06.277Z", resp.Info.Build.Date.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "v14.21.4", resp.Info.Build.NodeVersion) + require.Equal(t, "x64", resp.Info.Build.Arch) + require.Equal(t, "linux", resp.Info.Build.Platform) + require.Equal(t, "5.15.0-1053-azure", resp.Info.Build.OsRelease) + require.Equal(t, int64(16768364544), resp.Info.Build.TotalMemory) + require.Equal(t, 812339200, resp.Info.Build.FreeMemory) + require.Equal(t, 4, resp.Info.Build.Cpus) + require.Equal(t, "1.41.0", resp.Info.MarketplaceAPIVersion) + require.Equal(t, "3ebc8e0868c859d2d8e636787645c29a89dea1e5", resp.Info.Commit.Hash) + require.Equal(t, "Tue Jan 2 23:38:47 2024 -0300", resp.Info.Commit.Date) + require.Equal(t, "Diego Sampaio", resp.Info.Commit.Author) + require.Equal(t, "chore: fix dep version", resp.Info.Commit.Subject) + require.Equal(t, "6.5.2", resp.Info.Commit.Tag) + require.Equal(t, "HEAD", resp.Info.Commit.Branch) +} + +func TestDirectory(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"result":[{"_id":"jRca8kibJx8NkLJxt","createdAt":"2018-04-13T12:46:26.517Z","emails":[{"address":"user.test.1523623548558@rocket.chat","verified":false}],"name":"EditedRealNameuser.test.1523623548558","username":"editedusernameuser.test.1523623548558","avatarETag":"6YbLtc4v9b4conXon"}],"count":1,"offset":0,"total":1,"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.Directory() + require.NoError(t, err) + + require.Equal(t, 1, resp.Count) + require.Equal(t, 0, resp.Offset) + require.Equal(t, 1, resp.Total) + require.Equal(t, "jRca8kibJx8NkLJxt", resp.Result[0].ID) + require.Equal(t, "2018-04-13T12:46:26.517Z", resp.Result[0].CreatedAt.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "user.test.1523623548558@rocket.chat", resp.Result[0].Emails[0].Address) + require.Equal(t, false, resp.Result[0].Emails[0].Verified) + require.Equal(t, "EditedRealNameuser.test.1523623548558", resp.Result[0].Name) + require.Equal(t, "editedusernameuser.test.1523623548558", resp.Result[0].Username) + require.Equal(t, "6YbLtc4v9b4conXon", resp.Result[0].AvatarETag) +} + +func TestSpotlight(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"users":[{"_id":"rocket.cat","name":"Rocket.Cat","username":"rocket.cat","status":"online","avatarETag":"5BB9B5ny5DkKdrwkq"}],"rooms":[],"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.Spotlight("rocket") + require.NoError(t, err) + + require.Equal(t, 1, len(resp.Users)) + require.Equal(t, "rocket.cat", resp.Users[0].ID) + require.Equal(t, "Rocket.Cat", resp.Users[0].Name) + require.Equal(t, "rocket.cat", resp.Users[0].Username) + require.Equal(t, "online", resp.Users[0].Status) + require.Equal(t, "5BB9B5ny5DkKdrwkq", resp.Users[0].AvatarETag) + require.Equal(t, 0, len(resp.Rooms)) +} + +func TestStatistics(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"_id":"wufRdmSrjmSMhBdTN","uniqueId":"wD4EP3M7FeFzJZgk9","installedAt":"2018-02-18T19:40:45.369Z","version":"0.61.0-develop","totalUsers":88,"activeUsers":88,"nonActiveUsers":0,"onlineUsers":0,"awayUsers":1,"offlineUsers":87,"totalRooms":81,"totalChannels":41,"totalPrivateGroups":37,"totalDirect":3,"totlalLivechat":0,"totalMessages":2408,"totalChannelMessages":730,"totalPrivateGroupMessages":1869,"totalDirectMessages":25,"totalLivechatMessages":0,"lastLogin":"2018-02-24T12:44:45.045Z","lastMessageSentAt":"2018-02-23T18:14:03.490Z","lastSeenSubscription":"2018-02-23T17:58:54.779Z","os":{"type":"Linux","platform":"linux","arch":"x64","release":"4.13.0-32-generic","uptime":76242,"loadavg":[0.0576171875,0.04638671875,0.00439453125],"totalmem":5787901952,"freemem":1151168512,"cpus":[{"model":"Intel(R) Xeon(R) CPU E5620 @ 2.40GHz","speed":2405,"times":{"user":6437000,"nice":586500,"sys":1432200,"idle":750117500,"irq":0}},{"model":"Intel(R) Xeon(R) CPU E5620 @ 2.40GHz","speed":2405,"times":{"user":7319700,"nice":268800,"sys":1823600,"idle":747642700,"irq":0}},{"model":"Intel(R) Xeon(R) CPU E5620 @ 2.40GHz","speed":2405,"times":{"user":7484600,"nice":1003500,"sys":1446000,"idle":748873400,"irq":0}},{"model":"Intel(R) Xeon(R) CPU E5620 @ 2.40GHz","speed":2405,"times":{"user":8378200,"nice":548500,"sys":1443200,"idle":747053300,"irq":0}}]},"process":{"nodeVersion":"v8.9.4","pid":11736,"uptime":16265.506},"deploy":{"method":"tar","platform":"selfinstall"},"migration":{"_id":"control","version":106,"locked":false,"lockedAt":"2018-02-23T18:13:13.948Z","buildAt":"2018-02-18T17:22:51.212Z"},"instanceCount":1,"createdAt":"2018-02-24T13:13:00.236Z","_updatedAt":"2018-02-24T13:13:00.236Z","success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.Statistics() + require.NoError(t, err) + + require.Equal(t, "wufRdmSrjmSMhBdTN", resp.ID) + require.Equal(t, "wD4EP3M7FeFzJZgk9", resp.UniqueID) + require.Equal(t, "2018-02-18T19:40:45.369Z", resp.InstalledAt.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "0.61.0-develop", resp.Version) + require.Equal(t, 88, resp.TotalUsers) + require.Equal(t, 88, resp.ActiveUsers) + require.Equal(t, 0, resp.NonActiveUsers) + require.Equal(t, 0, resp.OnlineUsers) + require.Equal(t, 1, resp.AwayUsers) + require.Equal(t, 87, resp.OfflineUsers) + require.Equal(t, 81, resp.TotalRooms) + require.Equal(t, 41, resp.TotalChannels) + require.Equal(t, 37, resp.TotalPrivateGroups) + require.Equal(t, 3, resp.TotalDirect) + require.Equal(t, 0, resp.TotalLivechat) + require.Equal(t, 2408, resp.TotalMessages) + require.Equal(t, 730, resp.TotalChannelMessages) + require.Equal(t, 1869, resp.TotalPrivateGroupMessages) + require.Equal(t, 25, resp.TotalDirectMessages) + require.Equal(t, 0, resp.TotalLivechatMessages) + require.Equal(t, "2018-02-24T12:44:45.045Z", resp.LastLogin.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "2018-02-23T18:14:03.490Z", resp.LastMessageSentAt.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "2018-02-23T17:58:54.779Z", resp.LastSeenSubscription.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "Linux", resp.Os.Type) + require.Equal(t, "linux", resp.Os.Platform) + require.Equal(t, "x64", resp.Os.Arch) + require.Equal(t, "4.13.0-32-generic", resp.Os.Release) + require.Equal(t, 76242, resp.Os.Uptime) + require.Equal(t, int64(5787901952), resp.Os.Totalmem) + require.Equal(t, 1151168512, resp.Os.Freemem) + require.Equal(t, 4, len(resp.Os.Cpus)) + require.Equal(t, "Intel(R) Xeon(R) CPU E5620 @ 2.40GHz", resp.Os.Cpus[0].Model) + require.Equal(t, 2405, resp.Os.Cpus[0].Speed) + require.Equal(t, 6437000, resp.Os.Cpus[0].Times.User) + require.Equal(t, 586500, resp.Os.Cpus[0].Times.Nice) + require.Equal(t, 1432200, resp.Os.Cpus[0].Times.Sys) + require.Equal(t, 750117500, resp.Os.Cpus[0].Times.Idle) + require.Equal(t, 0, resp.Os.Cpus[0].Times.Irq) + require.Equal(t, "Intel(R) Xeon(R) CPU E5620 @ 2.40GHz", resp.Os.Cpus[1].Model) + require.Equal(t, 2405, resp.Os.Cpus[1].Speed) + require.Equal(t, 7319700, resp.Os.Cpus[1].Times.User) + require.Equal(t, 268800, resp.Os.Cpus[1].Times.Nice) + require.Equal(t, 1823600, resp.Os.Cpus[1].Times.Sys) + require.Equal(t, 747642700, resp.Os.Cpus[1].Times.Idle) + require.Equal(t, 0, resp.Os.Cpus[1].Times.Irq) + require.Equal(t, "Intel(R) Xeon(R) CPU E5620 @ 2.40GHz", resp.Os.Cpus[2].Model) + require.Equal(t, 2405, resp.Os.Cpus[2].Speed) + require.Equal(t, 7484600, resp.Os.Cpus[2].Times.User) + require.Equal(t, 1003500, resp.Os.Cpus[2].Times.Nice) + require.Equal(t, 1446000, resp.Os.Cpus[2].Times.Sys) + require.Equal(t, 748873400, resp.Os.Cpus[2].Times.Idle) + require.Equal(t, 0, resp.Os.Cpus[2].Times.Irq) + require.Equal(t, "Intel(R) Xeon(R) CPU E5620 @ 2.40GHz", resp.Os.Cpus[3].Model) + require.Equal(t, 2405, resp.Os.Cpus[3].Speed) + require.Equal(t, 8378200, resp.Os.Cpus[3].Times.User) + require.Equal(t, 548500, resp.Os.Cpus[3].Times.Nice) + require.Equal(t, 1443200, resp.Os.Cpus[3].Times.Sys) + require.Equal(t, 747053300, resp.Os.Cpus[3].Times.Idle) + require.Equal(t, 0, resp.Os.Cpus[3].Times.Irq) + require.Equal(t, "v8.9.4", resp.Process.NodeVersion) + require.Equal(t, 11736, resp.Process.Pid) + require.Equal(t, 16265.506, resp.Process.Uptime) + require.Equal(t, "tar", resp.Deploy.Method) + require.Equal(t, "selfinstall", resp.Deploy.Platform) + require.Equal(t, "control", resp.Migration.ID) + require.Equal(t, 106, resp.Migration.Version) + require.Equal(t, false, resp.Migration.Locked) + require.Equal(t, 1, resp.InstanceCount) + require.Equal(t, "2018-02-24T13:13:00.236Z", resp.CreatedAt.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "2018-02-24T13:13:00.236Z", resp.UpdatedAt.Format("2006-01-02T15:04:05.000Z")) +} + +func TestStatisticsList(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"statistics":[{"_id":"v3D4mvobwfznKozH8","uniqueId":"wD4EP3M7FeFzJZgk9","installedAt":"2018-02-18T19:40:45.369Z","version":"0.61.0-develop","totalUsers":88,"activeUsers":88,"nonActiveUsers":0,"onlineUsers":0,"awayUsers":1,"offlineUsers":87,"totalRooms":81,"totalChannels":41,"totalPrivateGroups":37,"totalDirect":3,"totlalLivechat":0,"totalMessages":2408,"totalChannelMessages":730,"totalPrivateGroupMessages":1869,"totalDirectMessages":25,"totalLivechatMessages":0,"lastLogin":"2018-02-24T12:44:45.045Z","lastMessageSentAt":"2018-02-23T18:14:03.490Z","lastSeenSubscription":"2018-02-23T17:58:54.779Z","instanceCount":1,"createdAt":"2018-02-24T15:13:00.312Z","_updatedAt":"2018-02-24T15:13:00.312Z"}],"count":1,"offset":0,"total":1,"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.StatisticsList() + require.NoError(t, err) + + require.Equal(t, 1, resp.Count) + require.Equal(t, 0, resp.Offset) + require.Equal(t, 1, resp.Total) + require.Equal(t, "v3D4mvobwfznKozH8", resp.Statistics[0].ID) + require.Equal(t, "wD4EP3M7FeFzJZgk9", resp.Statistics[0].UniqueID) + require.Equal(t, "2018-02-18T19:40:45.369Z", resp.Statistics[0].InstalledAt.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "0.61.0-develop", resp.Statistics[0].Version) + require.Equal(t, 88, resp.Statistics[0].TotalUsers) + require.Equal(t, 88, resp.Statistics[0].ActiveUsers) + require.Equal(t, 0, resp.Statistics[0].NonActiveUsers) + require.Equal(t, 0, resp.Statistics[0].OnlineUsers) + require.Equal(t, 1, resp.Statistics[0].AwayUsers) + require.Equal(t, 87, resp.Statistics[0].OfflineUsers) + require.Equal(t, 81, resp.Statistics[0].TotalRooms) + require.Equal(t, 41, resp.Statistics[0].TotalChannels) + require.Equal(t, 37, resp.Statistics[0].TotalPrivateGroups) + require.Equal(t, 3, resp.Statistics[0].TotalDirect) + require.Equal(t, 0, resp.Statistics[0].TotalLivechat) + require.Equal(t, 2408, resp.Statistics[0].TotalMessages) + require.Equal(t, 730, resp.Statistics[0].TotalChannelMessages) + require.Equal(t, 1869, resp.Statistics[0].TotalPrivateGroupMessages) + require.Equal(t, 25, resp.Statistics[0].TotalDirectMessages) + require.Equal(t, 0, resp.Statistics[0].TotalLivechatMessages) + require.Equal(t, "2018-02-24T12:44:45.045Z", resp.Statistics[0].LastLogin.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "2018-02-23T18:14:03.490Z", resp.Statistics[0].LastMessageSentAt.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "2018-02-23T17:58:54.779Z", resp.Statistics[0].LastSeenSubscription.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, 1, resp.Statistics[0].InstanceCount) + require.Equal(t, "2018-02-24T15:13:00.312Z", resp.Statistics[0].CreatedAt.Format("2006-01-02T15:04:05.000Z")) + require.Equal(t, "2018-02-24T15:13:00.312Z", resp.Statistics[0].UpdatedAt.Format("2006-01-02T15:04:05.000Z")) +} diff --git a/users.go b/users.go index 01323f7..dedcd65 100644 --- a/users.go +++ b/users.go @@ -114,19 +114,20 @@ type GetStatusResponse struct { } type UsersInfoResponse struct { - User singleUserInfo `json:"user"` + User SingleUserInfo `json:"user"` Success bool `json:"success"` } -type singleUserInfo struct { - ID string `json:"_id"` - Type string `json:"type"` - Status string `json:"status"` - Active bool `json:"active"` - Name string `json:"name"` - UtcOffset float64 `json:"utcOffset"` - Username string `json:"username"` - AvatarETag string `json:"avatarETag,omitempty"` +type SingleUserInfo struct { + ID string `json:"_id"` + CreatedAt time.Time `json:"createdAt"` + Type string `json:"type"` + Status string `json:"status"` + Active bool `json:"active"` + Name string `json:"name"` + UtcOffset float64 `json:"utcOffset"` + Username string `json:"username"` + AvatarETag string `json:"avatarETag,omitempty"` } type UserRegisterRequest struct { @@ -182,7 +183,7 @@ type userUpdateInfo struct { Name string `json:"name"` } -// Gets all connected users presence +// UsersPresence gets all connected users presence func (c *Client) UsersPresence(query string) (*UsersPresenceResponse, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s/users.presence?from=%s", c.baseURL, c.apiVersion, query), nil) @@ -198,7 +199,7 @@ func (c *Client) UsersPresence(query string) (*UsersPresenceResponse, error) { return &res, nil } -// Create a new user. Requires create-user permission. +// UsersCreate creates a new user. func (c *Client) UsersCreate(user *NewUser) (*UserCreateResponse, error) { opt, _ := json.Marshal(user) @@ -219,7 +220,7 @@ func (c *Client) UsersCreate(user *NewUser) (*UserCreateResponse, error) { return &res, nil } -// Deletes an existing user. Requires delete-user permission. +// UsersDelete deletes a user. func (c *Client) UsersDelete(user *UsersDelete) (*SimpleSuccessResponse, error) { opt, _ := json.Marshal(user) @@ -240,7 +241,7 @@ func (c *Client) UsersDelete(user *UsersDelete) (*SimpleSuccessResponse, error) return &res, nil } -// Create a user authentication token. +// UsersCreateToken creates a user token. func (c *Client) UsersCreateToken(user *SimpleUserRequest) (*CreateTokenResponse, error) { opt, _ := json.Marshal(user) @@ -261,12 +262,12 @@ func (c *Client) UsersCreateToken(user *SimpleUserRequest) (*CreateTokenResponse return &res, nil } -// Deactivate Idle users. Requires edit-other-user-active-status permission. +// UsersDeactivateIdle deactivates idle users. func (c *Client) UsersDeactivateIdle(params *DeactivateRequest) (*DeactivateResponse, error) { opt, _ := json.Marshal(params) req, err := http.NewRequest("POST", - fmt.Sprintf("%s/%s/users.createToken", c.baseURL, c.apiVersion), + fmt.Sprintf("%s/%s/users.deactivateIdle", c.baseURL, c.apiVersion), bytes.NewBuffer(opt)) if err != nil { @@ -282,14 +283,14 @@ func (c *Client) UsersDeactivateIdle(params *DeactivateRequest) (*DeactivateResp return &res, nil } -// Deletes your own user. Requires Allow Users to Delete Own Account enabled. Accessible from Administration -> Accounts. +// UsersDeleteOwnAccount deletes your own account. func (c *Client) UsersDeleteOwnAccount(pass string) (*SimpleSuccessResponse, error) { param := struct { - password string `json:"password"` + Password string `json:"password"` }{} - param.password = fmt.Sprintf("%x", sha256.Sum256([]byte(pass))) + param.Password = fmt.Sprintf("%x", sha256.Sum256([]byte(pass))) opt, _ := json.Marshal(param) @@ -310,12 +311,12 @@ func (c *Client) UsersDeleteOwnAccount(pass string) (*SimpleSuccessResponse, err return &res, nil } -// Send email to reset your password. +// UsersForgotPassword send an email to reset your password func (c *Client) UsersForgotPassword(email string) (*SimpleSuccessResponse, error) { param := struct { - email string `json:"email"` + Email string `json:"email"` }{ - email: email, + Email: email, } opt, _ := json.Marshal(param) @@ -337,7 +338,7 @@ func (c *Client) UsersForgotPassword(email string) (*SimpleSuccessResponse, erro return &res, nil } -// Generate Personal Access Token. Requires create-personal-access-tokens permission. +// UsersGeneratePersonalAccessToken generates a personal access token func (c *Client) UsersGeneratePersonalAccessToken(params *GetNewToken) (*NewTokenResponse, error) { opt, _ := json.Marshal(params) @@ -358,12 +359,12 @@ func (c *Client) UsersGeneratePersonalAccessToken(params *GetNewToken) (*NewToke return &res, nil } -// Gets a user's Status if the query string userId or username is provided, otherwise it gets the callee's. +// UsersGetStatus gets the status of a user func (c *Client) UsersGetStatus(user *SimpleUserRequest) (*GetStatusResponse, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s/users.getStatus", c.baseURL, c.apiVersion), nil) if user.Username == "" && user.UserId == "" { - return nil, fmt.Errorf("False parameters") + return nil, fmt.Errorf("false parameters") } url := req.URL.Query() @@ -387,7 +388,7 @@ func (c *Client) UsersGetStatus(user *SimpleUserRequest) (*GetStatusResponse, er return &res, nil } -// Retrieves information about a user, the result is only limited to what the callee has access to view. +// UsersInfo gets the information of a user func (c *Client) UsersInfo(user *SimpleUserRequest) (*UsersInfoResponse, error) { opt, _ := json.Marshal(user) @@ -396,7 +397,7 @@ func (c *Client) UsersInfo(user *SimpleUserRequest) (*UsersInfoResponse, error) bytes.NewBuffer(opt)) if user.Username == "" && user.UserId == "" { - return nil, fmt.Errorf("False parameters") + return nil, fmt.Errorf("false parameters") } url := req.URL.Query() @@ -421,7 +422,7 @@ func (c *Client) UsersInfo(user *SimpleUserRequest) (*UsersInfoResponse, error) return &res, nil } -// Register a new user. +// UsersRegister registers a new user func (c *Client) UsersRegister(user *UserRegisterRequest) (*UsersInfoResponse, error) { opt, _ := json.Marshal(user) @@ -442,7 +443,7 @@ func (c *Client) UsersRegister(user *UserRegisterRequest) (*UsersInfoResponse, e return &res, nil } -// Sets a user Status when the status message and state is given. +// UsersSetStatus sets the status of a user func (c *Client) UsersSetStatus(status *SetStatus) (*SimpleSuccessResponse, error) { opt, _ := json.Marshal(status) @@ -463,7 +464,7 @@ func (c *Client) UsersSetStatus(status *SetStatus) (*SimpleSuccessResponse, erro return &res, nil } -// Update an existing user. +// UsersUpdate updates a user func (c *Client) UsersUpdate(user *UserUpdateRequest) (*UserUpdateResponse, error) { opt, _ := json.Marshal(user) diff --git a/users_test.go b/users_test.go new file mode 100644 index 0000000..768e515 --- /dev/null +++ b/users_test.go @@ -0,0 +1,279 @@ +package gorocket + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUsersPresence(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"users":[{"_id":"rocket.cat","name":"Rocket.Cat","username":"rocket.cat","status":"online","utcOffset":0,"avatarETag":"5BB9B5ny5DkKdrwkq"},{"_id":"rocketchat.internal.admin.test","name":"RocketChat Internal Admin Test","username":"rocketchat.internal.admin.test","status":"online","utcOffset":-2,"avatarETag":"iEbEm4bTT327NJjXt"}],"full":true,"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.UsersPresence("rocket") + require.NoError(t, err) + + require.Equal(t, 2, len(resp.Users)) + require.Equal(t, "rocket.cat", resp.Users[0].ID) + require.Equal(t, "Rocket.Cat", resp.Users[0].Name) + require.Equal(t, "rocket.cat", resp.Users[0].Username) + require.Equal(t, "online", resp.Users[0].Status) + require.Equal(t, 0.0, resp.Users[0].UtcOffset) + require.Equal(t, "5BB9B5ny5DkKdrwkq", resp.Users[0].AvatarETag) + require.Equal(t, "rocketchat.internal.admin.test", resp.Users[1].ID) + require.Equal(t, "RocketChat Internal Admin Test", resp.Users[1].Name) + require.Equal(t, "rocketchat.internal.admin.test", resp.Users[1].Username) + require.Equal(t, "online", resp.Users[1].Status) + require.Equal(t, -2.0, resp.Users[1].UtcOffset) + require.Equal(t, "iEbEm4bTT327NJjXt", resp.Users[1].AvatarETag) + require.True(t, resp.Full) + require.True(t, resp.Success) +} + +func TestUsersCreate(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"user":{"_id":"BsNr28znDkG8aeo7W","createdAt":"2016-09-13T14:57:56.037Z","services":{"password":{"bcrypt":"$2a$i7BFS55uFYRf5TE4ErSUH8HymMNAbpMAvsOcl2C"}},"username":"uniqueusername","emails":[{"address":"email@user.tld","verified":false}],"type":"user","status":"offline","active":true,"roles":["user"],"_updatedAt":"2016-09-13T14:57:56.175Z","name":"name","settings":{}},"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + payload := NewUser{ + Email: "email@user.tld", + Name: "name", + Password: "password", + Username: "uniqueusername", + } + + resp, err := client.UsersCreate(&payload) + require.NoError(t, err) + + require.Equal(t, "BsNr28znDkG8aeo7W", resp.User.ID) + require.Equal(t, "2016-09-13T14:57:56.037Z", resp.User.CreatedAt.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "2016-09-13T14:57:56.175Z", resp.User.UpdatedAt.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "uniqueusername", resp.User.Username) + require.Equal(t, "email@user.tld", resp.User.Emails[0].Address) + require.Equal(t, false, resp.User.Emails[0].Verified) + require.Equal(t, "user", resp.User.Type) + require.Equal(t, "offline", resp.User.Status) + require.Equal(t, true, resp.User.Active) + require.Equal(t, "user", resp.User.Roles[0]) + require.Equal(t, "name", resp.User.Name) + require.True(t, resp.Success) +} + +func TestUsersDelete(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + req := UsersDelete{ + Username: "blabla", + } + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.UsersDelete(&req) + require.NoError(t, err) + + require.True(t, resp.Success) +} + +func TestUsersCreateToken(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"data":{"authToken":"9HqLlyZOugoStsXCUfD_0YdwnNnunAJF8V47U3QHXSq","userId":"aobEdbYhXfu5hkeqG"},"success":true}`, + })) + defer server.Close() + + req := SimpleUserRequest{ + Username: "username", + } + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.UsersCreateToken(&req) + require.NoError(t, err) + + require.Equal(t, "9HqLlyZOugoStsXCUfD_0YdwnNnunAJF8V47U3QHXSq", resp.Data.AuthToken) + require.Equal(t, "aobEdbYhXfu5hkeqG", resp.Data.UserID) + require.True(t, resp.Success) +} + +func TestUsersDeactivateIdle(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"count": 1,"success":true}`, + })) + defer server.Close() + + req := DeactivateRequest{ + DaysIdle: "2", + } + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.UsersDeactivateIdle(&req) + require.NoError(t, err) + + require.True(t, resp.Success) + require.Equal(t, 1, resp.Count) +} + +func TestUsersDeleteOwnAccount(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.UsersDeleteOwnAccount("password") + require.NoError(t, err) + + require.True(t, resp.Success) +} + +func TestUsersForgotPassword(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.UsersForgotPassword("rocket@cat.com") + + require.NoError(t, err) + require.True(t, resp.Success) +} + +func TestUsersGeneratePersonalAccessToken(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"token":"2jdk99wuSjXPO201XlAks9sjDjAhSJmskAKW301mSuj9Sk","success":true}`, + })) + defer server.Close() + + req := GetNewToken{ + Token: "test", + } + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.UsersGeneratePersonalAccessToken(&req) + + require.NoError(t, err) + require.Equal(t, "2jdk99wuSjXPO201XlAks9sjDjAhSJmskAKW301mSuj9Sk", resp.Token) + require.True(t, resp.Success) +} + +func TestUsersGetStatus(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"message":"Latest status","connectionStatus":"online","status":"online","success":true}`, + })) + defer server.Close() + + req := SimpleUserRequest{ + Username: "rocket.cat", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.UsersGetStatus(&req) + + require.NoError(t, err) + require.Equal(t, "Latest status", resp.Message) + require.Equal(t, "online", resp.ConnectionStatus) + require.Equal(t, "online", resp.Status) + require.True(t, resp.Success) +} + +func TestUsersInfo(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"user":{"_id":"5fRTXMt7DMJbpPJfh","createdAt":"2023-07-10T16:44:58.548Z","services":{"password":true,"email2fa":{"enabled":true,"changedAt":"2023-07-10T16:44:58.546Z"},"resume":{"loginTokens":[{"when":"2023-10-05T18:55:02.996Z","hashedToken":"..."},{"when":"2023-10-05T19:09:30.415Z","hashedToken":"....."},{"when":"2023-10-10T23:40:46.098Z","hashedToken":"...."}]}},"username":"test.john","emails":[{"address":"test.john@test.com","verified":true}],"type":"user","status":"offline","active":true,"roles":["user","admin"],"name":"Test John","requirePasswordChange":false,"lastLogin":"2023-10-10T23:40:46.093Z","statusConnection":"offline","utcOffset":1,"statusText":"","avatarETag":"GFoEi6wv3uAxnzDcD","nickname":"tesuser2","canViewAllInfo":true},"success":true}`, + })) + defer server.Close() + + req := SimpleUserRequest{ + Username: "test.john", + } + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.UsersInfo(&req) + + require.NoError(t, err) + require.Equal(t, "5fRTXMt7DMJbpPJfh", resp.User.ID) + require.Equal(t, "2023-07-10T16:44:58.548Z", resp.User.CreatedAt.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "test.john", resp.User.Username) + require.Equal(t, "user", resp.User.Type) + require.Equal(t, "offline", resp.User.Status) + require.Equal(t, true, resp.User.Active) + require.Equal(t, "Test John", resp.User.Name) + require.Equal(t, 1.0, resp.User.UtcOffset) + require.Equal(t, "GFoEi6wv3uAxnzDcD", resp.User.AvatarETag) + require.True(t, resp.Success) +} + +func TestUsersRegister(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"user":{"_id":"nSYqWzZ4GsKTX4dyK","type":"user","status":"offline","active":true,"name":"Example User","utcOffset":0,"username":"example"},"success":true}`, + })) + defer server.Close() + + req := UserRegisterRequest{ + Email: "rocket@chat.com", + Name: "Example User", + Pass: "password", + Username: "example", + } + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.UsersRegister(&req) + + require.NoError(t, err) + require.Equal(t, "nSYqWzZ4GsKTX4dyK", resp.User.ID) + require.Equal(t, "user", resp.User.Type) + require.Equal(t, "offline", resp.User.Status) + require.Equal(t, true, resp.User.Active) + require.Equal(t, "Example User", resp.User.Name) + require.Equal(t, 0.0, resp.User.UtcOffset) + require.Equal(t, "example", resp.User.Username) + require.True(t, resp.Success) +} + +func TestUsersSetStatus(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"success":true}`, + })) + defer server.Close() + + req := SetStatus{ + Message: "I am online", + Status: "online", + } + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.UsersSetStatus(&req) + + require.NoError(t, err) + require.True(t, resp.Success) +} + +func TestUsersUpdate(t *testing.T) { + server := httptest.NewServer(getHandler(t, &HandlerHelper{ + ResponseBody: `{"user":{"_id":"BsNr28znDkG8aeo7W","createdAt":"2016-09-13T14:57:56.037Z","services":{"password":{"bcrypt":"$2a$10$5I5nUzqNEs8jKhi7BFS55uFYRf5TE4ErSUH8HymMNAbpMAvsOcl2C"}},"username":"uniqueusername","emails":[{"address":"newemail@user.tld","verified":false}],"type":"user","status":"offline","active":true,"roles":["user"],"_updatedAt":"2016-09-13T14:57:56.175Z","name":"new name","customFields":{"twitter":"userstwitter"}},"success":true}`, + })) + defer server.Close() + + req := UserUpdateRequest{ + UserId: "BsNr28znDkG8aeo7W", + Data: UserUpdateData{Name: "new name"}, + } + + client := NewTestClientWithCustomHandler(t, server) + resp, err := client.UsersUpdate(&req) + + require.NoError(t, err) + require.Equal(t, "BsNr28znDkG8aeo7W", resp.User.ID) + require.Equal(t, "2016-09-13T14:57:56.037Z", resp.User.CreatedAt.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "2016-09-13T14:57:56.175Z", resp.User.UpdatedAt.Format("2006-01-02T15:04:05.999Z")) + require.Equal(t, "uniqueusername", resp.User.Username) + require.Equal(t, "newemail@user.tld", resp.User.Emails[0].Address) + require.Equal(t, false, resp.User.Emails[0].Verified) + require.Equal(t, "user", resp.User.Roles[0]) + require.Equal(t, "new name", resp.User.Name) + require.True(t, resp.Success) +}