diff --git a/coolfacts/cmd/coolfacts_client/client.go b/coolfacts/cmd/coolfacts_client/client.go index 34da0ff..08b4b46 100644 --- a/coolfacts/cmd/coolfacts_client/client.go +++ b/coolfacts/cmd/coolfacts_client/client.go @@ -47,7 +47,11 @@ func NewClient(endpoint string) *client { } func (c *client) GetLastCreatedFact() (coolfact.Fact, error) { - allFacts, err := c.GetFacts() + filters := coolfact.Filters{ + Limit: 1, + } + + allFacts, err := c.GetFacts(filters) if err != nil { return coolfact.Fact{}, fmt.Errorf("GetLastCreatedFact: %w", err) } @@ -59,8 +63,8 @@ func (c *client) GetLastCreatedFact() (coolfact.Fact, error) { return allFacts[0], nil } -func (c *client) GetFacts() ([]coolfact.Fact, error) { - ul := c.endpoint + pathGetFacts +func (c *client) GetFacts(filters coolfact.Filters) ([]coolfact.Fact, error) { + ul := fmt.Sprintf("%s%s?limit=%d&topic=%s", c.endpoint, pathGetFacts, filters.Limit, filters.Topic) res, err := c.httpClient.Get(ul) if err != nil { return nil, fmt.Errorf("client.GetLastCreatedFact to do request: %v", err) diff --git a/coolfacts/cmd/coolfacts_client/main.go b/coolfacts/cmd/coolfacts_client/main.go index a463fa9..88c246a 100644 --- a/coolfacts/cmd/coolfacts_client/main.go +++ b/coolfacts/cmd/coolfacts_client/main.go @@ -7,6 +7,7 @@ import ( "log" "os" "regexp" + "strconv" "strings" "github.com/FTBpro/go-workshop/coolfacts/coolfact" @@ -60,7 +61,25 @@ func processCmd(cl *client, cmd string, args []string) (string, error) { case "": return "", nil case commandGetFacts: - facts, err := cl.GetFacts() + if len(args) < 1 { + return "", fmt.Errorf("must add argument for limit") + } + limit, err := strconv.Atoi(args[0]) + if err != nil { + return "", fmt.Errorf("limit must be a number") + } + + var topic string + if len(args) > 1 { + topic = args[1] + } + + filters := coolfact.Filters{ + Topic: topic, + Limit: limit, + } + + facts, err := cl.GetFacts(filters) if err != nil { return "", err } diff --git a/coolfacts/cmd/coolfacts_server/server.go b/coolfacts/cmd/coolfacts_server/server.go index 523f254..bab58f7 100644 --- a/coolfacts/cmd/coolfacts_server/server.go +++ b/coolfacts/cmd/coolfacts_server/server.go @@ -12,6 +12,7 @@ import ( ) type FactsService interface { + // TODO: fix signature for GetFacts GetFacts() ([]coolfact.Fact, error) CreateFact(fact coolfact.Fact) error } @@ -75,7 +76,7 @@ func (s *server) HandlePing(w http.ResponseWriter, _ *http.Request) { } } -func (s *server) HandleGetFacts(w http.ResponseWriter, _ *http.Request) { +func (s *server) HandleGetFacts(w http.ResponseWriter, r *http.Request) { log.Println("Handling getFact ...") facts, err := s.factsService.GetFacts() @@ -145,6 +146,12 @@ func (s *server) HandleError(w http.ResponseWriter, err error) { } } +func (s *server) HandleBadRequest(w http.ResponseWriter, err error) { + log.Println("Handling Bad Request ...") + + // TODO: implement +} + func (s *server) formatGetFactsResponse(facts []coolfact.Fact) map[string]interface{} { formattedFacts := make([]map[string]interface{}, len(facts)) for i, coolFact := range facts { diff --git a/coolfacts/cmd/coolfacts_server/server_test.go b/coolfacts/cmd/coolfacts_server/server_test.go new file mode 100644 index 0000000..813d2ba --- /dev/null +++ b/coolfacts/cmd/coolfacts_server/server_test.go @@ -0,0 +1,219 @@ +package main_test + +import ( + "bytes" + "encoding/json" + "fmt" + "math/rand" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/require" + + server "github.com/FTBpro/go-workshop/coolfacts/cmd/coolfacts_server" + "github.com/FTBpro/go-workshop/coolfacts/coolfact" +) + +func Test_Server_GetFacts(t *testing.T) { + facts := generateRandomFactsDesc(10) + testCases := []struct { + name string + queryParamsToSend string + expectedFilters coolfact.Filters + want []coolfact.Fact + wantErr bool + expectedHTTPStatus int + }{ + { + name: "10 facts with filters", + queryParamsToSend: "?limit=10&topic=TV", + expectedFilters: coolfact.Filters{ + Topic: "TV", + Limit: 10, + }, + want: facts, + expectedHTTPStatus: http.StatusOK, + }, + { + name: "no topic", + queryParamsToSend: "?limit=10", + expectedFilters: coolfact.Filters{ + Topic: "", + Limit: 10, + }, + want: facts, + expectedHTTPStatus: http.StatusOK, + }, + { + name: "no limit - expect bad request", + queryParamsToSend: "", + want: nil, + wantErr: true, + expectedHTTPStatus: http.StatusBadRequest, + }, + { + name: "limit is not an int - expect bad request", + queryParamsToSend: "?limit=one", + want: nil, + wantErr: true, + expectedHTTPStatus: http.StatusBadRequest, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockService := mockFactsService{ + factsToReturn: tc.want, + } + + srv := server.NewServer(&mockService) + ts := httptest.NewServer(srv) + + res, err := http.Get(ts.URL + "/facts" + tc.queryParamsToSend) + require.NoError(t, err) + require.Equal(t, tc.expectedHTTPStatus, res.StatusCode) + + if tc.wantErr { + return + } + + gotFacts, err := factsFromResponse(t, res) + require.NoError(t, err) + + require.Equal(t, tc.expectedFilters, mockService.filtersGot) + expectEqualFacts(t, tc.want, gotFacts) + }) + } +} + +func Test_Server_CreateFacts(t *testing.T) { + facts := generateRandomFactsDesc(10) + testCases := []struct { + name string + queryParamsToSend string + factToCreate coolfact.Fact + wantErr bool + expectedHTTPStatus int + }{ + { + name: "10 facts with filters", + factToCreate: facts[0], + expectedHTTPStatus: http.StatusOK, + }, + { + name: "no topic", + factToCreate: facts[0], + expectedHTTPStatus: http.StatusOK, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockService := mockFactsService{} + srv := server.NewServer(&mockService) + ts := httptest.NewServer(srv) + defer ts.Close() + + payload := map[string]interface{}{ + "topic": tc.factToCreate.Topic, + "description": tc.factToCreate.Description, + } + + postBody, err := json.Marshal(payload) + require.NoError(t, err) + + responseBody := bytes.NewBuffer(postBody) + + res, err := http.Post(ts.URL+"/facts", "application/json", responseBody) + require.NoError(t, err) + require.Equal(t, tc.expectedHTTPStatus, res.StatusCode) + + if tc.wantErr { + return + } + require.Equal(t, tc.factToCreate.Topic, mockService.createFactGotFact.Topic) + require.Equal(t, tc.factToCreate.Description, mockService.createFactGotFact.Description) + }) + } +} + +func generateRandomFactsDesc(n int) []coolfact.Fact { + var facts []coolfact.Fact + for i := 0; i < n; i++ { + fact := randomFact() + fact.CreatedAt = time.Now().Add(-(time.Duration(i) * time.Hour)).UTC() + facts = append(facts, fact) + } + + return facts +} + +func randomFact() coolfact.Fact { + rand.Seed(time.Now().UnixNano()) + return coolfact.Fact{ + Topic: fmt.Sprintf("Topic %d", rand.Intn(10000)), + Description: fmt.Sprintf("Some Description %d", rand.Intn(10000)), + } +} + +type getFactsResponse struct { + Facts []struct { + Topic string `json:"topic"` + Description string `json:"description"` + CreatedAt time.Time `json:"createdAt"` + } `json:"facts"` +} + +func factsFromResponse(t *testing.T, res *http.Response) ([]coolfact.Fact, error) { + var factsResponse getFactsResponse + err := json.NewDecoder(res.Body).Decode(&factsResponse) + require.NoErrorf(t, err, "factsFromResponse failed decode get facts response") + + facts := make([]coolfact.Fact, len(factsResponse.Facts)) + for i, fact := range factsResponse.Facts { + facts[i] = coolfact.Fact(fact) + } + + return facts, nil +} + +type mockFactsService struct { + filtersGot coolfact.Filters + factsToReturn []coolfact.Fact + shouldReturnError bool + + createFactGotFact coolfact.Fact +} + +func (m *mockFactsService) GetFacts(filters coolfact.Filters) ([]coolfact.Fact, error) { + m.filtersGot = filters + + if m.shouldReturnError { + return nil, fmt.Errorf("mockFactsService asked to return an error") + } + + return m.factsToReturn, nil +} + +func (m *mockFactsService) CreateFact(fact coolfact.Fact) error { + m.createFactGotFact = fact + + if m.shouldReturnError { + return fmt.Errorf("mockFactsService asked to return an error") + } + + return nil +} + +func expectEqualFacts(t *testing.T, expected, got []coolfact.Fact) { + require.Equalf(t, len(expected), len(got), "expectEqualFacts: different length") + + for i, gotFact := range got { + expectedFact := expected[i] + require.Equal(t, expectedFact.Topic, gotFact.Topic) + require.Equal(t, expectedFact.Description, gotFact.Description) + require.Equal(t, expectedFact.CreatedAt, gotFact.CreatedAt) + } +} diff --git a/coolfacts/coolfact/fact.go b/coolfacts/coolfact/fact.go index a764521..49420f1 100644 --- a/coolfacts/coolfact/fact.go +++ b/coolfacts/coolfact/fact.go @@ -7,3 +7,7 @@ type Fact struct { Description string CreatedAt time.Time } + +// TODO: add struct Filters with: +// - Topic +// - Limit diff --git a/coolfacts/coolfact/service.go b/coolfacts/coolfact/service.go index 9037ea0..641c588 100644 --- a/coolfacts/coolfact/service.go +++ b/coolfacts/coolfact/service.go @@ -3,6 +3,7 @@ package coolfact import "fmt" type Repository interface { + //TODO: fix signature for GetFacts GetFacts() ([]Fact, error) CreateFact(fct Fact) error } @@ -17,6 +18,7 @@ func NewService(factsRepo Repository) *service { } } +//TODO: fix signatur and call to repo func (s *service) GetFacts() ([]Fact, error) { facts, err := s.factsRepo.GetFacts() if err != nil { diff --git a/coolfacts/coolfact/service_test.go b/coolfacts/coolfact/service_test.go index 6ce2390..9a0ad0d 100644 --- a/coolfacts/coolfact/service_test.go +++ b/coolfacts/coolfact/service_test.go @@ -16,41 +16,63 @@ func Test_service_GetFacts(t *testing.T) { facts := generateRandomFactsDesc(10) testCases := []struct { - name string - repoField coolfact.Repository - want []coolfact.Fact - wantErr bool + name string + repoField coolfact.Repository + filtersInput coolfact.Filters + want []coolfact.Fact + wantErr bool }{ { name: "with facts", repoField: inmem.NewFactsRepository(facts...), - want: facts, - wantErr: false, + filtersInput: coolfact.Filters{ + Limit: 10, + }, + want: facts, + wantErr: false, }, { name: "add unsorted facts", repoField: inmem.NewFactsRepository(facts[5], facts[4], facts[2]), - want: []coolfact.Fact{facts[2], facts[4], facts[5]}, - wantErr: false, + filtersInput: coolfact.Filters{ + Limit: 10, + }, + want: []coolfact.Fact{facts[2], facts[4], facts[5]}, + wantErr: false, }, { name: "no facts - should get nil", repoField: inmem.NewFactsRepository(), - want: nil, - wantErr: false, + filtersInput: coolfact.Filters{ + Limit: 10, + }, + want: nil, + wantErr: false, }, { name: "repo returns error", repoField: mockRepoError{}, - want: nil, - wantErr: true, + filtersInput: coolfact.Filters{ + Limit: 10, + }, + want: nil, + wantErr: true, + }, + { + name: "limit", + repoField: inmem.NewFactsRepository(facts...), + filtersInput: coolfact.Filters{ + Limit: 5, + }, + want: facts[:5], + wantErr: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := coolfact.NewService(tc.repoField) - got, err := s.GetFacts() + got, err := s.GetFacts(tc.filtersInput) if err != nil { require.True(t, tc.wantErr, "got an unexpected error from service") return @@ -114,7 +136,11 @@ func Test_service_CreateFact(t *testing.T) { return } - gotFacts, err := s.GetFacts() + filters := coolfact.Filters{ + Limit: 10, + } + + gotFacts, err := s.GetFacts(filters) require.NoError(t, err) require.Equal(t, gotFacts, tc.want) }) @@ -156,7 +182,7 @@ func expectEqualFacts(t *testing.T, expected, got []coolfact.Fact) { type mockRepoError struct { } -func (m mockRepoError) GetFacts() ([]coolfact.Fact, error) { +func (m mockRepoError) GetFacts(_ coolfact.Filters) ([]coolfact.Fact, error) { return nil, fmt.Errorf("mock repo returns error") } diff --git a/coolfacts/inmem/factsrepo.go b/coolfacts/inmem/factsrepo.go index 56681fb..f943508 100644 --- a/coolfacts/inmem/factsrepo.go +++ b/coolfacts/inmem/factsrepo.go @@ -7,22 +7,26 @@ import ( ) type factsRepo struct { - facts []coolfact.Fact + factsByTopic map[string][]coolfact.Fact } func NewFactsRepository(facts ...coolfact.Fact) *factsRepo { + // TODO: fix initialization according to the new field type return &factsRepo{ facts: facts, } } -func (r *factsRepo) GetFacts() ([]coolfact.Fact, error) { +func (r *factsRepo) GetFacts(filters coolfact.Filters) ([]coolfact.Fact, error) { + // TODO: fix method. Return according to the filters. + // note - topic is optional. sort.Sort(byCreatedAt(r.facts)) return r.facts, nil } func (r *factsRepo) CreateFact(fact coolfact.Fact) error { + // TODO: fix according to the new field type r.facts = append(r.facts, fact) return nil }