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