Explorar o código

Web API: POST OK at 72% coverage.

Frederic G. MARAND %!s(int64=5) %!d(string=hai) anos
pai
achega
64198fec01
Modificáronse 6 ficheiros con 162 adicións e 26 borrados
  1. 11 0
      api/api_testing.go
  2. 5 3
      api/doc.go
  3. 3 12
      api/get_test.go
  4. 67 2
      api/post.go
  5. 72 5
      api/post_test.go
  6. 4 4
      domain/domain_api_testing.go

+ 11 - 0
api/api_testing.go

@@ -0,0 +1,11 @@
+package api
+
+import "net/http"
+
+const sampleShort = "short"
+const sampleTarget = "http://example.com/"
+
+// Use the special case of ErrUseLastResponse to ignore redirects.
+func doNotFollowRedirects(_ *http.Request, _ []*http.Request) error {
+	return http.ErrUseLastResponse
+}

+ 5 - 3
api/doc.go

@@ -2,13 +2,15 @@
 The Kurz Web API exposes these routes:
 
   - GET "/<short>" : resolve a short URL
-	- Handler: HandleGetShort()
+    - Handler: HandleGetShort()
     - Success: 307 to matching target URL
-    - Short not yet defined: 404 no matching target
     - Client request incorrect: 400
+    - Short not yet defined: 404 no matching target
+    - Target blocked for permission reasons: 403
+    - Target legally censored: 451
     - Server failure: 50*
   - POST "/" with <target>: create a short URL from a target URL
-	- Handler: HandlePostTarget()
+    - Handler: HandlePostTarget()
     - Success: 201 with <new short>
     - Already existing short: 409 with <existing short>
     - Target blocked for permission reasons: 403

+ 3 - 12
api/get_test.go

@@ -10,13 +10,6 @@ import (
 	"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)
@@ -29,17 +22,15 @@ func setupGet() (*httptest.Server, *http.Client) {
 }
 
 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}
