From 3611eae62a2e3c31eb236357cf7d726623e96a92 Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Thu, 1 Dec 2022 10:45:31 +0200 Subject: [PATCH 1/4] add solutuion --- coolfacts/cmd/coolfacts_client/client.go | 51 +++++++++++++++++----- coolfacts/cmd/coolfacts_client/main.go | 3 +- coolfacts/cmd/coolfacts_server/server.go | 54 +++++++++++++++--------- coolfacts/coolfact/fact.go | 4 +- coolfacts/coolfact/service.go | 6 ++- coolfacts/inmem/factsrepo.go | 24 ++++++++--- 6 files changed, 102 insertions(+), 40 deletions(-) diff --git a/coolfacts/cmd/coolfacts_client/client.go b/coolfacts/cmd/coolfacts_client/client.go index 51bbbf6..8963e09 100644 --- a/coolfacts/cmd/coolfacts_client/client.go +++ b/coolfacts/cmd/coolfacts_client/client.go @@ -7,6 +7,7 @@ import ( "io" "io/ioutil" "net/http" + "time" "github.com/FTBpro/go-workshop/coolfacts/coolfact" ) @@ -18,9 +19,9 @@ const ( type getFactsResponse struct { Facts []struct { - Image string `json:"image"` - Description string `json:"description"` - // TODO: add a field for createdAt + Image string `json:"image"` + Description string `json:"description"` + CreatedAt time.Time `json:"createdAt"` } `json:"facts"` } @@ -46,8 +47,16 @@ func NewClient(endpoint string) *client { } func (c *client) GetLastCreatedFact() (coolfact.Fact, error) { - // TODO: implement this method. - // Use the method GetAllFacts for getting all the facts + allFacts, err := c.GetAllFacts() + if err != nil { + return coolfact.Fact{}, fmt.Errorf("GetLastCreatedFact: %w", err) + } + + if len(allFacts) == 0 { + return coolfact.Fact{}, fmt.Errorf("fact not found") + } + + return allFacts[0], nil } func (c *client) GetAllFacts() ([]coolfact.Fact, error) { @@ -100,11 +109,33 @@ func (c *client) CreateFact(fct coolfact.Fact) error { } responseBody := bytes.NewBuffer(postBody) - // TODO: - // 1. create a new request. Use http.NewRequestWithContext. For argument use the ul and the responseBody. - // 2. Do the request using c.httpClient - // 3. As in GetAllFacts, in case of a failure (response status code is not 200), return error using readError - // * don't forget to close the body like we did in GetAllFacts method + req, err := http.NewRequest(http.MethodPost, ul, responseBody) + if err != nil { + return fmt.Errorf("client.CreateFact failed to create request: %v", err) + } + + res, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("client.CreateFact failed to do request: %v", err) + } + + defer func() { + if res != nil && res.Body != nil { + _, _ = io.Copy(ioutil.Discard, res.Body) + _ = res.Body.Close() + } + }() + + if res.StatusCode != http.StatusOK { + errMessage, err := c.readError(res) + if err != nil { + return fmt.Errorf("client.CreateFact: %s", err) + } + + return fmt.Errorf("client.CreateFact got an error from server. status: %d. error: %s", res.StatusCode, errMessage) + } + + return nil } type errorResponse struct { diff --git a/coolfacts/cmd/coolfacts_client/main.go b/coolfacts/cmd/coolfacts_client/main.go index b63c0c0..dc22219 100644 --- a/coolfacts/cmd/coolfacts_client/main.go +++ b/coolfacts/cmd/coolfacts_client/main.go @@ -4,11 +4,12 @@ import ( "bufio" "errors" "fmt" - "github.com/FTBpro/go-workshop/coolfacts/coolfact" "log" "os" "regexp" "strings" + + "github.com/FTBpro/go-workshop/coolfacts/coolfact" ) const ( diff --git a/coolfacts/cmd/coolfacts_server/server.go b/coolfacts/cmd/coolfacts_server/server.go index 8dbe10f..80f2f66 100644 --- a/coolfacts/cmd/coolfacts_server/server.go +++ b/coolfacts/cmd/coolfacts_server/server.go @@ -12,17 +12,20 @@ import ( type FactsService interface { GetFacts() ([]coolfact.Fact, error) - // TODO: add method CreateFact + CreateFact(fact coolfact.Fact) error } -// TODO: add struct factRequest -// This struct should represent the client request for creating a new fact. -// The client sends JSON: -// { -// "image": "...", -// "description": "..." -// } -// TODO: add method on this struct `ToCoolFact` that convert it into an entity coolfact.Fact +type createFactRequest struct { + Image string `json:"image"` + Description string `json:"description"` +} + +func (r createFactRequest) ToCoolFact() coolfact.Fact { + return coolfact.Fact{ + Image: r.Image, + Description: r.Description, + } +} type server struct { factsService FactsService @@ -37,10 +40,6 @@ func NewServer(factsService FactsService) *server { func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Println("incoming request", r.Method, r.URL.Path) - // TODO: add case to support the create fact API - // the expected path for creating a fact is "/paths", and the http method is POST (http.MethodPost) - // use server method HandleCreateFact - switch r.Method { case http.MethodGet: switch strings.ToLower(r.URL.Path) { @@ -52,6 +51,14 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { err := fmt.Errorf("path %q wasn't found", r.URL.Path) s.HandleNotFound(w, err) } + case http.MethodPost: + switch strings.ToLower(r.URL.Path) { + case "/facts": + s.HandleCreateFact(w, r) + default: + err := fmt.Errorf("path %q wasn't found", r.URL.Path) + s.HandleNotFound(w, err) + } default: err := fmt.Errorf("method %q is not allowed", r.Method) s.HandleNotFound(w, err) @@ -84,7 +91,7 @@ func (s *server) HandleGetFacts(w http.ResponseWriter) { formattedFacts[i] = map[string]interface{}{ "image": coolFact.Image, "description": coolFact.Description, - // TODO: add create at to the response + "createdAt": coolFact.CreatedAt, } } @@ -106,11 +113,20 @@ func (s *server) HandleGetFacts(w http.ResponseWriter) { func (s *server) HandleCreateFact(w http.ResponseWriter, r *http.Request) { log.Println("Handling createFact ...") - // TODO: - // 1. Read the request body into factRequest - // Use json.NewDecoder and Decode - // 2. Call the service for creating a fact - // 3. On success return status OK + var request createFactRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + err = fmt.Errorf("server.HandleCreateFact failed to decode request: %s", err) + s.HandleError(w, err) + return + } + + if err := s.factsService.CreateFact(request.ToCoolFact()); err != nil { + err = fmt.Errorf("server.HandleCreateFact: %s", err) + s.HandleError(w, err) + return + } + + w.WriteHeader(http.StatusOK) } func (s *server) HandleNotFound(w http.ResponseWriter, err error) { diff --git a/coolfacts/coolfact/fact.go b/coolfacts/coolfact/fact.go index d0ae393..d392a2e 100644 --- a/coolfacts/coolfact/fact.go +++ b/coolfacts/coolfact/fact.go @@ -1,7 +1,9 @@ package coolfact +import "time" + type Fact struct { Image string Description string - // TODO: add field CreatedAt from type time.Time. (time.Time represents an instant in time and have designated convenience methods) + CreatedAt time.Time } diff --git a/coolfacts/coolfact/service.go b/coolfacts/coolfact/service.go index 80b826e..9037ea0 100644 --- a/coolfacts/coolfact/service.go +++ b/coolfacts/coolfact/service.go @@ -4,7 +4,7 @@ import "fmt" type Repository interface { GetFacts() ([]Fact, error) - // TODO: add method createFact + CreateFact(fct Fact) error } type service struct { @@ -27,7 +27,9 @@ func (s *service) GetFacts() ([]Fact, error) { } func (s *service) CreateFact(fact Fact) error { - // TODO: implement CreateFact + if err := s.factsRepo.CreateFact(fact); err != nil { + return fmt.Errorf("factsService.CreateFact: %w", err) + } return nil } diff --git a/coolfacts/inmem/factsrepo.go b/coolfacts/inmem/factsrepo.go index c5355cd..000bc4a 100644 --- a/coolfacts/inmem/factsrepo.go +++ b/coolfacts/inmem/factsrepo.go @@ -1,6 +1,9 @@ package inmem import ( + "sort" + "time" + "github.com/FTBpro/go-workshop/coolfacts/coolfact" ) @@ -10,33 +13,40 @@ type factsRepo struct { func NewFactsRepository() *factsRepo { return &factsRepo{ - // TODO: add createdAt to the facts. You can set it to now facts: []coolfact.Fact{ { Image: "https://images2.minutemediacdn.com/image/upload/v1556645500/shape/cover/entertainment/D5aliXvWsAEcYoK-fe997566220c082b98030508e654948e.jpg", Description: "Did you know sonic is a hedgehog?!", + CreatedAt: time.Now(), }, { Image: "https://images2.minutemediacdn.com/image/upload/v1556641470/shape/cover/entertainment/uncropped-Screen-Shot-2019-04-30-at-122411-PM-3b804f143c543dfab4b75c81833bed1b.jpg", Description: "You won't believe what happened to Arya!", + CreatedAt: time.Now(), }, }, } } func (r *factsRepo) GetFacts() ([]coolfact.Fact, error) { - //TODO: before returning the facts, sort the facts according to the createdAt - // Use sort.Sort method with the slice byCreatedAt. The most recent facts will be return first. - // For explain on sort.Sort, see example: https://gobyexample.com/sorting-by-functions - // Check methods of time.Time to decide how can you check if one time is after another one. (https://pkg.go.dev/time#Time.Before) + sort.Sort(byCreatedAt(r.facts)) return r.facts, nil } func (r *factsRepo) CreateFact(fact coolfact.Fact) error { - // TODO: implement + r.facts = append(r.facts, fact) + return nil } type byCreatedAt []coolfact.Fact -// TODO: make type byCreatedAt implement sort.Interface. Example: https://gobyexample.com/sorting-by-functions +func (s byCreatedAt) Len() int { + return len(s) +} +func (s byCreatedAt) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} +func (s byCreatedAt) Less(i, j int) bool { + return s[i].CreatedAt.After(s[j].CreatedAt) +} From 54bb803f00c18f800d99cc6fb2cfcf66b82cad19 Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Thu, 1 Dec 2022 14:00:08 +0200 Subject: [PATCH 2/4] doc --- coolfacts/cmd/coolfacts_server/server.go | 2 + coolfacts/docs/ex5-create-fact.md | 225 ++++++++++++++++++++++- 2 files changed, 226 insertions(+), 1 deletion(-) diff --git a/coolfacts/cmd/coolfacts_server/server.go b/coolfacts/cmd/coolfacts_server/server.go index 80f2f66..eed0db0 100644 --- a/coolfacts/cmd/coolfacts_server/server.go +++ b/coolfacts/cmd/coolfacts_server/server.go @@ -6,6 +6,7 @@ import ( "log" "net/http" "strings" + "time" "github.com/FTBpro/go-workshop/coolfacts/coolfact" ) @@ -24,6 +25,7 @@ func (r createFactRequest) ToCoolFact() coolfact.Fact { return coolfact.Fact{ Image: r.Image, Description: r.Description, + CreatedAt: time.Now(), } } diff --git a/coolfacts/docs/ex5-create-fact.md b/coolfacts/docs/ex5-create-fact.md index 003a5f7..f5054b3 100644 --- a/coolfacts/docs/ex5-create-fact.md +++ b/coolfacts/docs/ex5-create-fact.md @@ -57,7 +57,7 @@ Failure: - Missing field: 404 - Error in the server: 500 ``` -- The returned facts from the service should have a new field for the created time. Add this fieldin the formatted response in the method `HandleGetFacts` +- The returned facts from the service should have a new field for the created time. Add this field in the formatted response in the method `HandleGetFacts` - Add the required method in the `FactsService` interface. - Add a new case in the method `ServeHTTP`. Note that the http method is POST request, and the path is "/path". In case of a such a request, call the server method `HandleCreateFact` - Add struct `factService` for decoding the request into. This struct should be the representation of the request body. @@ -77,3 +77,226 @@ TODO:(oren) add gif ## Full Walkthrough +## Step 1 - Implement The BL +### coolfact/fact.go +In our entity, we add a new field `CreatedAt` for specifying the time the fact was created. +```go +type Fact struct { + Image string + Description string + CreatedAt time.Time // new +} +``` + +### coolfact/service.go +The service requires another method from the `Repository` interface +```go +type Repository interface { + GetFacts() ([]Fact, error) + CreateFact(fct Fact) error // new +} +``` +And in the `CreateFact` we just calling the repo: +```go +func (s *service) CreateFact(fact Fact) error { + if err := s.factsRepo.CreateFact(fact); err != nil { + return fmt.Errorf("factsService.CreateFact: %w", err) + } + + return nil +} +``` + +## Step 2 - The repo +In the `CreateFacts`, before reurning the facts, we will sort them based on their `CreatedAt` +```go +func (r *factsRepo) GetFacts() ([]coolfact.Fact, error) { + sort.Sort(byCreatedAt(r.facts)) + + return r.facts, nil +} +``` +We use method `sort.Sort`. This method expects an argument that implements `sort.Interface`: +```go +type Interface interface { + Len() int + Less(i, j int) bool + Swap(i, j int) +} +``` +Using these methods, it sorts the given slice. Since we can't our `r.facts` slice (because it doesn't implement the `sort.Interface`), we will use the type `byCreatedAt`: +```go +type byCreatedAt []coolfact.Fact + +func (s byCreatedAt) Len() int { + return len(s) +} + +func (s byCreatedAt) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s byCreatedAt) Less(i, j int) bool { + return s[i].CreatedAt.After(s[j].CreatedAt) +} +``` + +Note the type conversion: `byCreatedAt(r.facts)`. Type conversion is simply to convert some value to other type. +This is possible since the two types are compatible. + +## Step 3 - Server +The service exports a new API for creating the fact. The http method is `POST`, and the path is `"/facts"`. We will add a new case in our `ServeHTTP` method: +```go +func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Println("incoming request", r.Method, r.URL.Path) + + switch r.Method { + case http.MethodGet: + // code omitted + case http.MethodPost: + switch strings.ToLower(r.URL.Path) { + case "/facts": + s.HandleCreateFact(w, r) + default: + err := fmt.Errorf("path %q wasn't found", r.URL.Path) + s.HandleNotFound(w, err) + } + default: + // code omitted + } +} +``` +For decoding the request, we would use a struct `createFactRequest` which is a representation of the request payload: +```go +type createFactRequest struct { + Image string `json:"image"` + Description string `json:"description"` +} + +func (r createFactRequest) ToCoolFact() coolfact.Fact { + return coolfact.Fact{ + Image: r.Image, + Description: r.Description, + CreatedAt: time.Now(), + } +} +``` +Note that in the method `ToCoolFact` we also set the `CreatedAt` to `time.Now()`. + +And then we could implement `HandleCreateFact`: +```go +func (s *server) HandleCreateFact(w http.ResponseWriter, r *http.Request) { + log.Println("Handling createFact ...") + + var request createFactRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + err = fmt.Errorf("server.HandleCreateFact failed to decode request: %s", err) + s.HandleError(w, err) + return + } + + if err := s.factsService.CreateFact(request.ToCoolFact()); err != nil { + err = fmt.Errorf("server.HandleCreateFact: %s", err) + s.HandleError(w, err) + return + } + + w.WriteHeader(http.StatusOK) +} +``` + +Finally, in `HandleGetFacts` we just need to add the `createdAt` to the response: +```go +func (s *server) HandleGetFacts(w http.ResponseWriter) { + log.Println("Handling getFact ...") + facts, err := s.factsService.GetFacts() + if err != nil { + formattedFacts[i] = map[string]interface{}{ + "image": coolFact.Image, + "description": coolFact.Description, + "createdAt": coolFact.CreatedAt, // new + } + } + + // code omitted +} + +``` + +## Step 4 - The Client +The client receives the response of the facts from the service, so we'll add the field `createdAt`, so it will be decoded as well: +```go +type getFactsResponse struct { + Facts []struct { + Image string `json:"image"` + Description string `json:"description"` + CreatedAt time.Time `json:"createdAt"` // new + } `json:"facts"` +} +``` + +We will implement the method GetLAstFact using the existing method `GetAllFacts`: +```go +func (c *client) GetLastCreatedFact() (coolfact.Fact, error) { + allFacts, err := c.GetAllFacts() + if err != nil { + return coolfact.Fact{}, fmt.Errorf("GetLastCreatedFact: %w", err) + } + + if len(allFacts) == 0 { + return coolfact.Fact{}, fmt.Errorf("fact not found") + } + + return allFacts[0], nil +} + +``` + +And finally, we implement the method `CreateFact` which call the service +```go +func (c *client) CreateFact(fct coolfact.Fact) error { + ul := c.endpoint + pathCreateFact + // First we are preparing the payload + payload := map[string]interface{}{ + "image": fct.Image, + "description": fct.Description, + } + // we need io.Reader to create a new http request. + // we will create bytes.Buffer which implement this interface + postBody, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("client.CreateFact failed to marshal payload: %v", err) + } + responseBody := bytes.NewBuffer(postBody) + + req, err := http.NewRequest(http.MethodPost, ul, responseBody) + if err != nil { + return fmt.Errorf("client.CreateFact failed to create request: %v", err) + } + + res, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("client.CreateFact failed to do request: %v", err) + } + + defer func() { + if res != nil && res.Body != nil { + _, _ = io.Copy(ioutil.Discard, res.Body) + _ = res.Body.Close() + } + }() + + if res.StatusCode != http.StatusOK { + errMessage, err := c.readError(res) + if err != nil { + return fmt.Errorf("client.CreateFact: %s", err) + } + + return fmt.Errorf("client.CreateFact got an error from server. status: %d. error: %s", res.StatusCode, errMessage) + } + + return nil +} +``` + +# Finish! \ No newline at end of file From f35a2243dfbe0506675d776f5613ec238ecdb05e Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Sat, 3 Dec 2022 16:05:55 +0200 Subject: [PATCH 3/4] add test --- coolfacts/cmd/coolfacts_client/client.go | 42 +++++++------- coolfacts/coolfact/service_test.go | 74 +++++++++++------------- coolfacts/docs/ex5-create-fact.md | 57 ++++++++++++++++++ 3 files changed, 113 insertions(+), 60 deletions(-) diff --git a/coolfacts/cmd/coolfacts_client/client.go b/coolfacts/cmd/coolfacts_client/client.go index 65fed14..4262a44 100644 --- a/coolfacts/cmd/coolfacts_client/client.go +++ b/coolfacts/cmd/coolfacts_client/client.go @@ -8,7 +8,7 @@ import ( "io/ioutil" "net/http" "time" - + "github.com/FTBpro/go-workshop/coolfacts/coolfact" ) @@ -30,7 +30,7 @@ func (r getFactsResponse) toCoolFacts() []coolfact.Fact { for i, fact := range r.Facts { coolfacts[i] = coolfact.Fact(fact) } - + return coolfacts } @@ -51,11 +51,11 @@ func (c *client) GetLastCreatedFact() (coolfact.Fact, error) { if err != nil { return coolfact.Fact{}, fmt.Errorf("GetLastCreatedFact: %w", err) } - + if len(allFacts) == 0 { return coolfact.Fact{}, fmt.Errorf("fact not found") } - + return allFacts[0], nil } @@ -65,7 +65,7 @@ func (c *client) GetAllFacts() ([]coolfact.Fact, error) { if err != nil { return nil, fmt.Errorf("client.GetLastCreatedFact to do request: %v", err) } - + // The client must close the body after the response is handled // We must read all the body before closing it, so for reading the body and copying to ioutil.Discard, which does nothing defer func() { @@ -74,33 +74,33 @@ func (c *client) GetAllFacts() ([]coolfact.Fact, error) { res.Body.Close() } }() - + if res.StatusCode != http.StatusOK { errMessage, err := c.readError(res) if err != nil { return nil, fmt.Errorf("client.CreateFact: %s", err) } - + return nil, fmt.Errorf("client.GetLastCreatedFact got an error from server. status: %d. error: %s", res.StatusCode, errMessage) } - + getFactsRes, err := c.readResponseGetFacts(res) if err != nil { return nil, fmt.Errorf("client.GetLastCreatedFact: %s", err) } - + return getFactsRes.toCoolFacts(), nil } func (c *client) CreateFact(fact coolfact.Fact) error { ul := c.endpoint + pathCreateFact - + // First we are preparing the payload payload := map[string]interface{}{ "topic": fact.Topic, "description": fact.Description, } - + // we need io.Reader to create a new http request. // we will create bytes.Buffer which implement this interface postBody, err := json.Marshal(payload) @@ -108,33 +108,33 @@ func (c *client) CreateFact(fact coolfact.Fact) error { return fmt.Errorf("client.CreateFact failed to marshal payload: %v", err) } responseBody := bytes.NewBuffer(postBody) - + req, err := http.NewRequest(http.MethodPost, ul, responseBody) if err != nil { return fmt.Errorf("client.CreateFact failed to create request: %v", err) } - + res, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("client.CreateFact failed to do request: %v", err) } - + defer func() { if res != nil && res.Body != nil { _, _ = io.Copy(ioutil.Discard, res.Body) _ = res.Body.Close() } }() - + if res.StatusCode != http.StatusOK { errMessage, err := c.readError(res) if err != nil { return fmt.Errorf("client.CreateFact: %s", err) } - + return fmt.Errorf("client.CreateFact got an error from server. status: %d. error: %s", res.StatusCode, errMessage) } - + return nil } @@ -145,17 +145,17 @@ type errorResponse struct { func (c *client) readError(res *http.Response) (string, error) { var errRes errorResponse if err := json.NewDecoder(res.Body).Decode(&errRes); err != nil { - return "", fmt.Errorf("readBody failed to read response body: %v. \nbody string is: %s", err) + return "", fmt.Errorf("readBody failed to read response body: %v", err) } - + return errRes.Error, nil } func (c *client) readResponseGetFacts(res *http.Response) (getFactsResponse, error) { var factsRes getFactsResponse if err := json.NewDecoder(res.Body).Decode(&factsRes); err != nil { - return getFactsResponse{}, fmt.Errorf("readResponseGetFacts failed to read response body: %v. \nbody string is: %s", err) + return getFactsResponse{}, fmt.Errorf("readResponseGetFacts failed to read response body: %v", err) } - + return factsRes, nil } diff --git a/coolfacts/coolfact/service_test.go b/coolfacts/coolfact/service_test.go index d4c3122..eb9c2cb 100644 --- a/coolfacts/coolfact/service_test.go +++ b/coolfacts/coolfact/service_test.go @@ -14,46 +14,42 @@ import ( ) func Test_service_GetFacts(t *testing.T) { - type fields struct { - factsRepo coolfact.Repository - } - facts := generateRandomFactsDesc(10) tests := []struct { - name string - fields fields - want []coolfact.Fact - wantErr bool + name string + repoField coolfact.Repository + want []coolfact.Fact + wantErr bool }{ { - name: "10 facts", - fields: fields{ - factsRepo: inmem.NewFactsRepository(facts...), - }, - want: facts, - wantErr: false, + name: "add in a sorted way", + repoField: inmem.NewFactsRepository(facts...), + want: facts, + wantErr: false, + }, + { + name: "add in a UNsorted way", + repoField: inmem.NewFactsRepository(facts[5], facts[4], facts[2]), + want: []coolfact.Fact{facts[2], facts[4], facts[5]}, + wantErr: false, }, { - name: "no facts - should get nil", - fields: fields{ - factsRepo: inmem.NewFactsRepository(), - }, - want: nil, - wantErr: false, + name: "no facts - should get nil", + repoField: inmem.NewFactsRepository(), + want: nil, + wantErr: false, }, { - name: "repo returns error", - fields: fields{ - factsRepo: mockRepoError{}, - }, - want: nil, - wantErr: true, + name: "repo returns error", + repoField: mockRepoError{}, + want: nil, + wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := coolfact.NewService(tt.fields.factsRepo) + s := coolfact.NewService(tt.repoField) got, err := s.GetFacts() if (err != nil) != tt.wantErr { t.Errorf("GetFacts() error = %v, wantErr %v", err, tt.wantErr) @@ -71,40 +67,40 @@ func Test_service_CreateFact(t *testing.T) { tests := []struct { name string - inputRepo coolfact.Repository + repoField coolfact.Repository factsToCreate []coolfact.Fact - wantFacts []coolfact.Fact + want []coolfact.Fact wantErr bool }{ { name: "base case - adding sorted", - inputRepo: inmem.NewFactsRepository(facts[5]), + repoField: inmem.NewFactsRepository(facts[5]), factsToCreate: []coolfact.Fact{facts[2]}, - wantFacts: []coolfact.Fact{facts[2], facts[5]}, + want: []coolfact.Fact{facts[2], facts[5]}, }, { name: "add fact from the past", - inputRepo: inmem.NewFactsRepository(facts[3]), + repoField: inmem.NewFactsRepository(facts[3]), factsToCreate: []coolfact.Fact{facts[5]}, - wantFacts: []coolfact.Fact{facts[3], facts[5]}, + want: []coolfact.Fact{facts[3], facts[5]}, }, { name: "add many mixed facts", - inputRepo: inmem.NewFactsRepository(facts[3], facts[5], facts[1], facts[9]), + repoField: inmem.NewFactsRepository(facts[3], facts[5], facts[1], facts[9]), factsToCreate: []coolfact.Fact{facts[4], facts[2], facts[0]}, - wantFacts: []coolfact.Fact{facts[0], facts[1], facts[2], facts[3], facts[4], facts[5], facts[9]}, + want: []coolfact.Fact{facts[0], facts[1], facts[2], facts[3], facts[4], facts[5], facts[9]}, }, { name: "repo returns error", - inputRepo: mockRepoError{}, + repoField: mockRepoError{}, factsToCreate: nil, - wantFacts: nil, + want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := coolfact.NewService(tt.inputRepo) + s := coolfact.NewService(tt.repoField) for _, fact := range tt.factsToCreate { err := s.CreateFact(fact) if tt.wantErr { @@ -122,7 +118,7 @@ func Test_service_CreateFact(t *testing.T) { } require.NoError(t, err) - require.Equal(t, gotFacts, tt.wantFacts) + require.Equal(t, gotFacts, tt.want) }) } } diff --git a/coolfacts/docs/ex5-create-fact.md b/coolfacts/docs/ex5-create-fact.md index 741be7c..31d7b12 100644 --- a/coolfacts/docs/ex5-create-fact.md +++ b/coolfacts/docs/ex5-create-fact.md @@ -26,6 +26,63 @@ The basic type is `time.Time` which represents an instant in time with nanosecon For sorting, you will learn and use the go package `sort`. +## Step 0.1 - Notice `coolfact/service_test.go` + +You can notice that wev'e added tests for our service, before you start to implement, let's understand what's in it. + +Tests files in go have the suffix `_test`. These files are not been built when you build the application. They are only considered when running the test go command: +```commandline +go test [build/test flags] [packages] [build/test flags & test binary flags] +``` + +For runnning all the tests, you need to be on the root folder (the one with the go.mod) and run +```commandline +.../coolfacts$ go test ./... +``` +Let's take a look in the file itself. notice it's package name `coolfact_test`. In Go, the only valid case for a folder to contain two packages is a test package. The suffix `_test` to the package isn't mandatory, but it helps when you only wish to test the public interface of the package. This is best for checking the interface from a real consumer POV. + +When running the `go test` command, Go searchs in all the `_test.go` files for functions with `Test` prefix. These files takes one argument `t *testing.T` which is a type passed to Test functions to manage test state and support formatted test logs. + +Test functions can be named anything with a `Test` prefix, but there is some convention: +```go +func Test__ +``` +In here, we test our `service` methods `GetFacts` and `CreateFacts`, so our test functions are named +```go +func Test_service_AllFacts(t *testing.T) {...} + +func Test_service_CreateFact(t *testing.T) {...} +``` + +The structure of the test is an example of _Table Driven Tests_: + +```go +func Test...(t *testing.T) { + type testCase struct {...} + + tests := []testCase{ + {...}, + {...}, + } + + for _, tc := range tests { + t.Run(tc.Name, func(t *testing.T) { + // Here we write the test itself + }) + } +} +``` +We declaring type `testCase` which is a struct that holds the parameters for the tests, these can be: +- Name of the test. +- Input for initializing the service. +- Arguments for the methods. +- Expected result. +- Indicator if we expect an error. + +In the `t.Run` method we write the test. In `GetFacts` for example, we initialize the service with the repo we set in the `repoField` field, call the method `GetFacts` and expected to receive either en error or what we set in the field `want`. + +You can also notice, that in the first test we've used `t.Fatal`, and in the second test we've used `require`. `require` is a package that provides helpful methods for testing. It also prints the failure in a more readable way. Since it's an external library, you can see that we've added a require in `go.mod`. + ## Step 1 - Implement The BL ### coolfact/fact.go - For supporting the sorting, add the field for the createdAt. From 2ed70ba7c75a6e359889e373dcf922567442ea87 Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Sat, 10 Dec 2022 18:35:53 +0200 Subject: [PATCH 4/4] fix --- coolfacts/cmd/coolfacts_client/main.go | 2 +- coolfacts/cmd/coolfacts_server/server.go | 32 ++++++++++++------------ coolfacts/docs/ex5-create-fact.md | 4 +-- coolfacts/inmem/factsrepo.go | 4 +-- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/coolfacts/cmd/coolfacts_client/main.go b/coolfacts/cmd/coolfacts_client/main.go index 087fd40..cc1710b 100644 --- a/coolfacts/cmd/coolfacts_client/main.go +++ b/coolfacts/cmd/coolfacts_client/main.go @@ -90,7 +90,7 @@ func processCmd(cl *client, cmd string, args []string) (string, error) { } err := cl.CreateFact(fct) - return "", err + return fmt.Sprintf(" ---> Fact created successfully"), err default: return "", errors.New("unknown command") diff --git a/coolfacts/cmd/coolfacts_server/server.go b/coolfacts/cmd/coolfacts_server/server.go index a7e4714..523f254 100644 --- a/coolfacts/cmd/coolfacts_server/server.go +++ b/coolfacts/cmd/coolfacts_server/server.go @@ -7,7 +7,7 @@ import ( "net/http" "strings" "time" - + "github.com/FTBpro/go-workshop/coolfacts/coolfact" ) @@ -41,7 +41,7 @@ func NewServer(factsService FactsService) *server { func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Println("incoming request", r.Method, r.URL.Path) - + switch r.Method { case http.MethodGet: switch strings.ToLower(r.URL.Path) { @@ -66,9 +66,9 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *server) HandlePing(w http.ResponseWriter, _ *http.Request) { log.Println("Handling Ping ...") - + w.WriteHeader(http.StatusOK) - + if _, err := fmt.Fprint(w, "PONG"); err != nil { fmt.Printf("ERROR writing to ResponseWriter: %s\n", err) return @@ -77,20 +77,20 @@ func (s *server) HandlePing(w http.ResponseWriter, _ *http.Request) { func (s *server) HandleGetFacts(w http.ResponseWriter, _ *http.Request) { log.Println("Handling getFact ...") - + facts, err := s.factsService.GetFacts() if err != nil { s.HandleError(w, fmt.Errorf("server.GetFactsHandler: %w", err)) return } - + response := s.formatGetFactsResponse(facts) - + // write status and content-type // status must be written before the body w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") - + // write the body. We use json encoding if err := json.NewEncoder(w).Encode(response); err != nil { fmt.Printf("HandleGetFacts ERROR writing response: %s", err) @@ -99,33 +99,33 @@ func (s *server) HandleGetFacts(w http.ResponseWriter, _ *http.Request) { func (s *server) HandleCreateFact(w http.ResponseWriter, r *http.Request) { log.Println("Handling createFact ...") - + var request createFactRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { err = fmt.Errorf("server.HandleCreateFact failed to decode request: %s", err) s.HandleError(w, err) return } - + if err := s.factsService.CreateFact(request.ToCoolFact()); err != nil { err = fmt.Errorf("server.HandleCreateFact: %s", err) s.HandleError(w, err) return } - + w.WriteHeader(http.StatusOK) } func (s *server) HandleNotFound(w http.ResponseWriter, r *http.Request) { log.Println("Handling notFound ...") - + w.WriteHeader(http.StatusNotFound) w.Header().Set("Content-Type", "application/json") - + response := map[string]string{ "error": fmt.Sprintf("path %s %s not found", r.Method, r.URL.Path), } - + if err := json.NewEncoder(w).Encode(response); err != nil { err = fmt.Errorf("HandleNotFound failed to decode: %s", err) s.HandleError(w, err) @@ -134,7 +134,7 @@ func (s *server) HandleNotFound(w http.ResponseWriter, r *http.Request) { func (s *server) HandleError(w http.ResponseWriter, err error) { log.Println("Handling error ...") - + w.WriteHeader(http.StatusInternalServerError) w.Header().Set("Content-Type", "application/json") response := map[string]string{ @@ -154,7 +154,7 @@ func (s *server) formatGetFactsResponse(facts []coolfact.Fact) map[string]interf "createdAt": coolFact.CreatedAt, } } - + return map[string]interface{}{ "facts": formattedFacts, } diff --git a/coolfacts/docs/ex5-create-fact.md b/coolfacts/docs/ex5-create-fact.md index b16f4f9..f241fc0 100644 --- a/coolfacts/docs/ex5-create-fact.md +++ b/coolfacts/docs/ex5-create-fact.md @@ -71,9 +71,9 @@ Failure: ## Building and Running -If everything is implemented well, this is what the final result should look like when running the program, in one tab we are running the server as we did in the previous exercise, and in another tab we are running the client, and writing the command for getting the facts: +If everything is implemented well, this is what the final result should look like when running the applications: -TODO:(oren) add gif +![v5-create](https://user-images.githubusercontent.com/5252381/206865178-d3b54bfc-fbf8-4e1e-ac7e-d6252106f527.gif) ## Full Walkthrough diff --git a/coolfacts/inmem/factsrepo.go b/coolfacts/inmem/factsrepo.go index 6495e00..56681fb 100644 --- a/coolfacts/inmem/factsrepo.go +++ b/coolfacts/inmem/factsrepo.go @@ -2,7 +2,7 @@ package inmem import ( "sort" - + "github.com/FTBpro/go-workshop/coolfacts/coolfact" ) @@ -18,7 +18,7 @@ func NewFactsRepository(facts ...coolfact.Fact) *factsRepo { func (r *factsRepo) GetFacts() ([]coolfact.Fact, error) { sort.Sort(byCreatedAt(r.facts)) - + return r.facts, nil }