Ver Fonte

Web API: GET /{short} OK, 65% coverage, POST as 501 100% coverage.

Frederic G. MARAND há 5 anos atrás
pai
commit
5ce586d710
10 ficheiros alterados com 230 adições e 45 exclusões
  1. 22 0
      api/doc.go
  2. 49 0
      api/get.go
  3. 73 0
      api/get_test.go
  4. 10 0
      api/post.go
  5. 23 0
      api/post_test.go
  6. 4 44
      domain/domain_api_test.go
  7. 40 0
      domain/domain_api_testing.go
  8. 3 1
      domain/errors.go
  9. 2 0
      go.mod
  10. 4 0
      go.sum

+ 22 - 0
api/doc.go

@@ -0,0 +1,22 @@
+/*
+The Kurz Web API exposes these routes:
+
+  - GET "/<short>" : resolve a short URL
+	- Handler: HandleGetShort()
+    - Success: 307 to matching target URL
+    - Short not yet defined: 404 no matching target
+    - Client request incorrect: 400
+    - Server failure: 50*
+  - POST "/" with <target>: create a short URL from a target URL
+	- Handler: HandlePostTarget()
+    - Success: 201 with <new short>
+    - Already existing short: 409 with <existing short>
+    - Target blocked for permission reasons: 403
+    - Target legally censored: 451
+    - Server failure: 50*
+
+Code 451 MAY be replaced by 403, for example when legal censorship includes a
+gag order, super-injunction (UK), National security letter (US) or similar
+mechanisms.
+*/
+package api

+ 49 - 0
api/get.go

@@ -0,0 +1,49 @@
+package api
+
+import (
+	"net/http"
+
+	"code.osinet.fr/fgm/kurz/domain"
+	"github.com/gorilla/mux"
+)
+
+// HandleGetShort() handles GET /<short>.
+func HandleGetShort(w http.ResponseWriter, r *http.Request) {
+	short, ok := mux.Vars(r)["short"]
+	if !ok {
+		w.WriteHeader(http.StatusBadRequest)
+		return
+	}
+
+	target, err := domain.GetTargetURL(short)
+	// Happy path.
+	if err == nil {
+		w.Header().Set("Location", target)
+		w.WriteHeader(http.StatusTemporaryRedirect)
+		return
+	}
+
+	// Very sad path.
+	domainErr, ok := err.(domain.Error)
+	if !ok {
+		// All errors return by the API should be domain-specific errors.
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	// Normal sad paths.
+	var status int
+	switch domainErr.Kind {
+	case domain.ShortNotFound:
+		status = http.StatusNotFound
+	case domain.TargetBlockedError:
+		status = http.StatusForbidden
+	case domain.TargetCensoredError:
+		status = http.StatusUnavailableForLegalReasons
+	default:
+		// TargetInvalid is not supposed to happen in this case, so it is an internal error too.
+		status = http.StatusInternalServerError
+	}
+
+	w.WriteHeader(status)
+}

+ 73 - 0
api/get_test.go

@@ -0,0 +1,73 @@
+package api
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/gorilla/mux"
+
+	"code.osinet.fr/fgm/kurz/domain"
+)
+
+// Use the special case of ErrUseLastResponse to ignore redirects.
+func doNotFollowRedirects(_ *http.Request, _ []*http.Request) error {
+	return http.ErrUseLastResponse
+}
+
+const SampleShort = "short"
+
+func setupGet() (*httptest.Server, *http.Client) {
+	router := mux.NewRouter()
+	router.HandleFunc("/{short}", HandleGetShort)
+	ts := httptest.NewServer(router)
+
+	c := ts.Client()
+	c.CheckRedirect = doNotFollowRedirects
+
+	return ts, c
+}
+
+func TestHandleGetShortHappy(t *testing.T) {
+	const SampleTarget = "http://example.com/"
+
+	// Ensure storage contains the expected mapping.
+	sr := make(domain.MockShortRepo)
+	sr[domain.ShortURL{URL: SampleShort}] = domain.TargetURL{URL: SampleTarget}
+	domain.RegisterRepositories(sr, nil)
+
+	ts, c := setupGet()
+	defer ts.Close()
+
+	res, err := c.Get(ts.URL + "/" + SampleShort)
+
+	if err != nil {
+		t.Error("Getting an existing short URL should not fail")
+	}
+	res.Body.Close()
+
+	if res.StatusCode != http.StatusTemporaryRedirect {
+		t.Errorf("Existing short URL returned %d, expected %d", res.StatusCode, http.StatusTemporaryRedirect)
+	}
+}
+
+func TestHandleGetShortSadNotFound(t *testing.T) {
+	// Ensure empty storage.
+	sr := make(domain.MockShortRepo)
+	domain.RegisterRepositories(sr, nil)
+
+	ts, c := setupGet()
+	defer ts.Close()
+
+	res, err := c.Get(ts.URL + "/" + SampleShort)
+
+	if err != nil {
+		t.Error("Getting an nonexistent short URL should not fail")
+	}
+
+	res.Body.Close()
+
+	if res.StatusCode != http.StatusNotFound {
+		t.Errorf("Existing short URL returned %d, expected %d", res.StatusCode, http.StatusNotFound)
+	}
+}

+ 10 - 0
api/post.go

@@ -0,0 +1,10 @@
+package api
+
+import (
+	"net/http"
+)
+
+// HandlePostTarget() handles POST /(target).
+func HandlePostTarget(w http.ResponseWriter, r *http.Request) {
+	w.WriteHeader(http.StatusNotImplemented)
+}

