Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func (c Client) String() string {
/*/// Handle parsing of nested interface type response ///*/
type jsonResponse struct {
Response response `json:"response"`
Meta `json:"meta"`
Meta Meta `json:"meta"`
}

func newJSONResponse(i interface{}) *jsonResponse {
Expand Down
28 changes: 28 additions & 0 deletions json.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package groupme
import (
"encoding/json"
"fmt"
"sort"
)

// Meta is the error type returned in the GroupMe response.
Expand All @@ -13,6 +14,33 @@ type Meta struct {
Errors []string `json:"errors,omitempty"`
}

func (m *Meta) UnmarshalJSON(data []byte) error {
var raw struct {
Code HTTPStatusCode `json:"code"`
Errors json.RawMessage `json:"errors"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
m.Code, m.Errors = raw.Code, nil
if len(raw.Errors) == 0 {
return nil
}
if err := json.Unmarshal(raw.Errors, &m.Errors); err == nil {
return nil
}
// Poll validation errors are keyed by the rejected field.
var fields map[string]string
if err := json.Unmarshal(raw.Errors, &fields); err != nil {
return err
}
for field, reason := range fields {
m.Errors = append(m.Errors, field+": "+reason)
}
sort.Strings(m.Errors)
return nil
}

// Error returns the code and the error list as a string.
// Satisfies the error interface
func (m Meta) Error() string {
Expand Down
34 changes: 33 additions & 1 deletion poll_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type PollDetail struct {
LastModified Timestamp `json:"last_modified,omitempty"`
// Type is "single" or "multi".
Type string `json:"type,omitempty"`
// Visibility has only been observed as "anonymous".
// Visibility is "public" or "anonymous".
Visibility string `json:"visibility,omitempty"`
// MyVotes is the authenticated user's selection from the poll envelope.
MyVotes []string `json:"-"`
Expand All @@ -48,6 +48,38 @@ type getPollResponse struct {
} `json:"poll"`
}

type CreatePollRequest struct {
Subject string `json:"subject"`
Options []PollOption `json:"options"`
Expiration Timestamp `json:"expiration"`
Visibility string `json:"visibility"`
Type string `json:"type"`
}

// CreatePoll returns the poll and the ID of its poll.created announcement.
func (c *Client) CreatePoll(ctx context.Context, conversationID string, poll *CreatePollRequest) (*PollDetail, ID, error) {
body, err := json.Marshal(poll)
if err != nil {
return nil, "", err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpointBase+"/poll/"+url.PathEscape(conversationID), bytes.NewReader(body))
if err != nil {
return nil, "", err
}
var resp struct {
getPollResponse
Message Message `json:"message"`
}
if err := c.doWithAuthToken(ctx, req, &resp); err != nil {
return nil, "", err
}
if resp.Poll.Data.ID == "" || resp.Message.ID == "" {
return nil, "", fmt.Errorf("GroupMe poll creation response has a missing poll or message ID")
}
result, err := resp.result(conversationID, resp.Poll.Data.ID)
return result, resp.Message.ID, err
}

func (c *Client) GetPoll(ctx context.Context, conversationID, pollID string) (*PollDetail, error) {
url := fmt.Sprintf(c.endpointBase+pollEndpoint, url.PathEscape(conversationID), url.PathEscape(pollID))

Expand Down
50 changes: 50 additions & 0 deletions poll_api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package groupme

import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"reflect"
"testing"
)

func TestCreatePoll(t *testing.T) {
for _, tc := range []struct {
name, response string
status int
reason string
}{
{"created", `{"meta":{"code":20100},"response":{"poll":{"data":{"id":"poll-1","conversation_id":"88","options":[{"id":"7","title":"Yes"},{"id":"9","title":"No"}]}},"message":{"id":"announcement-1"}}}`, 201, ""},
{"invalid expiration", `{"meta":{"code":40002,"errors":["Poll expiration is invalid"]}}`, 400, "Poll expiration is invalid"},
{"invalid options", `{"meta":{"code":40003,"errors":{"options":"the length must be between 2 and 30"}}}`, 400, "options: the length must be between 2 and 30"},
} {
t.Run(tc.name, func(t *testing.T) {
input := &CreatePollRequest{Subject: "Choose", Options: []PollOption{{Title: "Yes"}, {Title: "No"}}, Expiration: 1794414187, Visibility: "public", Type: "single"}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" || r.URL.Path != "/poll/88" || r.Header.Get("X-Access-Token") != "test-token" || r.Header.Get("Content-Type") != "application/json" {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var got CreatePollRequest
if err := json.NewDecoder(r.Body).Decode(&got); err != nil || !reflect.DeepEqual(&got, input) {
t.Errorf("unexpected request body: %+v, %v", got, err)
}
w.WriteHeader(tc.status)
_, _ = w.Write([]byte(tc.response))
}))
defer server.Close()
client := NewClient("test-token")
client.endpointBase = server.URL
poll, messageID, err := client.CreatePoll(context.Background(), "88", input)
if tc.reason != "" {
var meta *Meta
if !errors.As(err, &meta) || meta.Code != 400 || !reflect.DeepEqual(meta.Errors, []string{tc.reason}) {
t.Fatalf("unexpected rejection: %v", err)
}
} else if err != nil || messageID != "announcement-1" || poll.ID != "poll-1" || poll.Options[0].ID != "7" || poll.Options[1].ID != "9" {
t.Fatalf("unexpected create result: %+v, %s, %v", poll, messageID, err)
}
})
}
}
Loading