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