Skip to content

gh action #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/lint-github-action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: lint
on: [push]
jobs:
golint:
runs-on: ubuntu-latest
name: lint
steps:
- uses: actions/checkout@v2
- name: lint
id: golint
uses: Jerome1337/go-action/lint@master
- name: lint output
run: echo "${{ steps.golint.outputs.golint-output }}"
2 changes: 1 addition & 1 deletion common/constant.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ const (
ShortCodeLength = 6
)

// ShortCodePattern is regex to check if a string looks like short code
// ShortCodeRegex is regex to check if a string looks like short code
var ShortCodeRegex, _ = regexp.Compile("^[a-zA-Z0-9]{4,12}$")
11 changes: 6 additions & 5 deletions common/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ import "errors"

// Common errors
var (
ErrInvalidUrl = errors.New("url is invalid")
ErrInvalidUrlLen = errors.New("url is too short or too long, should be 7-2048 chars")
ErrBlacklistedUrl = errors.New("url matches blacklist pattern")
ErrInvalidURL = errors.New("url is invalid")
ErrInvalidURLLen = errors.New("url is too short or too long, should be 7-2048 chars")
ErrBlacklistedURL = errors.New("url matches blacklist pattern")
ErrKeywordsCount = errors.New("url must not have more than 10 keywords")
ErrKeywordLength = errors.New("keyword must contain 2-25 characters")
ErrInvalidDate = errors.New("expires_on should be in 'yyyy-mm-dd hh:mm:ss' format")
ErrPastExpiration = errors.New("expires_on can not be date in past")
ErrUrlAlreadyShort = errors.New("the url is already shortened")
ErrURLAlreadyShort = errors.New("the url is already shortened")
ErrNoMatchingData = errors.New("no data matching given criteria found")
ErrTokenRequired = errors.New("auth token is required")
ErrTokenInvalid = errors.New("auth token is invalid")
ErrNoShortCode = errors.New("the given short code is not found")
ErrShortCodeEmpty = errors.New("short_code must not be empty")
ErrNoShortCode = errors.New("the given short_code is not found")
)
12 changes: 6 additions & 6 deletions controller/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import (
"github.com/adhocore/urlsh/service/url"
)