+ 23 - 0
api/post_test.go

@@ -0,0 +1,23 @@
+package api
+
+import (
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+)
+
+func TestHandlePostTarget(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(HandlePostTarget))
+	defer ts.Close()
+
+	res, err := http.Post(ts.URL, "application/json", strings.NewReader(""))
+	if err != nil {
+		fmt.Println(err)
+		t.FailNow()
+	}
+	if res.StatusCode != http.StatusNotImplemented {
+		t.Error("Post should return a 'not implemented' error")
+	}
+}

+ 4 - 44
domain/domain_api_test.go

@@ -1,58 +1,18 @@
 package domain
 
 import (
-	"errors"
 	"testing"
 )
 
-type mockShortRepo map[ShortURL]TargetURL
-
-func (sr mockShortRepo) GetTarget(su ShortURL) (tu TargetURL, err error) {
-	tu, ok := sr[su]
-	if !ok {
-		err = errors.New("no such short")
-	}
-	return
-}
-
-type mockTargetRepo struct {
-	data   map[TargetURL]ShortURL
-	create bool
-}
-
-func (tr mockTargetRepo) GetShort(tu TargetURL) (su ShortURL, isNew bool, err error) {
-	su, ok := tr.data[tu]
-	if ok {
-		return
-	}
-	if tr.create {
-		su = ShortURL{URL: tu.URL}
-		tr.data[tu] = su
-		isNew = true
-	} else {
-		err = errors.New("short not found and not created")
-	}
-
-	return
-}
-
-func makeMockTargetRepo(create bool) mockTargetRepo {
-	r := mockTargetRepo{
-		data:   make(map[TargetURL]ShortURL),
-		create: create,
-	}
-	return r
-}
-
 func TestGetTargetURL(t *testing.T) {
 	// Empty repos should not find any short.
-	RegisterRepositories(make(mockShortRepo), nil)
+	RegisterRepositories(make(MockShortRepo), nil)
 	_, err := GetTargetURL("")
 	if err == nil {
 		t.Error("empty repository should fail to get a target")
 	}
 
-	var mockSR mockShortRepo = make(map[ShortURL]TargetURL)
+	var mockSR MockShortRepo = make(map[ShortURL]TargetURL)
 	shortURL := "some_short"
 	expected := TargetURL{URL: URL("some target")}
 	mockSR[ShortURL{URL: URL(shortURL)}] = expected
@@ -83,7 +43,7 @@ func TestGetShortURL(t *testing.T) {
 	}
 
 	// Empty repos unable to create shorts should not find any short.
-	RegisterRepositories(nil, makeMockTargetRepo(false))
+	RegisterRepositories(nil, MakeMockTargetRepo(false))
 	_, isNew, err := GetShortURL("some_target")
 	if err == nil {
 		t.Error("empty repository should fail to get a short")
@@ -93,7 +53,7 @@ func TestGetShortURL(t *testing.T) {
 	}
 
 	// Empty repos able to create shorts should return a short for a valid target.
-	RegisterRepositories(nil, makeMockTargetRepo(true))
+	RegisterRepositories(nil, MakeMockTargetRepo(true))
 	const targetURL = "some_target"
 	actual, isNew, err := GetShortURL(targetURL)
 	if err != nil {

+ 40 - 0
domain/domain_api_testing.go

@@ -0,0 +1,40 @@
+package domain
+
+type MockShortRepo map[ShortURL]TargetURL
+
+func (sr MockShortRepo) GetTarget(su ShortURL) (tu TargetURL, err error) {
+	tu, ok := sr[su]
+	if !ok {
+		err = MakeError(ShortNotFound, "")
+	}
+	return
+}
+
+type MockTargetRepo struct {
+	data   map[TargetURL]ShortURL
+	create bool
+}
+
+func (tr MockTargetRepo) GetShort(tu TargetURL) (su ShortURL, isNew bool, err error) {
+	su, ok := tr.data[tu]
+	if ok {
+		return
+	}
+	if tr.create {
+		su = ShortURL{URL: tu.URL}
+		tr.data[tu] = su
+		isNew = true
+	} else {
+		err = MakeError(ShortNotCreated, "")
+	}
+
+	return
+}
+
+func MakeMockTargetRepo(create bool) MockTargetRepo {
+	r := MockTargetRepo{
+		data:   make(map[TargetURL]ShortURL),
+		create: create,
+	}
+	return r
+}

+ 3 - 1
domain/errors.go

@@ -11,6 +11,7 @@ const (
 	NoError ErrorKind = iota
 	Unimplemented
 
+	ShortNotCreated
 	ShortNotFound
 
 	TargetBlockedError
@@ -26,7 +27,8 @@ var Errors = map[ErrorKind]string{
 	NoError:       "No error",
 	Unimplemented: "Not yet implemented",
 
-	ShortNotFound: "Short URL not defined",
+	ShortNotCreated: "Short URL not created for a new target",
+	ShortNotFound:   "Short URL not defined",
 
 	TargetBlockedError:  "Target blocked",
 	TargetCensoredError: "Target unavailable for legal reasons",

+ 2 - 0
go.mod

@@ -3,6 +3,8 @@ module code.osinet.fr/fgm/kurz
 require (
 	github.com/BurntSushi/toml v0.3.1 // indirect
 	github.com/go-sql-driver/mysql v1.4.0
+	github.com/gorilla/context v1.1.1 // indirect
+	github.com/gorilla/mux v1.6.2
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/pressly/goose v2.4.3+incompatible

+ 4 - 0
go.sum

@@ -7,6 +7,10 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
 github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
+github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=