Browse Source

Finished the initial url shortener

:D
tags/v1.0.0^2
Néfix Estrada 1 year ago
parent
commit
93642635ef
9 changed files with 1223 additions and 0 deletions
  1. +48
    -0
      Dockerfile
  2. +20
    -0
      Makefile
  3. +36
    -0
      cmd/urlshortener/main.go
  4. +80
    -0
      pkg/db/db.go
  5. +403
    -0
      pkg/db/db_test.go
  6. +75
    -0
      pkg/handler/handler.go
  7. +159
    -0
      pkg/handler/handler_internal_test.go
  8. +272
    -0
      pkg/handler/handler_test.go
  9. +130
    -0
      pkg/handler/static/index.html

+ 48
- 0
Dockerfile View File

@@ -0,0 +1,48 @@
#
# URL Shortener, Néfix Estrada, 2018
# https://gitea.nefixestrada.com/nefix/urlshortener
#

#
# Build stage
#

# Use golang 1.11 as stage stage
FROM golang:1.11 as build

# Download the URL Shortener
RUN go get -d -v gitea.nefixestrada.com/nefix/urlshortener

# Move to the correct directory
WORKDIR /go/src/gitea.nefixestrada.com/nefix/urlshortener

# Download all the dependencies
RUN go get -d -v

# Compile the binary
RUN make

# Create the user
RUN adduser -D -g '' app

#
# Base stage
#

# Use alpine 3.8 as base
FROM alpine:3.8

# Copy the /etc/passwd (which contains the user 'app') from the build stage
COPY --from=build /etc/passwd /etc/passwd

# Copy the compiled binary from the build stage
COPY --from=build /go/src/gitea.nefixestrada.com/nefix/urlshortener/urlshortener /srv

# Use the 'app' user
USER app

# Expose the required port
EXPOSE 3000

# Run the service
CMD [ "/srv/urlshortener" ]

+ 20
- 0
Makefile View File

@@ -0,0 +1,20 @@
.PHONY: build
build: rice test
# Incrustate the static files
cd pkg/handler ; rice embed-go
go build -a -ldflags "-s -w" -o urlshortener cmd/urlshortener/main.go

.PHONY: rice
go get github.com/GeertJohan/go.rice/rice

.PHONY: test
test: lint
go test ./...

.PHONY: lint
lint: $(GOMETALINTER)
gometalinter ./...

$(GOMETALINTER):
go get github.com/alecthomas/gometalinter
gometalinter --install &> /dev/null

+ 36
- 0
cmd/urlshortener/main.go View File

@@ -0,0 +1,36 @@
package main

import (
"log"
"net/http"

bolt "go.etcd.io/bbolt"

"gitea.nefixestrada.com/nefix/urlshortener/pkg/db"
"gitea.nefixestrada.com/nefix/urlshortener/pkg/handler"
)

func main() {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
log.Fatalf("error opening the DB: %v", err)
}
defer func() {
if err = boltDB.Close(); err != nil {
log.Fatalf("error closing the DB connection: %v", err)
}
}()

db := &db.DB{
DB: boltDB,
}

if err := db.Initialize(); err != nil {
log.Fatalf("error initializing the DB: %v", err)
}

log.Println("Starting to listen at port :3000")
if err := http.ListenAndServe(":3000", handler.Default(db)); err != nil {
log.Fatalf("error listening: %v", err)
}
}

+ 80
- 0
pkg/db/db.go View File

@@ -0,0 +1,80 @@
package db

import (
"errors"

"github.com/asaskevich/govalidator"

bolt "go.etcd.io/bbolt"
)

// DB is the struct that contains the connection with the Bold DB
type DB struct {
DB *bolt.DB
}

// ReadURL reads a shortened URL from the DB and returns the target URL for it
func (d *DB) ReadURL(shortURL string) (fullURL string, err error) {
if err := d.DB.View(func(tx *bolt.Tx) error {
fullURL = string(tx.Bucket([]byte("urls")).Get([]byte(shortURL)))

if fullURL == "" {
return errors.New("the shortened URL wasn't found in the DB")
}

return nil
}); err != nil {
return "", err
}

return fullURL, nil
}

