123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448 |
- //
- // Copyright (C) 2017-2021 Marcus Rohrmoser, http://purl.mro.name/ShaarliGo
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU General Public License for more details.
- //
- // You should have received a copy of the GNU General Public License
- // along with this program. If not, see <http://www.gnu.org/licenses/>.
- //
- package main
- import (
- "bufio"
- "encoding/xml"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/url"
- "os"
- "path/filepath"
- "strconv"
- "strings"
- "sync"
- "time"
- "github.com/yhat/scrape"
- "golang.org/x/net/html"
- "golang.org/x/net/html/atom"
- "github.com/stretchr/testify/assert"
- "net/http/httptest"
- "testing"
- )
- const dirTmp = "go-test~" // volatile cwd while testing
- // https://stackoverflow.com/a/42310257
- func prepTeardown(t *testing.T) func() {
- // t.Log("sub test [")
- assert.Nil(t, os.RemoveAll(dirTmp), "aha")
- assert.Nil(t, os.MkdirAll(dirTmp, 0700), "aha")
- cwd, _ := os.Getwd()
- os.Chdir(dirTmp)
- return func() {
- // t.Log("] sub test")
- os.Chdir(cwd)
- assert.Nil(t, os.RemoveAll(dirTmp), "aha")
- }
- }
- func TestQueryParse(t *testing.T) {
- t.Parallel()
- u := mustParseURL("http://example.com/a/shaarligo.cgi?do=login&foo=bar&do=auch")
- assert.Equal(t, "http://example.com/a/shaarligo.cgi?do=login&foo=bar&do=auch", u.String(), "ach")
- assert.Equal(t, "do=login&foo=bar&do=auch", u.RawQuery, "ach")
- v := u.Query()
- assert.Equal(t, 2, len(v["do"]), "omg")
- assert.Equal(t, "login", v["do"][0], "omg")
- {
- parts := strings.Split("", "/")
- assert.Equal(t, 1, len(parts), "ja, genau")
- assert.Equal(t, "", parts[0], "ja, genau")
- }
- {
- parts := strings.Split("/config", "/")
- assert.Equal(t, 2, len(parts), "ja, genau")
- assert.Equal(t, "", parts[0], "ja, genau")
- assert.Equal(t, "config", parts[1], "ja, genau")
- }
- }
- // non-Ascii paths and Cookies...
- func TestUrlParseµ(t *testing.T) {
- t.Parallel()
- u := mustParseURL("http://example.com/µ/")
- assert.Equal(t, "/µ/", u.Path, "omg")
- assert.Equal(t, "/%C2%B5/", u.EscapedPath(), "omg")
- }
- func TestGetConfigRaw(t *testing.T) {
- defer prepTeardown(t)()
- pi := "/config/"
- os.Setenv("PATH_INFO", pi)
- ts := httptest.NewServer(handleMux(&sync.WaitGroup{}))
- defer ts.Close()
- c := http.Client{Timeout: time.Second}
- r, _ := c.Get(ts.URL + pi)
- assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
- b, _ := ioutil.ReadAll(r.Body)
- assert.Equal(t, xml.Header+`<?xml-stylesheet type='text/xsl' href='../../themes/current/config.xslt'?>
- <!--
- The html you see here is for compatibility with vanilla shaarli.
- The main reason is backward compatibility for e.g. http://app.mro.name/ShaarliOS and
- https://github.com/dimtion/Shaarlier as tested via
- https://code.mro.name/mro/Shaarli-API-test
- -->
- <html xmlns="http://www.w3.org/1999/xhtml">
- <head/>
- <body>
- <form method="post" name="configform" id="configform">
- <input type="text" name="setlogin" value=""/>
- <input type="password" name="setpassword" />
- <input type="text" name="title" value=""/>
- <input type="submit" name="Save" value="Save config" />
- </form>
- </body>
- </html>
- `, string(b), "aha")
- fi, _ := os.Stat(fileFeedStorage)
- assert.Equal(t, int64(1019), fi.Size(), "uhu")
- _, err := os.Stat(filepath.Join("tpl", "tools.html"))
- assert.Equal(t, true, os.IsNotExist(err), "oje")
- }
- func TestGetConfigScraped(t *testing.T) {
- defer prepTeardown(t)()
- pi := "/config/"
- os.Setenv("PATH_INFO", pi)
- ts := httptest.NewServer(handleMux(&sync.WaitGroup{}))
- defer ts.Close()
- c := http.Client{Timeout: time.Second}
- r, _ := c.Get(ts.URL + pi)
- assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
- assert.Equal(t, "200 OK", r.Status, "aha")
- fo, _ := formValuesFromReader(r.Body, "configform")
- assert.Equal(t, url.Values(url.Values{
- "setlogin": []string{""},
- "setpassword": []string{""},
- "title": []string{""},
- "Save": []string{"Save config"},
- }), fo, "aha")
- }
- func TestHttpServer(t *testing.T) {
- defer prepTeardown(t)()
- var u *url.URL
- // Using this server doesn't result in absolute request urls as is the case running as CGI.
- // And Atom needs absolute urls.
- // Shaarligo relies on them, however.
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintf(w, "%s", u.String())
- }))
- defer ts.Close()
- u, _ = url.Parse(ts.URL)
- c := http.Client{Timeout: time.Second}
- re, _ := c.Get(ts.URL + "/uhu")
- b, _ := ioutil.ReadAll(re.Body)
- assert.Equal(t, ts.URL, string(b), "aha")
- }
- func _TestPostConfigG(t *testing.T) {
- defer prepTeardown(t)()
- cgi := "/sub/shaarligo.cgi"
- pi := "/config/"
- os.Setenv("SCRIPT_NAME", cgi)
- os.Setenv("PATH_INFO", pi)
- ts := httptest.NewServer(handleMux(&sync.WaitGroup{}))
- defer ts.Close()
- c := http.Client{Timeout: time.Second}
- r, err := c.PostForm(ts.URL+cgi+pi, url.Values{
- "title": []string{"A"},
- "setlogin": []string{"B"},
- "setpassword": []string{"123456789012"},
- "import_shaarli_url": []string{""},
- "import_shaarli_setlogin": []string{""},
- "import_shaarli_setpassword": []string{""},
- })
- assert.Equal(t, nil, err, "aha")
- assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
- assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"], "aha")
- body, _ := ioutil.ReadAll(r.Body)
- assert.Equal(t, "", string(body), "soso")
- cfg, err := ioutil.ReadFile(filepath.Join(dirApp, "config.yaml"))
- assert.Nil(t, err, "aha")
- assert.True(t, strings.HasPrefix(string(cfg), "title: A\nuid: B\n"), string(cfg))
- // assert.Equal(t, 1, len(r.Header["Set-Cookie"]), "naja")
- // stat, _ := os.Stat(uriPub)
- // assert.Equal(t, 0755, int(stat.Mode()&os.ModePerm), "ach, wieso?")
- }
- func doHttp(method, path_info string) (*http.Response, error) {
- cgi := "shaarligo.cgi"
- os.Setenv("SCRIPT_NAME", "/sub/"+cgi)
- os.Setenv("SERVER_PROTOCOL", "HTTP/1.1")
- os.Setenv("HTTP_HOST", "example.com")
- os.Setenv("REQUEST_METHOD", method)
- os.Setenv("PATH_INFO", path_info)
- fname := "stdout"
- old := os.Stdout
- temp, _ := os.Create(fname)
- os.Stdout = temp
- defer func() { temp.Close(); os.Stdout = old }()
- fmt.Print("HTTP/1.1 600 Overwrite me asap.\r\n")
- fmt.Print("Server: go-test\r\n")
- main()
- temp.Close()
- if f, err := os.Open(fname); err == nil {
- if ret, err := http.ReadResponse(bufio.NewReader(f), nil); err == nil {
- ret.Status = ret.Header["Status"][0]
- if i, err := strconv.Atoi(strings.SplitN(ret.Status, " ", 2)[0]); err == nil {
- delete(ret.Header, "Status")
- ret.StatusCode = i
- return ret, err
- } else {
- return nil, err
- }
- } else {
- return nil, err
- }
- } else {
- return nil, err
- }
- }
- func doGet(path_info string) (*http.Response, error) {
- return doHttp("GET", path_info)
- }
- func doPost(path_info string, body []byte) (*http.Response, error) {
- fname := "stdin"
- if err := ioutil.WriteFile(fname, body, 0600); err != nil {
- panic(err)
- }
- old := os.Stdin
- temp, err := os.Open(fname)
- if err != nil {
- panic(err)
- }
- os.Stdin = temp
- defer func() { temp.Close(); os.Stdin = old }()
- os.Setenv("CONTENT_LENGTH", fmt.Sprintf("%d", len(body)))
- os.Setenv("CONTENT_TYPE", "application/x-www-form-urlencoded")
- ret, err := doHttp("POST", path_info)
- return ret, err
- }
- func TestPostConfig(t *testing.T) {
- defer prepTeardown(t)()
- r, err := doPost("/config/", []byte(`title=A&setlogin=B&setpassword=123456789012&import_shaarli_url=&import_shaarli_setlogin=&import_shaarli_setpassword=`))
- assert.Nil(t, err, "aha")
- assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
- assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"][0], "aha")
- body, err := ioutil.ReadAll(r.Body)
- assert.Nil(t, err, "aha")
- assert.Equal(t, 0, len(body), "soso")
- cfg, err := ioutil.ReadFile(filepath.Join(dirApp, "config.yaml"))
- assert.Nil(t, err, "aha")
- assert.True(t, strings.HasPrefix(string(cfg), "title: A\nuid: B\n"), string(cfg))
- assert.Equal(t, 1, len(r.Header["Set-Cookie"]), "naja")
- // stat, _ := os.Stat(uriPub)
- // assert.Equal(t, 0755, int(stat.Mode()&os.ModePerm), "ach, wieso?")
- }
- func TestGetLoginWithoutRedir(t *testing.T) {
- defer prepTeardown(t)()
- r, err := doPost("/config/", []byte(`title=A&setlogin=B&setpassword=123456789012&import_shaarli_url=&import_shaarli_setlogin=&import_shaarli_setpassword=`))
- assert.Nil(t, err, "aha")
- assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
- assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"][0], "aha")
- os.Setenv("QUERY_STRING", "do=login")
- r, err = doGet("")
- assert.Nil(t, err, "aha")
- assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
- root, err := html.Parse(r.Body)
- assert.Nil(t, err, "aha")
- assert.NotNil(t, root, "aha")
- inputs := scrape.FindAll(root, func(n *html.Node) bool { return atom.Input == n.DataAtom })
- assert.Equal(t, 6, len(inputs), "aha")
- r, err = doPost("", []byte(`login=B&password=123456789012&token=foo`))
- assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
- assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"][0], "aha")
- // cook := r.Header["Set-Cookie"][0]
- // assert.True(t, strings.HasPrefix(cook, "ShaarliGo=MTU"), cook)
- }
- func TestGetLoginWithRedir(t *testing.T) {
- defer prepTeardown(t)()
- os.Unsetenv("COOKIE")
- r, err := doPost("/config/", []byte(`title=A&setlogin=B&setpassword=123456789012&import_shaarli_url=&import_shaarli_setlogin=&import_shaarli_setpassword=`))
- assert.Nil(t, err, "aha")
- assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
- assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"][0], "aha")
- returnurl := "/sub/" + uriPubPosts + "anyid/?foo=bar#baz"
- os.Setenv("QUERY_STRING", "do=login&returnurl="+url.QueryEscape(returnurl))
- r, err = doGet("")
- assert.Nil(t, err, "aha")
- assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
- root, err := html.Parse(r.Body)
- assert.Nil(t, err, "aha")
- assert.NotNil(t, root, "aha")
- inputs := scrape.FindAll(root, func(n *html.Node) bool { return atom.Input == n.DataAtom })
- assert.Equal(t, 6, len(inputs), "aha")
- r, err = doPost("", []byte(`login=B&password=123456789012&token=foo&returnurl=/sub/`+uriPubPosts+`anyid/?foo=bar#baz`))
- assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
- assert.Equal(t, returnurl, r.Header["Location"][0], "aha")
- // cook := r.Header["Set-Cookie"][0]
- // assert.True(t, strings.HasPrefix(cook, "ShaarliGo=MTU"), cook)
- }
- func _TestGetPostNew(t *testing.T) {
- defer prepTeardown(t)()
- r, err := doPost("/config", []byte(`title=A&setlogin=B&setpassword=123456789012&import_shaarli_url=&import_shaarli_setlogin=&import_shaarli_setpassword=`))
- assert.Nil(t, err, "aha")
- assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
- assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"][0], "aha")
- purl := fmt.Sprintf("?post=%s&title=%s&source=%s", url.QueryEscape("http://example.com/foo?bar=baz#grr"), url.QueryEscape("A first post"), url.QueryEscape("me"))
- os.Setenv("QUERY_STRING", purl)
- r, err = doGet("")
- assert.Nil(t, err, "aha")
- assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
- assert.Equal(t, "/sub/shaarligo.cgi?do=login", r.Header["Location"], "aha")
- r, err = doGet(fmt.Sprintf("?do=login&returnurl=/sub/shaarligo.cgi%s", url.QueryEscape(purl)))
- assert.Nil(t, err, "aha")
- assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
- cook := r.Header["Set-Cookie"][0]
- assert.True(t, strings.HasPrefix(cook, "ShaarliGo=MTU"), cook)
- os.Setenv("COOKIE", cook)
- root, err := html.Parse(r.Body)
- assert.Nil(t, err, "aha")
- assert.NotNil(t, root, "aha")
- assert.Equal(t, 4, len(scrape.FindAll(root, func(n *html.Node) bool { return atom.Input == n.DataAtom })), "aha")
- r, err = doPost(fmt.Sprintf("?do=login&returnurl=/sub/shaarligo.cgi%s", url.QueryEscape(purl)), []byte(`login=B&password=123456789012`))
- os.Setenv("COOKIE", r.Header["Set-Cookie"][0])
- r, err = doGet(purl)
- assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
- os.Setenv("COOKIE", r.Header["Set-Cookie"][0])
- root, err = html.Parse(r.Body)
- r, err = doPost(purl, nil)
- assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
- assert.Equal(t, "/sub/"+uriPubPosts+"?#foo", r.Header["Location"], "aha")
- }
- func BenchmarkHello(b *testing.B) {
- for i := 0; i < b.N; i++ {
- s := fmt.Sprintf("hello")
- assert.NotNil(b, s, "aha")
- }
- }
- func fileIOPayload(idx int) {
- strFile := filepath.Join("testdata", strconv.Itoa(idx))
- if f, err := os.Create(strFile); err == nil {
- f.WriteString(strFile)
- f.Close()
- os.Remove(strFile)
- } else {
- panic(err)
- }
- }
- func BenchmarkFileCreateDeleteSequential(b *testing.B) {
- for i := 0; i < b.N; i++ {
- fileIOPayload(i)
- }
- }
- func _BenchmarkFileCreateDeleteParallel(b *testing.B) {
- var wg sync.WaitGroup
- for i := 0; i < b.N; i++ {
- go func(ii int) {
- wg.Add(1)
- defer wg.Done()
- fileIOPayload(ii)
- }(i)
- }
- wg.Wait()
- }
- func BenchmarkFileCreateDeleteParallelChannel(b *testing.B) {
- var wg sync.WaitGroup
- worker := func(id int, jobs <-chan int) {
- for j := range jobs {
- func() {
- wg.Add(1)
- defer wg.Done()
- fileIOPayload(j)
- }()
- }
- }
- jobs := make(chan int, 10)
- for w := 0; w < 5; w++ {
- go worker(w, jobs)
- }
- for j := 0; j < b.N; j++ {
- jobs <- j
- }
- close(jobs)
- wg.Wait()
- }
|