Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rukenshia/trello Merge #92

Merged
merged 14 commits into from Mar 28, 2022
2 changes: 0 additions & 2 deletions TODO.txt

This file was deleted.

7 changes: 7 additions & 0 deletions card.go
Expand Up @@ -43,6 +43,7 @@ type Card struct {
ShortURL string `json:"shortUrl"`
URL string `json:"url"`
Desc string `json:"desc"`
Start *time.Time `json:"start"`
Due *time.Time `json:"due"`
DueComplete bool `json:"dueComplete"`
Closed bool `json:"closed"`
Expand Down Expand Up @@ -275,6 +276,9 @@ func (c *Client) CreateCard(card *Card, extraArgs ...Arguments) error {
if card.Due != nil {
args["due"] = card.Due.Format(time.RFC3339)
}
if card.Start != nil {
args["start"] = card.Start.Format(time.RFC3339)
}

args.flatten(extraArgs)
err := c.Post(path, args, &card)
Expand All @@ -296,6 +300,9 @@ func (l *List) AddCard(card *Card, extraArgs ...Arguments) error {
if card.Due != nil {
args["due"] = card.Due.Format(time.RFC3339)
}
if card.Start != nil {
args["start"] = card.Start.Format(time.RFC3339)
}

args.flatten(extraArgs)

Expand Down
83 changes: 55 additions & 28 deletions card_test.go
Expand Up @@ -6,6 +6,7 @@
package trello

import (
"net/http"
"testing"
"time"
)
Expand All @@ -25,10 +26,10 @@ func TestCardCreatedAt(t *testing.T) {
func TestGetCardsOnBoard(t *testing.T) {
board := testBoard(t)

server := mockDynamicPathResponse()
server := NewMockResponder(t)
defer server.Close()

board.client.BaseURL = server.URL
board.client.BaseURL = server.URL()
cards, err := board.GetCards(Defaults())
if err != nil {
t.Fatal(err)
Expand All @@ -41,9 +42,9 @@ func TestGetCardsOnBoard(t *testing.T) {
func TestGetCardsInList(t *testing.T) {
list := testList(t)

server := mockResponse("cards", "list-cards-api-example.json")
server := NewMockResponder(t, "cards", "list-cards-api-example.json")
defer server.Close()
list.client.BaseURL = server.URL
list.client.BaseURL = server.URL()

cards, err := list.GetCards(Defaults())
if err != nil {
Expand All @@ -57,9 +58,9 @@ func TestGetCardsInList(t *testing.T) {
func TestCardsCustomFields(t *testing.T) {
list := testList(t)

server := mockResponse("cards", "list-cards-api-example.json")
server := NewMockResponder(t, "cards", "list-cards-api-example.json")
defer server.Close()
list.client.BaseURL = server.URL
list.client.BaseURL = server.URL()

cards, err := list.GetCards(Defaults())
if err != nil {
Expand Down Expand Up @@ -94,10 +95,10 @@ func TestCardsCustomFields(t *testing.T) {
func TestBoardContainsCopyOfCard(t *testing.T) {
board := testBoard(t)

server := mockResponse("actions", "board-actions-copyCard.json")
server := NewMockResponder(t, "actions", "board-actions-copyCard.json")
defer server.Close()

board.client.BaseURL = server.URL
board.client.BaseURL = server.URL()
firstResult, err := board.ContainsCopyOfCard("57f50c552b96e3fffe588aad", Defaults())
if err != nil {
t.Error(err)
Expand All @@ -117,16 +118,29 @@ func TestBoardContainsCopyOfCard(t *testing.T) {

func TestCreateCard(t *testing.T) {
c := testClient()
server := mockResponse("cards", "card-create.json")
server := NewMockResponder(t, "cards", "card-create.json")
defer server.Close()

c.BaseURL = server.URL
server.AssertRequest(func(t *testing.T, r *http.Request) {
due := r.URL.Query().Get("due")
if _, err := time.Parse(time.RFC3339, due); err != nil {
t.Errorf("Expected due to be in RFC3339 format, but value was '%v'", due)
}

start := r.URL.Query().Get("start")
if _, err := time.Parse(time.RFC3339, start); err != nil {
t.Errorf("Expected start to be in RFC3339 format, but value was '%v'", start)
}
})

c.BaseURL = server.URL()
dueDate := time.Now().AddDate(0, 0, 3)
startDate := time.Now().AddDate(0, 0, 2)

card := Card{
Name: "Test Card Create",
Desc: "What its about",
Due: &dueDate,
Start: &startDate,
IDList: "57f03a06b5ff33a63c8be316",
IDLabels: []string{"label1", "label2"},
}
Expand Down Expand Up @@ -156,15 +170,28 @@ func TestCreateCard(t *testing.T) {
func TestAddCardToList(t *testing.T) {
l := testList(t)

server := mockResponse("cards", "card-posted-to-bottom-of-list.json")
server := NewMockResponder(t, "cards", "card-posted-to-bottom-of-list.json")
server.AssertRequest(func(t *testing.T, r *http.Request) {
due := r.URL.Query().Get("due")
if _, err := time.Parse(time.RFC3339, due); err != nil {
t.Errorf("Expected due to be in RFC3339 format, but value was '%v'", due)
}

start := r.URL.Query().Get("start")
if _, err := time.Parse(time.RFC3339, start); err != nil {
t.Errorf("Expected start to be in RFC3339 format, but value was '%v'", start)
}
})
defer server.Close()
l.client.BaseURL = server.URL
dueDate := time.Now().AddDate(0, 0, 1)
l.client.BaseURL = server.URL()
dueDate := time.Now().AddDate(0, 0, 2)
startDate := time.Now().AddDate(0, 0, 1)

card := Card{
Name: "Test Card POSTed to List",
Desc: "This is its description.",
Due: &dueDate,
Start: &startDate,
IDLabels: []string{"label1", "label2"},
}

Expand Down Expand Up @@ -193,16 +220,16 @@ func TestAddCardToList(t *testing.T) {
func TestArchiveUnarchive(t *testing.T) {
c := testCard(t)

server := mockResponse("cards", "card-archived.json")
c.client.BaseURL = server.URL
server := NewMockResponder(t, "cards", "card-archived.json")
c.client.BaseURL = server.URL()
c.Archive()
if c.Closed == false {
t.Errorf("Card should have been archived.")
}
server.Close()

server = mockResponse("cards", "card-unarchived.json")
c.client.BaseURL = server.URL
server = NewMockResponder(t, "cards", "card-unarchived.json")
c.client.BaseURL = server.URL()
c.Unarchive()
if c.Closed == true {
t.Errorf("Card should have been unarchived.")
Expand All @@ -213,9 +240,9 @@ func TestArchiveUnarchive(t *testing.T) {
func TestCopyCardToList(t *testing.T) {
c := testCard(t)

server := mockResponse("cards", "card-copied.json")
server := NewMockResponder(t, "cards", "card-copied.json")
defer server.Close()
c.client.BaseURL = server.URL
c.client.BaseURL = server.URL()

newCard, err := c.CopyToList("57f03a022cd45c863ca581f1", Defaults())
if err != nil {
Expand All @@ -234,9 +261,9 @@ func TestCopyCardToList(t *testing.T) {
func TestGetParentCard(t *testing.T) {
c := testCard(t)

server := mockDynamicPathResponse()
server := NewMockResponder(t)
defer server.Close()
c.client.BaseURL = server.URL
c.client.BaseURL = server.URL()

parent, err := c.GetParentCard(Defaults())
if err != nil {
Expand Down Expand Up @@ -265,10 +292,10 @@ func TestGetAncestorCards(t *testing.T) {

func TestAddMemberIdToCard(t *testing.T) {
c := testCard(t)
server := mockResponse("cards", "card-add-member-response.json")
server := NewMockResponder(t, "cards", "card-add-member-response.json")
defer server.Close()

c.client.BaseURL = server.URL
c.client.BaseURL = server.URL()
member, err := c.AddMemberID("testmemberid")
if err != nil {
t.Error(err)
Expand All @@ -283,10 +310,10 @@ func TestAddMemberIdToCard(t *testing.T) {

func TestAddURLAttachmentToCard(t *testing.T) {
c := testCard(t)
server := mockResponse("cards", "url-attachments.json")
server := NewMockResponder(t, "cards", "url-attachments.json")
defer server.Close()

c.client.BaseURL = server.URL
c.client.BaseURL = server.URL()
attachment := Attachment{
Name: "Test",
URL: "https://github.com/test",
Expand All @@ -313,10 +340,10 @@ func TestCardSetClient(t *testing.T) {
//
func testCard(t *testing.T) *Card {
c := testClient()
server := mockResponse("cards", "card-api-example.json")
server := NewMockResponder(t, "cards", "card-api-example.json")
defer server.Close()

c.BaseURL = server.URL
c.BaseURL = server.URL()
card, err := c.GetCard("4eea503", Defaults())
if err != nil {
t.Fatal(err)
Expand Down
153 changes: 153 additions & 0 deletions mock-responder_test.go
@@ -0,0 +1,153 @@
package trello

import (
"crypto/md5"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)

// MockResponder is a thin wrapper around the httptest.Server. It adds the
// ability to specify a file or directory of files to use as mock responses,
// and provides the AsertRequest method to add test assertions that requests
// are being made correctly. Just like with httptest.Server, the caller should
// defer a call to .Close() to shutdown the server when all requests complete.
// MockResponders should be created via the NewMockResponder() constructor.
//
type MockResponder interface {
Close()
URL() string
AssertRequest(func(t *testing.T, r *http.Request))
}

type mockResponder struct {
t *testing.T

// server will be nil until .URL() is called the first time
server *httptest.Server

// requestAssertions is a list of functions which is called on
// each HTTP request before finding and returning the mock response content.
// They should be used to make assertions on the contents of the HTTP
// request being made against this MockResponder
requestAssertions []func(t *testing.T, r *http.Request)

// mockPath holds the results of filepath.Join on the provided path parts.
// The constructor verifies the existence of the path, so this will always
// hold a valid path to either a mock file, or a directory of many mocks
mockPath string

// useDynamicPaths is set to true when mockPath is a directory. It triggers
// code which determies the mock file from the path of incoming HTTP
// requests
useDynamicPaths bool
}

// NewMockResponder creates a new MockResponder instance around the provided
// test case. The mockPath is the relative filesystem path under ./testdata/
// where the mock response JSON can be found.
//
// If mockPath describes the path to a *file*, then that file
// will be used for ALL requests. If the path is a directory, then the mock
// response will be built dynamically from the path of the request (e.g.
// GET /subdir/folder/file will return the file at subdir/folder/file.json,
// assuming it exists). This latter mode is described as "dynamic paths". When
// requests arrive with querystring arguments, the dynamic path builder will
// compute an MD5 hash of the arguments and include that as a suffix of the
// mock file path.
//
// If no mockPath is provided, then the MockResponder will run in dynamic path
// mode from the root of the testdata/ directory.
//
// The caller is expected to defer a call .Close() after NewMockResponder().
//
func NewMockResponder(t *testing.T, mockPath ...string) MockResponder {
r := &mockResponder{t: t}

// Verify a valid path was provided
r.mockPath = filepath.Join(append([]string{".", "testdata"}, mockPath...)...)
fi, err := os.Stat(r.mockPath)
if err != nil {
log.Fatalf("invalid mock path %v: %s", mockPath, err)
}

// If the provided mockPath points to a directory, then
// we'll figure out the ultimate path dynamically as requests occur.
r.useDynamicPaths = fi.IsDir()

return r
}

// AssertRequest adds a new function to be run on each HTTP request the mock
// responder recveives. Its intended use is to make test assertions on the
// content of the request
func (mr *mockResponder) AssertRequest(ra func(t *testing.T, r *http.Request)) {
mr.requestAssertions = append(mr.requestAssertions, ra)
}

// Close wraps the *httptest.Server's Close method
func (mr *mockResponder) Close() {
if mr.server != nil {
mr.server.Close()
}
}

// URL is equivalent to the *httptest.Server property of the same name, but it
// is responsible for *creating* the *httptest.Server. This function should
// be called after all customization (including calls to AssertRequest) is
// complete.
//
func (mr *mockResponder) URL() string {
if mr.server != nil {
mr.t.Error("URL() should only be called once, after completing configuration")
}
mr.server = httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
for _, assertion := range mr.requestAssertions {
assertion(mr.t, r)
}
mr.mockHandler(rw, r)
}))
return mr.server.URL
}

// mockHandler is the http.HandlerFunc for the httptest.Server inside the
// mockResponder. When the mockPath points to a single file, it simply returns
// that file in the HTTP response. Otherwise it dynamically determines the
// path of the mock file to use and returns that if the file is found...
// otherwise it responds with an error instructing the user where to put their
// mock file.
//
func (mr *mockResponder) mockHandler(rw http.ResponseWriter, r *http.Request) {
var filename string
if mr.useDynamicPaths {
parts := []string{mr.mockPath}
parts = append(parts, strings.Split(strings.TrimPrefix(r.URL.Path, "/"), "/")...)
queryStringPart := strings.Replace(r.URL.RawQuery, "key=user&token=pass", "", -1)
if queryStringPart != "" {
parts[len(parts)-1] = fmt.Sprintf("%s-%x", parts[len(parts)-1], md5.Sum([]byte(queryStringPart)))
}

filename = filepath.Join(parts...)
if !strings.HasSuffix(filename, ".json") {
filename = filename + ".json"
}
if _, err := os.Stat(filename); err != nil {
http.Error(rw, fmt.Sprintf("%s doesn't exist or couldn't be read. Create it with the mock you'd like to use.\n Args were: %s", filename, queryStringPart), http.StatusNotFound)
return
}
} else {
filename = mr.mockPath
}

mockData, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
rw.Write(mockData)
}