// AddURL adds a new URL to the DB
func (d *DB) AddURL(shortURL string, longURL string) error {
if shortURL == "" {
return errors.New("the short URL can't be empty")
}

if longURL == "" {
return errors.New("the long URL can't be empty")
}

if !govalidator.IsURL(longURL) {
return errors.New("the long URL needs to be a valid URL")
}

if err := d.DB.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("urls"))
if b == nil {
return errors.New("the bucket urls doesn't exist")
}

if content := b.Get([]byte(shortURL)); content != nil {
return errors.New("there's already an shortened URL with that URL")
}

if err := b.Put([]byte(shortURL), []byte(longURL)); err != nil {
return err
}

return nil
}); err != nil {
return err
}

return nil
}

// Initialize creates the required bucket
func (d *DB) Initialize() error {
if err := d.DB.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte("urls"))

return err
}); err != nil {
return err
}

return nil
}

+ 403
- 0
pkg/db/db_test.go View File

@@ -0,0 +1,403 @@
package db_test

import (
"os"
"testing"

"gitea.nefixestrada.com/nefix/urlshortener/pkg/db"

bolt "go.etcd.io/bbolt"
)

var tests = []struct {
shortURL string
longURL string
}{
{
shortURL: "git",
longURL: "https://gitea.nefixestrada.com",
},
}