// ListUrls is the controller for url listing endpoint using filters from http.Request
// ListURLs is the controller for url listing endpoint using filters from http.Request
// It responds to `GET /api/admin/urls` and requires auth token.
func ListUrls(res http.ResponseWriter, req *http.Request) {
urls, err := url.ListUrlsFilteredFromRequest(req)
func ListURLs(res http.ResponseWriter, req *http.Request) {
urls, err := url.ListURLsFilteredFromRequest(req)

if err != nil {
response.JSON(res, http.StatusNotFound, response.Body{"message": err.Error(), "urls": urls})
Expand All @@ -20,10 +20,10 @@ func ListUrls(res http.ResponseWriter, req *http.Request) {
response.JSON(res, http.StatusOK, response.Body{"urls": urls})
}

// DeleteShortUrl is the controller for deleting short url
// DeleteShortURL is the controller for deleting short url
// It responds to `DELETE /api/admin/urls` and requires auth token.
func DeleteShortUrl(res http.ResponseWriter, req *http.Request) {
if err := url.DeleteUrlFromRequest(req); err != nil {
func DeleteShortURL(res http.ResponseWriter, req *http.Request) {
if err := url.DeleteURLFromRequest(req); err != nil {
response.JSON(res, http.StatusNotFound, response.Body{"message": err.Error()})
return
}
Expand Down
20 changes: 10 additions & 10 deletions controller/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
"time"
)

func TestListUrl(t *testing.T) {
func TestListURL(t *testing.T) {
t.Run("list endpoint - not found", func(t *testing.T) {
resp := request("GET", "/api/admin/urls?short_code=z", TestBody{}, ListUrls)
resp := request("GET", "/api/admin/urls?short_code=z", TestBody{}, ListURLs)

resp.assertStatus(404, t)
})
Expand All @@ -21,46 +21,46 @@ func TestListUrl(t *testing.T) {
_ = os.Setenv("APP_ALLOW_DUPE_URL", "1")
url := fmt.Sprintf("http://localhost:1000/very/long/url-%v", rand.Intn(1000000))
body := TestBody{"url": url, "expires_on": "2030-01-01 00:00:00", "keywords": []string{"local"}}
resp := request("POST", "/api/urls", body, CreateShortUrl)
resp := request("POST", "/api/urls", body, CreateShortURL)

resp.assertStatus(200, t)

t.Run("by page", func(t *testing.T) {
resp := request("GET", "/api/admin/urls?page=1", TestBody{}, ListUrls)
resp := request("GET", "/api/admin/urls?page=1", TestBody{}, ListURLs)
resp.assertStatus(200, t)
})

t.Run("by keyword", func(t *testing.T) {
resp := request("GET", "/api/admin/urls?keyword=local", TestBody{}, ListUrls)
resp := request("GET", "/api/admin/urls?keyword=local", TestBody{}, ListURLs)
resp.assertStatus(200, t)
})
})
}

func TestDeleteShortUrl(t *testing.T) {
func TestDeleteShortURL(t *testing.T) {
t.Run("delete endpoint", func(t *testing.T) {
rand.Seed(time.Now().UTC().UnixNano())

body := TestBody{"url": fmt.Sprintf("https://localhost/test/delete/short/url/%v", rand.Intn(1000000))}
resp := request("POST", "/api/urls", body, CreateShortUrl)
resp := request("POST", "/api/urls", body, CreateShortURL)
resp.assertStatus(200, t)
resp.assertContains("short_code", t)

shortCode := resp["short_code"]

t.Run("delete - nok", func(t *testing.T) {
uri := fmt.Sprintf("/api/admin/urls?short_code=%v", rand.Intn(1000000))
resp := request("DELETE", uri, TestBody{}, DeleteShortUrl)
resp := request("DELETE", uri, TestBody{}, DeleteShortURL)
resp.assertStatus(404, t)
})

t.Run("delete - ok", func(t *testing.T) {
uri := fmt.Sprintf("/api/admin/urls?short_code=%v", shortCode)
resp := request("DELETE", uri, TestBody{}, DeleteShortUrl)
resp := request("DELETE", uri, TestBody{}, DeleteShortURL)
resp.assertStatus(200, t)

t.Run("delete - ok - nok", func(t *testing.T) {
resp = request("DELETE", uri, TestBody{}, DeleteShortUrl)
resp = request("DELETE", uri, TestBody{}, DeleteShortURL)
resp.assertStatus(404, t)
})
})
Expand Down
10 changes: 5 additions & 5 deletions controller/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import (
"github.com/adhocore/urlsh/service/url"
)

// CreateShortUrl is the controller for client to create short url from long url
// CreateShortURL is the controller for client to create short url from long url
// It responds to `POST /api/urls` and does not require auth token.
func CreateShortUrl(res http.ResponseWriter, req *http.Request) {
shortCode, err := url.CreateUrlShortCodeFromRequest(req)
shortUrl := fmt.Sprintf("%s%s%s", "http://", req.Host, "/" + shortCode)
body := response.Body{"short_code": shortCode, "short_url": shortUrl}
func CreateShortURL(res http.ResponseWriter, req *http.Request) {
shortCode, err := url.CreateURLShortCodeFromRequest(req)
shortURL := fmt.Sprintf("%s%s%s", "http://", req.Host, "/" + shortCode)
body := response.Body{"short_code": shortCode, "short_url": shortURL}

if err == nil {
response.JSON(res, http.StatusOK, body)
Expand Down
16 changes: 8 additions & 8 deletions controller/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,31 @@ import (
"github.com/adhocore/urlsh/common"
)

func TestCreateShortUrl(t *testing.T) {
func TestCreateShortURL(t *testing.T) {
t.Run("create short url - invalid length", func(t *testing.T) {
resp := request("POST", "/api/urls", TestBody{"url": ""}, CreateShortUrl)
resp := request("POST", "/api/urls", TestBody{"url": ""}, CreateShortURL)

resp.assertStatus(422, t)
resp.assertKeyValue("message", common.ErrInvalidUrlLen.Error(), t)
resp.assertKeyValue("message", common.ErrInvalidURLLen.Error(), t)
})

t.Run("create short url - invalid url", func(t *testing.T) {
resp := request("POST", "/api/urls", TestBody{"url": "http:/localhost"}, CreateShortUrl)
resp := request("POST", "/api/urls", TestBody{"url": "http:/localhost"}, CreateShortURL)

resp.assertStatus(422, t)
resp.assertKeyValue("message", common.ErrInvalidUrl.Error(), t)
resp.assertKeyValue("message", common.ErrInvalidURL.Error(), t)
})

t.Run("create short url - blacklist url", func(t *testing.T) {
resp := request("POST", "/api/urls", TestBody{"url": "http://localhost/xxx"}, CreateShortUrl)
resp := request("POST", "/api/urls", TestBody{"url": "http://localhost/xxx"}, CreateShortURL)

resp.assertStatus(422, t)
resp.assertKeyValue("message", "url matches blacklist pattern", t)
})

t.Run("create short url - past expiry", func(t *testing.T) {
body := TestBody{"url": "http://localhost", "expires_on": "2020-01-01 00:00:00"}
resp := request("POST", "/api/urls", body, CreateShortUrl)
resp := request("POST", "/api/urls", body, CreateShortURL)

resp.assertStatus(422, t)
resp.assertKeyValue("message", "expires_on can not be date in past", t)
Expand All @@ -41,7 +41,7 @@ func TestCreateShortUrl(t *testing.T) {
t.Run("create short url - OK", func(t *testing.T) {
tester := func(status int, message string) {
body := TestBody{"url": "http://localhost:1000/very/long/url", "expires_on": "2030-01-01 00:00:00"}
resp := request("POST", "/api/urls", body, CreateShortUrl)
resp := request("POST", "/api/urls", body, CreateShortURL)

resp.assertStatus(status, t)
resp.assertContains("short_code", t)
Expand Down
6 changes: 3 additions & 3 deletions controller/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ func NotFound(res http.ResponseWriter, _ *http.Request) {
response.JSON(res, http.StatusNotFound, response.Body{"message": "requested resource is not available"})
}

// ServeShortUrl is the controller for serving short urls
// ServeShortURL is the controller for serving short urls
// It responds to `GET /{shortCode}` and does not require auth token.
func ServeShortUrl(res http.ResponseWriter, req *http.Request) {
func ServeShortURL(res http.ResponseWriter, req *http.Request) {
shortCode := req.URL.Path[1:]
location, status := url.LookupOriginUrl(shortCode)
location, status := url.LookupOriginURL(shortCode)

if status != http.StatusFound {
response.JSON(res, status, response.Body{"message": "requested resource is not available"})
Expand Down
12 changes: 6 additions & 6 deletions controller/frontend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,27 @@ func TestNotFound(t *testing.T) {
})
}

func TestServeShortUrl(t *testing.T) {
func TestServeShortURL(t *testing.T) {
t.Run("serve short url", func(t *testing.T) {
url := fmt.Sprintf("http://urlsh.lvh.me/urlsh/lvh/me/%v", rand.Intn(100000))
resp := request("POST", "/api/urls", TestBody{"url": url}, CreateShortUrl)
resp := request("POST", "/api/urls", TestBody{"url": url}, CreateShortURL)
shortCode := resp.assertContains("short_code", t).(string)

t.Run("302", func(t *testing.T) {
resp := request("GET", "/" + shortCode, TestBody{}, ServeShortUrl)
resp := request("GET", "/" + shortCode, TestBody{}, ServeShortURL)
resp.assertStatus(302, t)
})

t.Run("404", func(t *testing.T) {
resp := request("GET", "/n0cod3", TestBody{}, ServeShortUrl)
resp := request("GET", "/n0cod3", TestBody{}, ServeShortURL)
resp.assertStatus(404, t)
})

t.Run("delete - 410", func(t *testing.T) {
resp := request("DELETE", "/api/admin/urls?short_code=" + shortCode, TestBody{}, DeleteShortUrl)
resp := request("DELETE", "/api/admin/urls?short_code=" + shortCode, TestBody{}, DeleteShortURL)

t.Run("410", func(t *testing.T) {
resp = request("GET", "/" + shortCode, TestBody{}, ServeShortUrl)
resp = request("GET", "/" + shortCode, TestBody{}, ServeShortURL)
resp.assertStatus(410, t)
})
})
Expand Down
5 changes: 3 additions & 2 deletions middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ import (
"github.com/adhocore/urlsh/response"
)

const AdminUriPrefix = "/api/admin"
// AdminURIPrefix is the uri to intercept by auth middleware
const AdminURIPrefix = "/api/admin"

// validateAdminToken validates request header token against env token for admin end
// It returns possible http status code and error if auth token missing/invalid.
func validateAdminToken(req *http.Request) (int, error) {
adminToken := os.Getenv("APP_ADMIN_TOKEN")

// Require token if only backend is configured and the uri matches admin prefix.
if adminToken == "" || strings.Index(req.URL.Path, AdminUriPrefix) != 0 {
if adminToken == "" || strings.Index(req.URL.Path, AdminURIPrefix) != 0 {
return 0, nil
}

Expand Down
2 changes: 1 addition & 1 deletion middleware/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func tester(token string, expectStatus int, t *testing.T) *httptest.ResponseReco

res := httptest.NewRecorder()
mux := http.NewServeMux()
mux.Handle("/", AdminAuth(http.HandlerFunc(controller.ListUrls)))
mux.Handle("/", AdminAuth(http.HandlerFunc(controller.ListURLs)))
mux.ServeHTTP(res, req)

if res.Result().StatusCode != expectStatus {
Expand Down
1 change: 1 addition & 0 deletions model/keyword.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package model

// Keyword is model for keywords
type Keyword struct {
ID uint `json:"-" gorm:"primaryKey"`
Keyword string `json:"keyword" gorm:"size:25;unique;not null"`
Expand Down
7 changes: 4 additions & 3 deletions model/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package model

import "time"

type Url struct {
// URL is model for short urls
type URL struct {
ID uint `json:"-" gorm:"primaryKey"`
ShortCode string `json:"short_code" gorm:"size:12;uniqueIndex;not null"`
OriginUrl string `json:"origin_url" gorm:"size:2048;index;not null"`
OriginURL string `json:"origin_url" gorm:"size:2048;index;not null"`
Hits uint `json:"hits" gorm:"default:0;not null"`
Deleted bool `json:"is_deleted" gorm:"default:false;not null"`
CreatedAt time.Time `json:"-" gorm:"not null"`
Expand All @@ -16,7 +17,7 @@ type Url struct {

// IsActive checks if the url model is active
// It returns true if url is not marked deleted or expired, false otherwise.
func (urlModel Url) IsActive() bool {
func (urlModel URL) IsActive() bool {
if urlModel.Deleted {
return false
}
Expand Down
8 changes: 4 additions & 4 deletions model/url_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,25 @@ import (
"github.com/adhocore/urlsh/common"
)

func TestUrl_IsActive(t *testing.T) {
func TestURL_IsActive(t *testing.T) {
t.Run("is active - deleted", func(t *testing.T) {
model := Url{Deleted: true}
model := URL{Deleted: true}
if model.IsActive() {
t.Errorf("should not be active if deleted")
}
})

t.Run("is active - expired", func(t *testing.T) {
past, _ := time.ParseInLocation(common.DateLayout, "2000-01-01 00:00:00", time.UTC)
model := Url{Deleted: false, ExpiresOn: past}
model := URL{Deleted: false, ExpiresOn: past}
if model.IsActive() {
t.Errorf("should not be active if expired")
}
})

t.Run("is active - OK", func(t *testing.T) {
future, _ := time.ParseInLocation(common.DateLayout, "3000-01-01 00:00:00", time.UTC)
model := Url{Deleted: false, ExpiresOn: future}
model := URL{Deleted: false, ExpiresOn: future}
if !model.IsActive() {
t.Errorf("should be active if not deleted or expired")
}
Expand Down
2 changes: 1 addition & 1 deletion orm/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func pgConnect() *gorm.DB {
log.Fatalf("database error: %v", err)
}

_ = db.AutoMigrate(&model.Keyword{}, &model.Url{})
_ = db.AutoMigrate(&model.Keyword{}, &model.URL{})

return db
}
Expand Down
Loading