commit
35764c9b12
5 changed files with 263 additions and 0 deletions
@ -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 |
|||
} |
|||
@ -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) |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
@ -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) |
|||
} |
|||
@ -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…
Reference in new issue