From bd2d8f0137d5607699241cf9d344b36da446dbc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Mon, 25 Sep 2017 22:29:00 +0200 Subject: [PATCH] adds frisby integration tests --- Makefile | 5 +- README.md | 4 +- server.go | 18 +-- stats.go | 48 ++++--- test/main.go | 344 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 390 insertions(+), 29 deletions(-) create mode 100644 test/main.go diff --git a/Makefile b/Makefile index 872def3..8329abd 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ run: - SKIP_CAPTCHA=1 go run *.go + TEST_MODE=1 go run *.go + +tests: + go run test/main.go db: echo 'CREATE TABLE "notes" (`id` VARCHAR(6) UNIQUE PRIMARY KEY, `text` TEXT, `published` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `edited` TIMESTAMP DEFAULT NULL, `password` VARCHAR(16), `views` INTEGER DEFAULT 0);' | sqlite3 database.sqlite diff --git a/README.md b/README.md index 5a1f97f..ecccd34 100644 --- a/README.md +++ b/README.md @@ -22,5 +22,5 @@ Dead simple hosting for markdown notes. - `NOTEHUB_ADMIN_EMAIL` - Recaptcha secret: - `RECAPTCHA_SECRET` -- Debugging: - - `SKIP_CAPTCHA` (expected to be non-empty) +- Test mode: + - `TEST_MODE` (expected to be non-empty; skips captcha, no writes buffering for stats) diff --git a/server.go b/server.go index afd35ab..10aa3b6 100644 --- a/server.go +++ b/server.go @@ -22,6 +22,8 @@ import ( const fraudThreshold = 7 +var TEST_MODE = false + type Template struct{ templates *template.Template } func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { @@ -38,7 +40,7 @@ func main() { } defer db.Close() - skipCaptcha := os.Getenv("SKIP_CAPTCHA") != "" + TEST_MODE = os.Getenv("TEST_MODE") != "" var ads []byte adsFName := os.Getenv("ADS") @@ -50,7 +52,7 @@ func main() { } } - go persistStats(e.Logger, db) + go flushStatsLoop(e.Logger, db) e.Renderer = &Template{templates: template.Must(template.ParseGlob("assets/templates/*.html"))} @@ -77,7 +79,7 @@ func main() { if code != http.StatusOK { return c.String(code, statuses[code]) } - defer incViews(n) + defer incViews(n, db) if fraudelent(n) { n.Ads = mdTmplHTML(ads) } @@ -87,13 +89,15 @@ func main() { e.GET("/:id/export", func(c echo.Context) error { id := c.Param("id") n, code := load(c, db) - content := statuses[code] + var content string if code == http.StatusOK { - defer incViews(n) + defer incViews(n, db) if fraudelent(n) { code = http.StatusForbidden + content = statuses[code] + } else { + content = n.Text } - content = n.Text } c.Logger().Debugf("/%s/export requested; response code: %d", id, code) return c.String(code, content) @@ -139,7 +143,7 @@ func main() { e.POST("/", func(c echo.Context) error { c.Logger().Debug("POST /") - if !skipCaptcha && !checkRecaptcha(c, c.FormValue("token")) { + if !TEST_MODE && !checkRecaptcha(c, c.FormValue("token")) { code := http.StatusForbidden return c.JSON(code, postResp{false, statuses[code] + ": robot check failed"}) } diff --git a/stats.go b/stats.go index 44b3030..8ddd950 100644 --- a/stats.go +++ b/stats.go @@ -12,33 +12,40 @@ const statsSavingInterval = 1 * time.Minute var stats = &sync.Map{} -func persistStats(logger echo.Logger, db *sql.DB) { +func flushStatsLoop(logger echo.Logger, db *sql.DB) { for { - time.Sleep(statsSavingInterval) - tx, err := db.Begin() + c, err := flush(db) if err != nil { - logger.Error(err) - return + logger.Errorf("couldn't flush stats: %v", err) } - c := 0 - stats.Range(func(id, views interface{}) bool { - stmt, _ := tx.Prepare("update notes set views = ? where id = ?") - _, err := stmt.Exec(views, id) - if err == nil { - c++ - } - stmt.Close() - defer stats.Delete(id) - return true - }) - tx.Commit() + if c > 0 { logger.Infof("successfully persisted %d values", c) } + time.Sleep(statsSavingInterval) + } +} + +func flush(db *sql.DB) (int, error) { + c := 0 + tx, err := db.Begin() + if err != nil { + return c, err } + stats.Range(func(id, views interface{}) bool { + stmt, _ := tx.Prepare("update notes set views = ? where id = ?") + _, err := stmt.Exec(views, id) + if err == nil { + c++ + } + stmt.Close() + defer stats.Delete(id) + return true + }) + return c, tx.Commit() } -func incViews(n *Note) { +func incViews(n *Note, db *sql.DB) { views := n.Views if val, ok := stats.Load(n.ID); ok { intVal, ok := val.(int) @@ -46,5 +53,8 @@ func incViews(n *Note) { views = intVal } } - defer stats.Store(n.ID, views+1) + stats.Store(n.ID, views+1) + if TEST_MODE { + flush(db) + } } diff --git a/test/main.go b/test/main.go new file mode 100644 index 0000000..7feb112 --- /dev/null +++ b/test/main.go @@ -0,0 +1,344 @@ +package main + +import ( + "strings" + + simplejson "github.com/bitly/go-simplejson" + "github.com/verdverm/frisby" +) + +func main() { + + service := "http://localhost:3000" + + frisby.Create("Test Notehub landing page"). + Get(service). + Send(). + ExpectHeader("Content-Type", "text/html; charset=utf-8"). + ExpectStatus(200). + ExpectContent("Pastebin for One-Off Markdown Publishing") + + frisby.Create("Test Notehub TOS Page"). + Get(service+"/TOS.md"). + Send(). + ExpectHeader("Content-Type", "text/html; charset=UTF-8"). + ExpectStatus(200). + ExpectContent("Terms of Service") + + frisby.Create("Test /new page"). + Get(service+"/new"). + Send(). + ExpectHeader("Content-Type", "text/html; charset=UTF-8"). + ExpectStatus(200). + ExpectContent("Publish Note") + + frisby.Create("Test non-existing page"). + Get(service+"/xxyyzz"). + Send(). + ExpectStatus(404). + ExpectHeader("Content-Type", "text/plain; charset=UTF-8"). + ExpectContent("Not found") + + frisby.Create("Test non-existing page: query params"). + Get(service + "/xxyyzz?q=v"). // TODO: test the same for valid note + Send(). + ExpectStatus(404). + ExpectContent("Not found") + + frisby.Create("Test non-existing page: alphabet violation"). + Get(service + "/login.php"). + Send(). + ExpectStatus(404). + ExpectContent("Not found") + + frisby.Create("Test publishing: no input"). + Post(service+"/"). + Send(). + ExpectStatus(412). + ExpectJson("Success", false). + ExpectJson("Payload", "Precondition failed") + + frisby.Create("Test publishing attempt: only TOS set"). + Post(service+"/"). + SetData("tos", "on"). + Send(). + ExpectStatus(400). + ExpectJson("Success", false). + ExpectJson("Payload", "Bad request: note length not accepted") + + testNote := "# Hello World!\nThis is a _test_ note!" + testNoteHTML := "

Hello World!

\n

This is a test note!

" + var id string + + tooLongNote := testNote + for len(tooLongNote) < 50000 { + tooLongNote += tooLongNote + } + + frisby.Create("Test publishing: too long note"). + Post(service+"/"). + SetData("tos", "on"). + SetData("text", tooLongNote). + Send(). + ExpectStatus(400). + ExpectJson("Success", false). + ExpectJson("Payload", "Bad request: note length not accepted") + + frisby.Create("Test publishing: correct inputs; no password"). + Post(service+"/"). + SetData("tos", "on"). + SetData("text", testNote). + Send(). + ExpectStatus(201). + ExpectJson("Success", true). + AfterJson(func(F *frisby.Frisby, json *simplejson.Json, err error) { + noteID, err := json.Get("Payload").String() + if err != nil { + F.AddError(err.Error()) + return + } + id = noteID + }) + + frisby.Create("Test retrieval of new note"). + Get(service + "/" + id). + Send(). + // simulate 3 requests (for stats) + Get(service + "/" + id). + Send(). + Get(service + "/" + id). + Send(). + ExpectStatus(200). + ExpectContent(testNoteHTML) + + frisby.Create("Test export of new note"). + Get(service+"/"+id+"/export"). + Send(). + ExpectStatus(200). + ExpectHeader("Content-type", "text/plain; charset=UTF-8"). + ExpectContent(testNote) + + // TODO: fix this + // frisby.Create("Test opening fake service on note"). + // Get(service + "/" + id + "/asd"). + // Send(). + // ExpectStatus(404). + // PrintBody(). + // ExpectContent("Not found") + // frisby.Create("Test opening fake service on note 2"). + // Get(service + "/" + id + "/exports"). + // Send(). + // ExpectStatus(404). + // ExpectContent("Not found") + + frisby.Create("Test stats of new note"). + Get(service + "/" + id + "/stats"). + Send(). + ExpectStatus(200). + ExpectContent("Views4"). + ExpectContent("Published") + + frisby.Create("Test edit page of new note"). + Get(service+"/"+id+"/edit"). + Send(). + ExpectStatus(200). + ExpectHeader("Content-type", "text/html; charset=UTF-8"). + ExpectContent(testNote) + + frisby.Create("Test invalid editing attempt: empty inputs"). + Post(service+"/"). + SetData("id", id). + Send(). + ExpectStatus(412) + + frisby.Create("Test invalid editing attempt: tos only"). + Post(service+"/"). + SetData("id", id). + SetData("tos", "on"). + Send(). + ExpectStatus(400). + ExpectJson("Success", false). + ExpectJson("Payload", "Bad request: password is empty") + + frisby.Create("Test invalid editing attempt: tos and password"). + Post(service+"/"). + SetData("id", id). + SetData("tos", "on"). + SetData("password", "aazzss"). + Send(). + ExpectStatus(401). + ExpectJson("Success", false). + ExpectJson("Payload", "Unauthorized: password is wrong") + + frisby.Create("Test invalid editing attempt: tos and password, but too short note"). + Post(service+"/"). + SetData("id", id). + SetData("tos", "on"). + SetData("text", "Test"). + SetData("password", "aazzss"). + Send(). + ExpectStatus(400). + ExpectJson("Success", false). + ExpectJson("Payload", "Bad request: note length not accepted") + + frisby.Create("Test publishing: correct inputs; with password"). + Post(service+"/"). + SetData("tos", "on"). + SetData("password", "aa11qq"). + SetData("text", testNote). + Send(). + ExpectStatus(201). + ExpectJson("Success", true). + AfterJson(func(F *frisby.Frisby, json *simplejson.Json, err error) { + noteID, err := json.Get("Payload").String() + if err != nil { + F.AddError(err.Error()) + return + } + id = noteID + }) + + updatedNote := strings.Replace(testNote, "is a", "is an updated", -1) + frisby.Create("Test invalid editing attempt: tos only"). + Post(service+"/"). + SetData("id", id). + SetData("tos", "on"). + Send(). + ExpectStatus(400). + ExpectJson("Success", false). + ExpectJson("Payload", "Bad request: password is empty") + + frisby.Create("Test invalid editing attempt: tos and wrong password"). + Post(service+"/"). + SetData("id", id). + SetData("tos", "on"). + SetData("text", updatedNote). + SetData("password", "aazzss"). + Send(). + ExpectStatus(401). + ExpectJson("Success", false). + ExpectJson("Payload", "Unauthorized: password is wrong") + + frisby.Create("Test editing: valid inputs, no tos"). + Post(service+"/"). + SetData("id", id). + SetData("text", updatedNote). + SetData("password", "aa11qq"). + Send(). + ExpectStatus(412). + ExpectJson("Success", false). + ExpectJson("Payload", "Precondition failed") + + frisby.Create("Test editing: valid inputs"). + Post(service+"/"). + SetData("id", id). + SetData("tos", "on"). + SetData("text", updatedNote). + SetData("password", "aa11qq"). + Send(). + ExpectStatus(200). + ExpectJson("Success", true) + + frisby.Create("Test retrieval of updated note"). + Get(service + "/" + id). + Send(). + ExpectStatus(200). + ExpectContent(strings.Replace(testNoteHTML, "is a", "is an updated", -1)) + + frisby.Create("Test export of new note"). + Get(service+"/"+id+"/export"). + Send(). + ExpectStatus(200). + ExpectHeader("Content-type", "text/plain; charset=UTF-8"). + ExpectContent(updatedNote) + + frisby.Create("Test deletion: valid inputs"). + Post(service+"/"). + SetData("id", id). + SetData("tos", "on"). + SetData("text", ""). + SetData("password", "aa11qq"). + Send(). + ExpectStatus(200). + ExpectJson("Success", true) + + frisby.Create("Test retrieval of deleted note"). + Get(service + "/" + id). + Send(). + ExpectStatus(404) + + fraudNote := "http://n.co https://a.co ftp://b.co" + + frisby.Create("Test publishing fraudulent note"). + Post(service+"/"). + SetData("tos", "on"). + SetData("password", "aa22qq"). + SetData("text", fraudNote). + Send(). + ExpectStatus(201). + ExpectJson("Success", true). + AfterJson(func(F *frisby.Frisby, json *simplejson.Json, err error) { + noteID, err := json.Get("Payload").String() + if err != nil { + F.AddError(err.Error()) + return + } + id = noteID + }) + + frisby.Create("Test new fraudulent note"). + Get(service+"/"+id). + Send(). + ExpectStatus(200). + ExpectHeader("Content-type", "text/html; charset=UTF-8"). + ExpectContent(`http://n.co https://a.co ftp://b.co`) + + frisby.Create("Test export of fraudulent note"). + Get(service+"/"+id+"/export"). + Send(). + ExpectStatus(200). + ExpectHeader("Content-type", "text/plain; charset=UTF-8"). + ExpectContent(fraudNote) + + // access fraudulent note more than 100 times + f := frisby.Create("Test export of fraudulent note again") + for i := 0; i < 100; i++ { + f.Get(service + "/" + id).Send() + } + + frisby.Create("Test stats of fradulent note"). + Get(service + "/" + id + "/stats"). + Send(). + ExpectStatus(200). + ExpectContent("Views102"). + ExpectContent("Published") + + frisby.Create("Test export of fraudulent note"). + Get(service+"/"+id+"/export"). + Send(). + ExpectStatus(403). + ExpectHeader("Content-type", "text/plain; charset=UTF-8"). + ExpectContent("Forbidden") + + frisby.Create("Test deletion of fraudulent note: wrong password inputs"). + Post(service+"/"). + SetData("id", id). + SetData("tos", "on"). + SetData("text", ""). + SetData("password", "aa11qq"). + Send(). + ExpectStatus(401). + ExpectJson("Success", false) + + frisby.Create("Test deletion of fraudulent note: correct password inputs"). + Post(service+"/"). + SetData("id", id). + SetData("tos", "on"). + SetData("text", ""). + SetData("password", "aa22qq"). + Send(). + ExpectStatus(200). + ExpectJson("Success", true) + + frisby.Global.PrintReport() +}