Browse Source

Basic implementation of a fizzbuzz API server

master
commit
35764c9b12
  1. 38
      fizzbuzz/fizzbuzz.go
  2. 43
      fizzbuzz/fizzbuzz_test.go
  3. 64
      fizzbuzz/server.go
  4. 103
      fizzbuzz/server_test.go
  5. 15
      main.go

38
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
}

43
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)
}
}
}

64
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
}

103
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)
}

15
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()))
}
Loading…
Cancel
Save