+	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)
+	res, err := c.Get(ts.URL + "/" + sampleShort)
 
 	if err != nil {
 		t.Error("Getting an existing short URL should not fail")
@@ -59,7 +50,7 @@ func TestHandleGetShortSadNotFound(t *testing.T) {
 	ts, c := setupGet()
 	defer ts.Close()
 
-	res, err := c.Get(ts.URL + "/" + SampleShort)
+	res, err := c.Get(ts.URL + "/" + sampleShort)
 
 	if err != nil {
 		t.Error("Getting an nonexistent short URL should not fail")

+ 67 - 2
api/post.go

@@ -1,10 +1,75 @@
 package api
 
 import (
+	"encoding/json"
+	"io/ioutil"
 	"net/http"
+
+	"code.osinet.fr/fgm/kurz/domain"
 )
 
-// HandlePostTarget() handles POST /(target).
+const postContentType = "application/json"
+
+func jsonFromString(s string) []byte {
+	j, err := json.Marshal([]byte(s))
+	if err != nil {
+		panic(err)
+	}
+	return j
+}
+
+// HandlePostTarget() handles "POST /" with { "target": "some target" }.
 func HandlePostTarget(w http.ResponseWriter, r *http.Request) {
-	w.WriteHeader(http.StatusNotImplemented)
+	payload, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		w.WriteHeader(http.StatusBadRequest)
+		w.Write(jsonFromString(`{ error: "Incomplete request body"}`))
+		return
+	}
+	wrapper := struct{ Target string }{}
+	err = json.Unmarshal(payload, &wrapper)
+	if err != nil {
+		w.WriteHeader(http.StatusBadRequest)
+		w.Write(jsonFromString(`{ error: "Invalid JSON request"}`))
+		return
+	}
+
+	su, isNew, err := domain.GetShortURL(wrapper.Target)
+
+	w.Header().Set("Content-type", postContentType)
+
+	if err != nil {
+		domainErr, ok := err.(domain.Error)
+		if ok {
+			switch domainErr.Kind {
+			case domain.TargetInvalidError:
+				w.WriteHeader(http.StatusBadRequest)
+				w.Write(jsonFromString(`{ error: "Invalid target requested"}`))
+
+			// Covers all the domain.Storage* error cases too.
+			default:
+				w.WriteHeader(http.StatusInternalServerError)
+			}
+		} else {
+			w.WriteHeader(http.StatusInternalServerError)
+		}
+
+		return
+	}
+
+	payload, err = json.Marshal(su)
+	if err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write(jsonFromString(`{ error: "Short URL serialization error"}`))
+		return
+	}
+
+	if isNew {
+		w.WriteHeader(http.StatusCreated)
+	} else {
+		w.WriteHeader(http.StatusConflict)
+	}
+
+	w.Write(payload)
+	return
 }

+ 72 - 5
api/post_test.go

@@ -1,23 +1,90 @@
 package api
 
 import (
+	"bytes"
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/http/httptest"
-	"strings"
 	"testing"
+
+	"code.osinet.fr/fgm/kurz/domain"
 )
 
-func TestHandlePostTarget(t *testing.T) {
+func setupPost(seed bool) (*httptest.Server, *http.Client) {
+	tr := domain.MakeMockTargetRepo(!seed)
+	if seed {
+		tr.Data[domain.TargetURL{URL: domain.URL(sampleTarget)}] = domain.ShortURL{URL: domain.URL(sampleShort)}
+	}
+	domain.RegisterRepositories(domain.MockShortRepo{}, tr)
+
 	ts := httptest.NewServer(http.HandlerFunc(HandlePostTarget))
+
+	c := ts.Client()
+	c.CheckRedirect = doNotFollowRedirects
+
+	return ts, c
+}
+
+func TestHandlePostTargetHappy(t *testing.T) {
+	ts, c := setupPost(false)
+	defer ts.Close()
+
+	target, err := json.Marshal(map[string]string{"target": sampleTarget})
+	if err != nil {
+		t.Log(err)
+		t.FailNow()
+	}
+
+	// Submitting a new valid target should succeed with 201 and return new short
+	res, err := c.Post(ts.URL, postContentType, bytes.NewReader(target))
+	if err != nil {
+		t.Log(err)
+		t.FailNow()
+	}
+	if res.StatusCode != http.StatusCreated {
+		t.Log("Creation of new short for valid target should succeed")
+		t.FailNow()
+	}
+
+	// Submitting an existing target should fail with 409 and return existing short
+	res, err = c.Post(ts.URL, postContentType, bytes.NewReader(target))
+	if err != nil {
+		t.Log(err)
+		t.FailNow()
+	}
+	if res.StatusCode != http.StatusConflict {
+		t.Error("Re-creation of existing short should conflict")
+	}
+}
+
+func TestHandlePostTargetSad(t *testing.T) {
+	ts, c := setupPost(true)
 	defer ts.Close()
 
-	res, err := http.Post(ts.URL, "application/json", strings.NewReader(""))
+	target, err := json.Marshal(map[string]string{"target": sampleTarget + "bis"})
+	if err != nil {
+		fmt.Println(err)
+		t.FailNow()
+	}
+
+	// Submitting an invalid target should fail with 400
+	res, err := c.Post(ts.URL, postContentType, nil)
+	if err != nil {
+		fmt.Println(err)
+		t.FailNow()
+	}
+	if res.StatusCode != http.StatusBadRequest {
+		t.Error("Creation of short for empty target should be a bad request")
+	}
+
+	// Submitting a new valid target should fail with 50x since mock repo is set to no creation.
+	res, err = c.Post(ts.URL, postContentType, bytes.NewReader(target))
 	if err != nil {
 		fmt.Println(err)
 		t.FailNow()
 	}
-	if res.StatusCode != http.StatusNotImplemented {
-		t.Error("Post should return a 'not implemented' error")
+	if res.StatusCode != http.StatusInternalServerError {
+		t.Error("Creation of new short for valid target should fail since repository cannot create")
 	}
 }

+ 4 - 4
domain/domain_api_testing.go

@@ -11,18 +11,18 @@ func (sr MockShortRepo) GetTarget(su ShortURL) (tu TargetURL, err error) {
 }
 
 type MockTargetRepo struct {
-	data   map[TargetURL]ShortURL
+	Data   map[TargetURL]ShortURL
 	create bool
 }
 
 func (tr MockTargetRepo) GetShort(tu TargetURL) (su ShortURL, isNew bool, err error) {
-	su, ok := tr.data[tu]
+	su, ok := tr.Data[tu]
 	if ok {
 		return
 	}
 	if tr.create {
 		su = ShortURL{URL: tu.URL}
-		tr.data[tu] = su
+		tr.Data[tu] = su
 		isNew = true
 	} else {
 		err = MakeError(ShortNotCreated, "")
@@ -33,7 +33,7 @@ func (tr MockTargetRepo) GetShort(tu TargetURL) (su ShortURL, isNew bool, err er
 
 func MakeMockTargetRepo(create bool) MockTargetRepo {
 	r := MockTargetRepo{
-		data:   make(map[TargetURL]ShortURL),
+		Data:   make(map[TargetURL]ShortURL),
 		create: create,
 	}
 	return r