From 35764c9b12e54c0befb3d296ed5e3f7a6f018185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Aguessy?= Date: Sun, 4 Mar 2018 22:12:33 +0100 Subject: [PATCH] Basic implementation of a fizzbuzz API server --- fizzbuzz/fizzbuzz.go | 38 ++++++++++++++ fizzbuzz/fizzbuzz_test.go | 43 ++++++++++++++++ fizzbuzz/server.go | 64 +++++++++++++++++++++++ fizzbuzz/server_test.go | 103 ++++++++++++++++++++++++++++++++++++++ main.go | 15 ++++++ 5 files changed, 263 insertions(+) create mode 100644 fizzbuzz/fizzbuzz.go create mode 100644 fizzbuzz/fizzbuzz_test.go create mode 100644 fizzbuzz/server.go create mode 100644 fizzbuzz/server_test.go create mode 100644 main.go diff --git a/fizzbuzz/fizzbuzz.go b/fizzbuzz/fizzbuzz.go new file mode 100644 index 0000000..478f6f8 --- /dev/null +++ b/fizzbuzz/fizzbuzz.go @@ -0,0 +1,38 @@ +package fizzbuzz + +import "fmt" + +// inputs represent the inputs received by the fizzbuzz server +type inputs struct { + string1, string2 string + int1, int2, limit int +} + +// generateFizzbuzz creates the fizzbuzz-like output related to the current input +// It is lenient with invalid inputs (nil input, <1 limits, empty strings or nul integers, ...) +func (in *inputs) generateFizzbuzz() []string { + out := []string{} + if in == nil { + return out + } + for j := 1; j <= in.limit; j++ { + switch { + case isAMultiple(j, in.int1) && isAMultiple(j, in.int2): + out = append(out, in.string1+in.string2) + case isAMultiple(j, in.int1): + out = append(out, in.string1) + case isAMultiple(j, in.int2): + out = append(out, in.string2) + default: + out = append(out, fmt.Sprint(j)) + } + } + return out +} + +func isAMultiple(toTest, divisor int) bool { + if divisor == 0 { + return false + } + return (toTest % divisor) == 0 +} diff --git a/fizzbuzz/fizzbuzz_test.go b/fizzbuzz/fizzbuzz_test.go new file mode 100644 index 0000000..63e291f --- /dev/null +++ b/fizzbuzz/fizzbuzz_test.go @@ -0,0 +1,43 @@ +package fizzbuzz + +import ( + "reflect" + "testing" +) + +func TestGenerateFizzbuzz(t *testing.T) { + tcases := []struct { + in *inputs + expect []string + }{ + // Invlid cases + {in: nil, expect: []string{}}, + {in: &inputs{string1: "aa", string2: "bb", int1: 3, int2: 5, limit: 0}, expect: []string{}}, + { + in: &inputs{string1: "aa", string2: "bb", int1: 0, int2: 0, limit: 5}, + expect: []string{"1", "2", "3", "4", "5"}, + }, + { + in: &inputs{string1: "", string2: "bb", int1: 2, int2: 0, limit: 5}, + expect: []string{"1", "", "3", "", "5"}, + }, + { + in: &inputs{string1: "", string2: "", int1: 1, int2: 2, limit: 5}, + expect: []string{"", "", "", "", ""}, + }, + // Valid cases + { + in: &inputs{string1: "aa", string2: "bb", int1: 3, int2: 2, limit: 12}, + expect: []string{"1", "bb", "aa", "bb", "5", "aabb", "7", "bb", "aa", "bb", "11", "aabb"}, + }, + { + in: &inputs{string1: "cc", string2: "dd", int1: 10, int2: 5, limit: 11}, + expect: []string{"1", "2", "3", "4", "dd", "6", "7", "8", "9", "ccdd", "11"}, + }, + } + for i, tcase := range tcases { + if got, want := tcase.in.generateFizzbuzz(), tcase.expect; !reflect.DeepEqual(got, want) { + t.Fatalf("%d: got %#v, want %#v", i+1, got, want) + } + } +} diff --git a/fizzbuzz/server.go b/fizzbuzz/server.go new file mode 100644 index 0000000..df7d418 --- /dev/null +++ b/fizzbuzz/server.go @@ -0,0 +1,64 @@ +package fizzbuzz + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/wallix/awless/logger" +) + +// Server represents an instance of a fizzbuzz server +type Server struct{} + +// Routes returns the routes of the fizzbuzz server +func (s *Server) Routes() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/", s.handleFizzBuzz) + return mux +} + +func (s *Server) handleFizzBuzz(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + // The "/" pattern matches everything, + // so we need to check that we're at the root here + http.NotFound(w, r) + return + } + in := &inputs{} + + in.string1 = r.URL.Query().Get("string1") + in.string2 = r.URL.Query().Get("string2") + + var err error + if in.int1, err = parseInt(r.URL, "int1"); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if in.int2, err = parseInt(r.URL, "int2"); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if in.limit, err = parseInt(r.URL, "limit"); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err = json.NewEncoder(w).Encode(in.generateFizzbuzz()); err != nil { + logger.Errorf("error while encoding '%#v': %s", in, err.Error()) + http.Error(w, "encoding problem", http.StatusInternalServerError) + } +} + +func parseInt(url *url.URL, paramName string) (int, error) { + if param := url.Query().Get(paramName); param != "" { + intParam, err := strconv.Atoi(param) + if err != nil { + return 0, fmt.Errorf("%s: invalid integer '%s'", paramName, param) + } + return intParam, nil + } + return 0, nil +} diff --git a/fizzbuzz/server_test.go b/fizzbuzz/server_test.go new file mode 100644 index 0000000..d91dfc4 --- /dev/null +++ b/fizzbuzz/server_test.go @@ -0,0 +1,103 @@ +package fizzbuzz + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" +) + +func TestServer(t *testing.T) { + restAPI := &Server{} + tserver := httptest.NewServer(restAPI.Routes()) + defer tserver.Close() + + t.Run("invalid URLs", func(t *testing.T) { + tcases := []struct { + urlParams string + errCode int + errContains string + }{ + {urlParams: "?limit=toto", errCode: 400, errContains: "invalid integer 'toto'"}, + {urlParams: "?int1=tata", errCode: 400, errContains: "int1"}, + {urlParams: "?int2=titi", errCode: 400, errContains: "int2"}, + {urlParams: "/notFoundPath", errCode: 404, errContains: "not found"}, + } + for i, tcase := range tcases { + resp, err := http.Get(tserver.URL + tcase.urlParams) + if err != nil { + t.Fatalf("%d: %s", i+1, err) + } + assertStatus(i, t, resp, tcase.errCode) + if got, want := readErrorFromResponse(t, resp), tcase.errContains; !strings.Contains(got, want) { + t.Fatalf("%d: expect errors contains %s, got %s", i+1, want, got) + } + } + }) + t.Run("valid URLs", func(t *testing.T) { + emptyList := []string{} + tcases := []struct { + urlParams string + expectOut []string + }{ + // Invalid cases + {urlParams: "", expectOut: emptyList}, + {urlParams: "?", expectOut: emptyList}, + {urlParams: "?limit=0", expectOut: emptyList}, + {urlParams: "?string1=aa&string2=bb&int1=2&int2=3&limit=0", expectOut: emptyList}, + // Valid case + { + urlParams: "?string1=aa&string2=bb&int1=3&int2=2&limit=12", + expectOut: []string{"1", "bb", "aa", "bb", "5", "aabb", "7", "bb", "aa", "bb", "11", "aabb"}, + }, + } + for i, tcase := range tcases { + resp, err := http.Get(tserver.URL + tcase.urlParams) + if err != nil { + t.Fatalf("%d: %s", i+1, err) + } + assertStatus(i, t, resp, 200) + if got, want := readListFromResponse(t, resp), tcase.expectOut; !reflect.DeepEqual(got, want) { + t.Fatalf("%d: got %#v, want %#v", i+1, got, want) + } + } + }) + +} + +func assertStatus(i int, t *testing.T, resp *http.Response, expect int) { + t.Helper() + if got, want := resp.StatusCode, expect; got != want { + t.Fatalf("%d, got %d, want %d", i+1, got, want) + } +} + +func readListFromResponse(t *testing.T, resp *http.Response) []string { + t.Helper() + defer resp.Body.Close() + var list []string + bytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if len(bytes) == 0 { + return list + } + if err = json.Unmarshal(bytes, &list); err != nil { + t.Fatalf("error while unmarshalling %s: %s", string(bytes), err) + } + return list +} + +func readErrorFromResponse(t *testing.T, resp *http.Response) string { + t.Helper() + defer resp.Body.Close() + bytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + return string(bytes) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..8bab1c2 --- /dev/null +++ b/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "log" + "net/http" + + "dev.fxaguessy.fr/fx/fizzbuzz-lbc/fizzbuzz" +) + +func main() { + hostPort := ":8080" + server := &fizzbuzz.Server{} + log.Printf("Starting fizzbuzz server on %s", hostPort) + log.Fatal(http.ListenAndServe(hostPort, server.Routes())) +}