From 19141a78a049f3759ad5af1057d511b00a3801fc Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 23 Nov 2017 16:26:59 +0100 Subject: [PATCH] Added unit tested and increased so the code coverage: fix #9 --- .gitignore | 1 + handlers/public.go | 10 +++++-- handlers/public_test.go | 38 ++++++++++++++++++++++++++ handlers/test.yaml | 9 +++++++ main_test.go | 21 +++++++++++++++ store/store.go | 15 ++++------- store/store_test.go | 59 +++++++++++++++++++++++++++++++++++++++++ store/util.go | 11 ++++---- util/config_test.go | 15 +++++++++++ util/private_test.go | 20 ++++++++++++++ util/test.yaml | 5 ++++ 11 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 main_test.go create mode 100644 util/config_test.go create mode 100644 util/private_test.go create mode 100644 util/test.yaml diff --git a/.gitignore b/.gitignore index 3c2c62a..ca9fdc3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ debug +debug.test *.db *.lock diff --git a/handlers/public.go b/handlers/public.go index 2043848..41e8ee9 100644 --- a/handlers/public.go +++ b/handlers/public.go @@ -1,6 +1,7 @@ package handlers import ( + "encoding/base64" "fmt" "net/http" "net/url" @@ -82,7 +83,7 @@ func (h *Handler) handleCreate(c *gin.Context) { originURL := h.getURLOrigin(c) c.JSON(http.StatusOK, urlUtil{ URL: fmt.Sprintf("%s/%s", originURL, id), - DeletionURL: fmt.Sprintf("%s/d/%s/%s", originURL, id, url.QueryEscape(delID)), + DeletionURL: fmt.Sprintf("%s/d/%s/%s", originURL, id, url.QueryEscape(base64.RawURLEncoding.EncodeToString(delID))), }) } @@ -97,7 +98,12 @@ func (h *Handler) handleInfo(c *gin.Context) { c.JSON(http.StatusOK, info) } func (h *Handler) handleDelete(c *gin.Context) { - if err := h.store.DeleteEntry(c.Param("id"), c.Param("hash")); err != nil { + givenHmac, err := base64.RawURLEncoding.DecodeString(c.Param("hash")) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not decode base64: %v", err)}) + return + } + if err := h.store.DeleteEntry(c.Param("id"), givenHmac); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } diff --git a/handlers/public_test.go b/handlers/public_test.go index 98584cf..1d3a72e 100644 --- a/handlers/public_test.go +++ b/handlers/public_test.go @@ -239,6 +239,44 @@ func testRedirect(t *testing.T, shortURL, longURL string) { } } +func TestHandleApplicationInfo(t *testing.T) { + resp, err := http.Get(server.URL + "/api/v1/info") + if err != nil { + t.Fatalf("could not get application info: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Errorf("expected status %d; got %d", http.StatusOK, resp.StatusCode) + } +} + +func TestHandleDeletion(t *testing.T) { + reqBody, err := json.Marshal(gin.H{ + "URL": testURL, + }) + if err != nil { + t.Fatalf("could not marshal json: %v", err) + } + respBody := createEntryWithJSON(t, reqBody, "application/json; charset=utf-8", http.StatusOK) + var body urlUtil + if err := json.Unmarshal(respBody, &body); err != nil { + t.Fatal("could not unmarshal create response") + } + resp, err := http.Get(body.DeletionURL) + if err != nil { + t.Fatalf("could not send deletion http request") + } + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected status: %d; got: %d", resp.StatusCode, http.StatusOK) + } + resp, err = http.Get(body.URL) + if err != nil { + t.Fatalf("could not send visit request: %v", err) + } + if resp.StatusCode != http.StatusNotFound { + t.Fatalf("expected status: %d; got: %d", http.StatusNotFound, resp.StatusCode) + } +} + func TestCloseB(t *testing.T) { TestCloseBackend(t) } diff --git a/handlers/test.yaml b/handlers/test.yaml index a8b8cdf..2e65e5d 100644 --- a/handlers/test.yaml +++ b/handlers/test.yaml @@ -1,3 +1,12 @@ data_dir: ../data enable_debug_mode: true shorted_id_length: 4 +Google: + ClientID: so + ClientSecret: secret +GitHub: + ClientID: so + ClientSecret: secret +Microsoft: + ClientID: so + ClientSecret: secret diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..1e8b263 --- /dev/null +++ b/main_test.go @@ -0,0 +1,21 @@ +package main + +import ( + "net/http" + "testing" + "time" + + "github.com/spf13/viper" +) + +func TestInitShortener(t *testing.T) { + close, err := initShortener() + if err != nil { + t.Fatalf("could not init shortener: %v", err) + } + time.Sleep(1) // Give the http server a second to boot up + if err := http.ListenAndServe(viper.GetString("listen_addr"), nil); err == nil { + t.Fatal("port is not in use") + } + close() +} diff --git a/store/store.go b/store/store.go index ae3bfe0..c9a2af6 100644 --- a/store/store.go +++ b/store/store.go @@ -4,7 +4,6 @@ package store import ( "crypto/hmac" "crypto/sha512" - "encoding/base64" "encoding/json" "path/filepath" "time" @@ -135,34 +134,30 @@ func (s *Store) GetEntryByIDRaw(id string) ([]byte, error) { } // CreateEntry creates a new record and returns his short id -func (s *Store) CreateEntry(entry Entry, givenID string) (string, string, error) { +func (s *Store) CreateEntry(entry Entry, givenID string) (string, []byte, error) { if !govalidator.IsURL(entry.Public.URL) { - return "", "", ErrNoValidURL + return "", nil, ErrNoValidURL } // try it 10 times to make a short URL for i := 1; i <= 10; i++ { id, delID, err := s.createEntry(entry, givenID) if err != nil && givenID != "" { - return "", "", err + return "", nil, err } else if err != nil { logrus.Debugf("Could not create entry: %v", err) continue } return id, delID, nil } - return "", "", ErrGeneratingIDFailed + return "", nil, ErrGeneratingIDFailed } // DeleteEntry deletes an Entry fully from the DB -func (s *Store) DeleteEntry(id, hash string) error { +func (s *Store) DeleteEntry(id string, givenHmac []byte) error { mac := hmac.New(sha512.New, util.GetPrivateKey()) if _, err := mac.Write([]byte(id)); err != nil { return errors.Wrap(err, "could not write hmac") } - givenHmac, err := base64.RawURLEncoding.DecodeString(hash) - if err != nil { - return errors.Wrap(err, "could not decode base64") - } if !hmac.Equal(mac.Sum(nil), givenHmac) { return errors.New("hmac verification failed") } diff --git a/store/store_test.go b/store/store_test.go index 60eba15..bd9e7f2 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -120,6 +120,65 @@ func TestIncreaseVisitCounter(t *testing.T) { } } +func TestDelete(t *testing.T) { + viper.Set("shorted_id_length", 4) + store, err := New() + if err != nil { + t.Fatalf("could not create store: %v", err) + } + defer cleanup(store) + entryID, delHMac, err := store.CreateEntry(Entry{ + Public: EntryPublicData{ + URL: "https://golang.org/", + }, + }, "") + if err != nil { + t.Fatalf("could not create entry: %v", err) + } + if err := store.DeleteEntry(entryID, delHMac); err != nil { + t.Fatalf("could not delete entry: %v", err) + } + if _, err := store.GetEntryByID(entryID); err != ErrNoEntryFound { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestGetURLAndIncrease(t *testing.T) { + viper.Set("shorted_id_length", 4) + store, err := New() + if err != nil { + t.Fatalf("could not create store: %v", err) + } + defer cleanup(store) + const url = "https://golang.org/" + entryID, _, err := store.CreateEntry(Entry{ + Public: EntryPublicData{ + URL: url, + }, + }, "") + if err != nil { + t.Fatalf("could not create entry: %v", err) + } + entryOne, err := store.GetEntryByID(entryID) + if err != nil { + t.Fatalf("could not get entry: %v", err) + } + entryURL, err := store.GetURLAndIncrease(entryID) + if err != nil { + t.Fatalf("could not get URL and increase the visitor counter: %v", err) + } + if entryURL != url { + t.Fatalf("url is not the expected one") + } + entryTwo, err := store.GetEntryByID(entryID) + if err != nil { + t.Fatalf("could not get entry: %v", err) + } + if entryOne.Public.VisitCount+1 != entryTwo.Public.VisitCount { + t.Fatalf("visitor count does not increase") + } +} + func cleanup(s *Store) { s.Close() os.Remove(testingDBName) diff --git a/store/util.go b/store/util.go index 3e18305..b909f13 100644 --- a/store/util.go +++ b/store/util.go @@ -4,7 +4,6 @@ import ( "crypto/hmac" "crypto/rand" "crypto/sha512" - "encoding/base64" "encoding/json" "math/big" "time" @@ -31,23 +30,23 @@ func (s *Store) createEntryRaw(key, value []byte) error { // createEntry creates a new entry with a randomly generated id. If on is present // then the given ID is used -func (s *Store) createEntry(entry Entry, entryID string) (string, string, error) { +func (s *Store) createEntry(entry Entry, entryID string) (string, []byte, error) { var err error if entryID == "" { if entryID, err = generateRandomString(s.idLength); err != nil { - return "", "", errors.Wrap(err, "could not generate random string") + return "", nil, errors.Wrap(err, "could not generate random string") } } entry.Public.CreatedOn = time.Now() rawEntry, err := json.Marshal(entry) if err != nil { - return "", "", err + return "", nil, err } mac := hmac.New(sha512.New, util.GetPrivateKey()) if _, err := mac.Write([]byte(entryID)); err != nil { - return "", "", errors.Wrap(err, "could not write hmac") + return "", nil, errors.Wrap(err, "could not write hmac") } - return entryID, base64.RawURLEncoding.EncodeToString(mac.Sum(nil)), s.createEntryRaw([]byte(entryID), rawEntry) + return entryID, mac.Sum(nil), s.createEntryRaw([]byte(entryID), rawEntry) } // generateRandomString generates a random string with an predefined length diff --git a/util/config_test.go b/util/config_test.go new file mode 100644 index 0000000..83e607f --- /dev/null +++ b/util/config_test.go @@ -0,0 +1,15 @@ +package util + +import ( + "testing" + + "github.com/spf13/viper" +) + +func TestReadInConfig(t *testing.T) { + DoNotSetConfigName = true + viper.SetConfigFile("test.yaml") + if err := ReadInConfig(); err != nil { + t.Fatalf("could not read in config file: %v", err) + } +} diff --git a/util/private_test.go b/util/private_test.go new file mode 100644 index 0000000..8ad134f --- /dev/null +++ b/util/private_test.go @@ -0,0 +1,20 @@ +package util + +import ( + "os" + "testing" +) + +func TestCheckforPrivateKey(t *testing.T) { + TestReadInConfig(t) + privateKey = nil + if err := CheckForPrivateKey(); err != nil { + t.Fatalf("could not check for private key: %v", err) + } + if GetPrivateKey() == nil { + t.Fatalf("private key is nil") + } + if err := os.RemoveAll(GetDataDir()); err != nil { + t.Fatalf("could not remove data dir: %v", err) + } +} diff --git a/util/test.yaml b/util/test.yaml new file mode 100644 index 0000000..2917c2c --- /dev/null +++ b/util/test.yaml @@ -0,0 +1,5 @@ +listen_addr: ':8080' +base_url: 'http://localhost:3000' +data_dir: ./data +enable_debug_mode: true +shorted_id_length: 4 \ No newline at end of file