// Should work as expected
func TestReadURL(t *testing.T) {
for _, tt := range tests {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

if err = boltDB.Update(func(tx *bolt.Tx) error {
var b *bolt.Bucket
b, err = tx.CreateBucket([]byte("urls"))
if err != nil {
return err
}

if err = b.Put([]byte(tt.shortURL), []byte(tt.longURL)); err != nil {
return err
}

return nil
}); err != nil {
t.Fatalf("error inserting test data to the DB: %v", err)
}

db := db.DB{
DB: boltDB,
}

rsp, err := db.ReadURL(tt.shortURL)
if err != nil {
t.Errorf("unexpected error when reading the URL in the DB: %v", err)
}

if rsp != tt.longURL {
t.Errorf("expecting %s, but got %s", tt.shortURL, tt.longURL)
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}
}

// Should return a not found error
func TestReadURLNotFound(t *testing.T) {
for _, tt := range tests {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

if err = boltDB.Update(func(tx *bolt.Tx) error {
_, err = tx.CreateBucket([]byte("urls"))
if err != nil {
return err
}

return nil
}); err != nil {
t.Fatalf("error inserting test data to the DB: %v", err)
}

db := db.DB{
DB: boltDB,
}

expectedErr := "the shortened URL wasn't found in the DB"

rsp, err := db.ReadURL(tt.shortURL)
if err.Error() != expectedErr {
t.Errorf("expecting %s, but got %v", expectedErr, err)
}

if rsp != "" {
t.Errorf("expecting %s, but got %s", "", rsp)
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}
}

// Should work as expected
func TestAddURL(t *testing.T) {
for _, tt := range tests {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

if err = boltDB.Update(func(tx *bolt.Tx) error {
_, err = tx.CreateBucket([]byte("urls"))
if err != nil {
return err
}

return nil
}); err != nil {
t.Fatalf("error inserting test data to the DB: %v", err)
}

db := db.DB{
DB: boltDB,
}

err = db.AddURL(tt.shortURL, tt.longURL)
if err != nil {
t.Errorf("unexpected error adding the URL: %v", err)
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}
}

// The short URL can't be empty
func TestAddURLShortNoEmpty(t *testing.T) {
for _, tt := range tests {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

if err = boltDB.Update(func(tx *bolt.Tx) error {
_, err = tx.CreateBucket([]byte("urls"))
if err != nil {
return err
}

return nil
}); err != nil {
t.Fatalf("error inserting test data to the DB: %v", err)
}

db := db.DB{
DB: boltDB,
}

expectedErr := "the short URL can't be empty"

err = db.AddURL("", tt.longURL)
if err.Error() != expectedErr {
t.Errorf("expecting %s, but got %v", expectedErr, err)
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}
}

// The long URL can't be empty
func TestAddURLLongNoEmpty(t *testing.T) {
for _, tt := range tests {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

if err = boltDB.Update(func(tx *bolt.Tx) error {
_, err = tx.CreateBucket([]byte("urls"))
if err != nil {
return err
}

return nil
}); err != nil {
t.Fatalf("error inserting test data to the DB: %v", err)
}

db := db.DB{
DB: boltDB,
}

expectedErr := "the long URL can't be empty"

err = db.AddURL(tt.shortURL, "")
if err.Error() != expectedErr {
t.Errorf("expecting %s, but got %v", expectedErr, err)
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}
}

// The long URL needs to be an URL
func TestAddURLLongIsURL(t *testing.T) {
for _, tt := range tests {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

if err = boltDB.Update(func(tx *bolt.Tx) error {
_, err = tx.CreateBucket([]byte("urls"))
if err != nil {
return err
}

return nil
}); err != nil {
t.Fatalf("error inserting test data to the DB: %v", err)
}

db := db.DB{
DB: boltDB,
}

expectedErr := "the long URL needs to be a valid URL"

err = db.AddURL(tt.shortURL, "https://notanurl!")
if err.Error() != expectedErr {
t.Errorf("expecting %s, but got %v", expectedErr, err)
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}
}

// The bucket 'urls' doesn't exist
func TestAddURLBucketNotExist(t *testing.T) {
for _, tt := range tests {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

db := db.DB{
DB: boltDB,
}

expectedErr := "the bucket urls doesn't exist"

err = db.AddURL(tt.shortURL, tt.longURL)
if err.Error() != expectedErr {
t.Errorf("expecting %s, but got %v", expectedErr, err)
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}
}

// The shortened URL already exists
func TestAddURLAlreadyExists(t *testing.T) {
for _, tt := range tests {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

if err = boltDB.Update(func(tx *bolt.Tx) error {
var b *bolt.Bucket
b, err = tx.CreateBucket([]byte("urls"))
if err != nil {
return err
}

if err = b.Put([]byte(tt.shortURL), []byte(tt.longURL)); err != nil {
return err
}

return nil
}); err != nil {
t.Fatalf("error inserting test data to the DB: %v", err)
}

db := db.DB{
DB: boltDB,
}

expectedErr := "there's already an shortened URL with that URL"

err = db.AddURL(tt.shortURL, tt.longURL)
if err.Error() != expectedErr {
t.Errorf("expecting %s, but got %v", expectedErr, err)
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}
}

// There should be an error when inserting the URL in the DB
func TestAddURLErrInserting(t *testing.T) {
for _, tt := range tests {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

if err = boltDB.Update(func(tx *bolt.Tx) error {
_, err = tx.CreateBucket([]byte("urls"))
if err != nil {
return err
}

return nil
}); err != nil {
t.Fatalf("error inserting test data to the DB: %v", err)
}

db := db.DB{
DB: boltDB,
}

expectedErr := "key too large"

longKey := make([]byte, bolt.MaxKeySize+1)

err = db.AddURL(string(longKey), tt.longURL)
if err.Error() != expectedErr {
t.Errorf("expecting %s, but got %v", expectedErr, err)
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}
}

// Should work as expected
func TestInitialize(t *testing.T) {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

db := db.DB{
DB: boltDB,
}

err = db.Initialize()
if err != nil {
t.Errorf("unexpected error initializing the DB: %v", err)
}

if err = boltDB.Update(func(tx *bolt.Tx) error {
_, err = tx.CreateBucket([]byte("urls"))

return err
}); err != bolt.ErrBucketExists {
t.Errorf("expecting %v, but got %v", bolt.ErrBucketExists, err)
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}

// There should be an error creating the bucket
func TestInitializeErrBucket(t *testing.T) {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

db := db.DB{
DB: boltDB,
}

db.DB.Close()

expectedErr := "database not open"

err = db.Initialize()
if err.Error() != expectedErr {
t.Errorf("expecting %s, but got %v", expectedErr, err)
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}

+ 75
- 0
pkg/handler/handler.go View File

@@ -0,0 +1,75 @@
package handler

import (
"fmt"
"io"
"log"
"net/http"
"strings"

"github.com/GeertJohan/go.rice"

"gitea.nefixestrada.com/nefix/urlshortener/pkg/db"
)

// Default is the default handler. It searches for the URL and if it doesn't exist or there's an error, it redirects to
// the main page
func Default(db *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path[1:]

if path == "" {
if r.Method == http.MethodPost {
addURL(db, w, r)
return
}

mainPage(w)
return
}

var toURL string
var err error
if toURL, err = db.ReadURL(path); err != nil {
errorPage(err, w)
return
}

if len(strings.Split(toURL, "://")) == 1 {
toURL = "http://" + toURL
}

http.Redirect(w, r, toURL, http.StatusFound)
}
}

// mainPage renders the main page
func mainPage(w io.Writer) {
if _, err := fmt.Fprint(w, rice.MustFindBox("static").MustString("index.html")); err != nil {
log.Printf("error writting the HTTP response at mainPage: %v", err)
}
}

// errorPage renders an error page with the error provided
func errorPage(err error, w http.ResponseWriter) {
w.WriteHeader(http.StatusBadRequest)
if _, writeErr := fmt.Fprintf(w, "There was an error processing your request: %v\n", err); writeErr != nil {
log.Printf("error writting the HTTP response at errorPage: %v", writeErr)
}
}

// addURL adds a new URL to the DB
func addURL(db *db.DB, w http.ResponseWriter, r *http.Request) {
if err := db.AddURL(r.FormValue("shortURL"), r.FormValue("longURL")); err != nil {
errorPage(err, w)
return
}

url := r.FormValue("longURL")

if len(strings.Split(url, "://")) == 1 {
url = "http://" + url
}

http.Redirect(w, r, url, http.StatusFound)
}

+ 159
- 0
pkg/handler/handler_internal_test.go View File

@@ -0,0 +1,159 @@
package handler

import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"

"gitea.nefixestrada.com/nefix/urlshortener/pkg/db"
bolt "go.etcd.io/bbolt"
)

type mockResponseWriter struct{}

func (mockResponseWriter) Header() http.Header {
return nil
}
func (mockResponseWriter) Write([]byte) (int, error) {
return 0, errors.New("testing error")
}
func (mockResponseWriter) WriteHeader(int) {}

// Should work as expected
func TestMainPage(t *testing.T) {
w := httptest.NewRecorder()

expected, err := ioutil.ReadFile("static/index.html")
if err != nil {
t.Errorf("unexpected error when reading the index.html file: %v", err)
}

mainPage(w)

if w.Code != http.StatusOK {
t.Errorf("expecting %d, but got %d", http.StatusOK, w.Code)
}

if !bytes.Equal(w.Body.Bytes(), expected) {
t.Errorf("expecting %v, but got %v", expected, w.Body.Bytes())
}
}

// Should fail when writting the response
func TestMainPageErr(t *testing.T) {
mainPage(mockResponseWriter{})
}

// Should work as expected
func TestErrorPage(t *testing.T) {
w := httptest.NewRecorder()

expected := []byte("There was an error processing your request: testing error\n")

errorPage(errors.New("testing error"), w)

if w.Code != http.StatusBadRequest {
t.Errorf("expecting %d, but got %d", http.StatusBadRequest, w.Code)
}

if !bytes.Equal(w.Body.Bytes(), expected) {
t.Errorf("expecting %v, but got %v", expected, w.Body.Bytes())
}
}

// Should fail when writting the response
func TestErrorPageErr(t *testing.T) {
errorPage(fmt.Errorf("%b", []byte("a")), mockResponseWriter{})
}

// Should work as expected
func TestAddURL(t *testing.T) {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)

}

db := &db.DB{
DB: boltDB,
}
err = db.Initialize()
if err != nil {
t.Fatalf("error initializing the DB: %v", err)
}

w := httptest.NewRecorder()

form := url.Values{}
form.Add("shortURL", "go")
form.Add("longURL", "https://golang.org")

r, err := http.NewRequest("POST", "/", strings.NewReader(form.Encode()))
if err != nil {
t.Fatalf("error preparing the HTTP request: %v", err)
}

r.Header.Add("Content-Type", "application/x-www-form-urlencoded")

var expected []byte

addURL(db, w, r)

if w.Code != http.StatusFound {
t.Errorf("expecting %d, but got %d", http.StatusFound, w.Code)
}

if !bytes.Equal(w.Body.Bytes(), expected) {
t.Errorf("expecting %s, but got %s", expected, w.Body.Bytes())
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}

// There should be an error when adding the URL to the DB
func TestAddURLErr(t *testing.T) {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)

}

db := &db.DB{
DB: boltDB,
}
err = db.Initialize()
if err != nil {
t.Fatalf("error initializing the DB: %v", err)
}

w := httptest.NewRecorder()
r, err := http.NewRequest("POST", "/", bytes.NewReader([]byte("")))
if err != nil {
t.Fatalf("error preparing the HTTP request: %v", err)
}

expected := []byte("There was an error processing your request: the short URL can't be empty\n")

addURL(db, w, r)

if w.Code != http.StatusBadRequest {
t.Errorf("expecting %d, but got %d", http.StatusBadRequest, w.Code)
}

if !bytes.Equal(w.Body.Bytes(), expected) {
t.Errorf("expecting %s, but got %s", expected, w.Body.Bytes())
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}

+ 272
- 0
pkg/handler/handler_test.go View File

@@ -0,0 +1,272 @@
package handler_test

import (
"bytes"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"

"gitea.nefixestrada.com/nefix/urlshortener/pkg/db"
"gitea.nefixestrada.com/nefix/urlshortener/pkg/handler"
bolt "go.etcd.io/bbolt"
)

// Should return the main page
func TestDefaultHandler(t *testing.T) {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

db := &db.DB{
DB: boltDB,
}

r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("error preparing the test: %v", err)
}

w := httptest.NewRecorder()

expected, err := ioutil.ReadFile("static/index.html")
if err != nil {
t.Errorf("unexpected error when reading the index.html file: %v", err)
}

handler := handler.Default(db)
handler(w, r)

if w.Code != http.StatusOK {
t.Errorf("expecting %d, but got %d", http.StatusOK, w.Code)
}

if !bytes.Equal(w.Body.Bytes(), expected) {
t.Errorf("expecting %s, but got %s", expected, w.Body.Bytes())
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}

// Should redirect to the requested page
func TestDefaultHandlerRedirect(t *testing.T) {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

db := &db.DB{
DB: boltDB,
}
err = db.Initialize()
if err != nil {
t.Fatalf("error initializing the DB: %v", err)
}

if err = boltDB.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("urls"))

return b.Put([]byte("test"), []byte("https://nefixestrada.com"))
}); err != nil {
t.Fatalf("error preparing the test: %v", err)
}

r, err := http.NewRequest("GET", "/test", nil)
if err != nil {
t.Fatalf("error preparing the test: %v", err)
}

w := httptest.NewRecorder()

expected := "https://nefixestrada.com"

handler := handler.Default(db)
handler(w, r)

if w.Code != http.StatusFound {
t.Errorf("expecting %d, but got %d", http.StatusFound, w.Code)
}

if expected != w.Header().Get("Location") {
t.Errorf("expecting %s, but got %s", expected, w.Header().Get("Location"))
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}

// Should append http:// when redirecting to an url that doesn't contain http://
func TestDefaultHandlerRedirectNoHttp(t *testing.T) {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

db := &db.DB{
DB: boltDB,
}
err = db.Initialize()
if err != nil {
t.Fatalf("error initializing the DB: %v", err)
}

if err = boltDB.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("urls"))

return b.Put([]byte("test"), []byte("nefixestrada.com"))
}); err != nil {
t.Fatalf("error preparing the test: %v", err)
}

r, err := http.NewRequest("GET", "/test", nil)
if err != nil {
t.Fatalf("error preparing the test: %v", err)
}

w := httptest.NewRecorder()

expected := "http://nefixestrada.com"

handler := handler.Default(db)
handler(w, r)

if w.Code != http.StatusFound {
t.Errorf("expecting %d, but got %d", http.StatusFound, w.Code)
}

if expected != w.Header().Get("Location") {
t.Errorf("expecting %s, but got %s", expected, w.Header().Get("Location"))
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}

// Should return a not found page
func TestDefaultHandlerNotFound(t *testing.T) {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

db := &db.DB{
DB: boltDB,
}
err = db.Initialize()
if err != nil {
t.Fatalf("error initializing the DB: %v", err)
}

r, err := http.NewRequest("GET", "/test", nil)
if err != nil {
t.Fatalf("error preparing the test: %v", err)
}

w := httptest.NewRecorder()

expected := []byte("There was an error processing your request: the shortened URL wasn't found in the DB\n")

handler := handler.Default(db)
handler(w, r)

if w.Code != http.StatusBadRequest {
t.Errorf("expecting %d, but got %d", http.StatusBadRequest, w.Code)
}

if !bytes.Equal(expected, w.Body.Bytes()) {
t.Errorf("expecting %s, but got %s", expected, w.Body.Bytes())
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}

// Should redirect to the new URL when adding an URL
func TestDefaultHandlerNew(t *testing.T) {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

db := &db.DB{
DB: boltDB,
}
err = db.Initialize()
if err != nil {
t.Fatalf("error initializing the DB: %v", err)
}

r, err := http.NewRequest("POST", "/", strings.NewReader("shortURL=test&longURL=https://nefixestrada.com"))
if err != nil {
t.Fatalf("error preparing the test: %v", err)
}
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")

w := httptest.NewRecorder()

expected := "https://nefixestrada.com"

handler := handler.Default(db)
handler(w, r)

if w.Code != http.StatusFound {
t.Errorf("expecting %d, but got %d", http.StatusFound, w.Code)
}

if expected != w.Header().Get("Location") {
t.Errorf("expecting %s, but got %s", expected, w.Header().Get("Location"))
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}

// Should append http:// when redirecting to an url that doesn't contain http://
func TestDefaultHandlerNewNoHttp(t *testing.T) {
boltDB, err := bolt.Open("urlshortener.db", 0600, nil)
if err != nil {
t.Fatalf("error creating the testing DB: %v", err)
}

db := &db.DB{
DB: boltDB,
}
err = db.Initialize()
if err != nil {
t.Fatalf("error initializing the DB: %v", err)
}

r, err := http.NewRequest("POST", "/", strings.NewReader("shortURL=test&longURL=nefixestrada.com"))
if err != nil {
t.Fatalf("error preparing the test: %v", err)
}
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")

w := httptest.NewRecorder()

expected := "http://nefixestrada.com"

handler := handler.Default(db)
handler(w, r)

if w.Code != http.StatusFound {
t.Errorf("expecting %d, but got %d", http.StatusFound, w.Code)
}

if expected != w.Header().Get("Location") {
t.Errorf("expecting %s, but got %s", expected, w.Header().Get("Location"))
}

if err := os.Remove("urlshortener.db"); err != nil {
t.Fatalf("error finishing the test: %v", err)
}
}

+ 130
- 0
pkg/handler/static/index.html View File

@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Néfix Estrada's URL shortener</title>

<link href="https://fonts.googleapis.com/css?family=Voltaire" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
</head>
<body>
<div class="content">
<h1>Welcome to Néfix Estrada's URL shortener!</h1>
<p>In order to add an URL, fill the following form:</p>

<form id="form" action="/" method="post" autocomplete="off">
<input type="text" name="shortURL" placeholder="/<something>">
<input type="text" name="longURL" placeholder="Redirect to...">
</form>

<button type="submit" form="form">Add...</button>
</div>

<style>
* {
/* Position */
margin: 0;
padding: 0;

/* Visual */
font-family: 'Roboto', sans-serif;
}

.content {
/* Size */
height: 100vh;
width: 100vw;

/* Flex */
display: flex;
flex-flow: column nowrap;
align-items: center;
justify-content: center;
}

h1 {
/* Size */
font-size: 3rem;

/* Position */
margin-bottom: 0.5em;

/* Visual */
font-family: 'Voltaire', sans-serif;
}

p {
/* Size */
font-size: 1.25rem;

/* Position */
margin-bottom: 1.25em;
}

input[type='text'] {
/* Position */
margin: 0.5em;
padding: 0.65em;

/* Visual */
background: transparent;
color: #000;
border: 2px solid #000;
}

button {
/* Size */
width: 125px;

/* Position */
position: relative;
margin-top: 1.25em;
padding: 0.75em;
/* Visual */
font-weight: 700;
color: #000;
background: transparent;
border: 1px solid #000;
cursor: pointer;
overflow: hidden;
transition: 0.3s;
}

button:hover {
/* Visual */
color: #ecface;
border: 1px solid #554d68;
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
}

button::after {
/* Size */
height: 120%;
width: 0;
/* Position */
position: absolute;
left: -10%;
bottom: -1px;
z-index: -1;

/* Visual */
background: #554d68;
content: '';
transition: 0.3s;
transform: skewX(15deg);
}

button:hover::after {
/* Size */
width: 120%;

/* Position */
left: -10%;
}
</style>
</body>
</html>

Loading…
Cancel
Save