ShaarliGo_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. //
  2. // Copyright (C) 2017-2021 Marcus Rohrmoser, http://purl.mro.name/ShaarliGo
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. //
  17. package main
  18. import (
  19. "bufio"
  20. "encoding/xml"
  21. "fmt"
  22. "io/ioutil"
  23. "net/http"
  24. "net/url"
  25. "os"
  26. "path/filepath"
  27. "strconv"
  28. "strings"
  29. "sync"
  30. "time"
  31. "github.com/yhat/scrape"
  32. "golang.org/x/net/html"
  33. "golang.org/x/net/html/atom"
  34. "github.com/stretchr/testify/assert"
  35. "net/http/httptest"
  36. "testing"
  37. )
  38. const dirTmp = "go-test~" // volatile cwd while testing
  39. // https://stackoverflow.com/a/42310257
  40. func prepTeardown(t *testing.T) func() {
  41. // t.Log("sub test [")
  42. assert.Nil(t, os.RemoveAll(dirTmp), "aha")
  43. assert.Nil(t, os.MkdirAll(dirTmp, 0700), "aha")
  44. cwd, _ := os.Getwd()
  45. os.Chdir(dirTmp)
  46. return func() {
  47. // t.Log("] sub test")
  48. os.Chdir(cwd)
  49. assert.Nil(t, os.RemoveAll(dirTmp), "aha")
  50. }
  51. }
  52. func TestQueryParse(t *testing.T) {
  53. t.Parallel()
  54. u := mustParseURL("http://example.com/a/shaarligo.cgi?do=login&foo=bar&do=auch")
  55. assert.Equal(t, "http://example.com/a/shaarligo.cgi?do=login&foo=bar&do=auch", u.String(), "ach")
  56. assert.Equal(t, "do=login&foo=bar&do=auch", u.RawQuery, "ach")
  57. v := u.Query()
  58. assert.Equal(t, 2, len(v["do"]), "omg")
  59. assert.Equal(t, "login", v["do"][0], "omg")
  60. {
  61. parts := strings.Split("", "/")
  62. assert.Equal(t, 1, len(parts), "ja, genau")
  63. assert.Equal(t, "", parts[0], "ja, genau")
  64. }
  65. {
  66. parts := strings.Split("/config", "/")
  67. assert.Equal(t, 2, len(parts), "ja, genau")
  68. assert.Equal(t, "", parts[0], "ja, genau")
  69. assert.Equal(t, "config", parts[1], "ja, genau")
  70. }
  71. }
  72. // non-Ascii paths and Cookies...
  73. func TestUrlParseµ(t *testing.T) {
  74. t.Parallel()
  75. u := mustParseURL("http://example.com/µ/")
  76. assert.Equal(t, "/µ/", u.Path, "omg")
  77. assert.Equal(t, "/%C2%B5/", u.EscapedPath(), "omg")
  78. }
  79. func TestGetConfigRaw(t *testing.T) {
  80. defer prepTeardown(t)()
  81. pi := "/config/"
  82. os.Setenv("PATH_INFO", pi)
  83. ts := httptest.NewServer(handleMux(&sync.WaitGroup{}))
  84. defer ts.Close()
  85. c := http.Client{Timeout: time.Second}
  86. r, _ := c.Get(ts.URL + pi)
  87. assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
  88. b, _ := ioutil.ReadAll(r.Body)
  89. assert.Equal(t, xml.Header+`<?xml-stylesheet type='text/xsl' href='../../themes/current/config.xslt'?>
  90. <!--
  91. The html you see here is for compatibility with vanilla shaarli.
  92. The main reason is backward compatibility for e.g. http://app.mro.name/ShaarliOS and
  93. https://github.com/dimtion/Shaarlier as tested via
  94. https://code.mro.name/mro/Shaarli-API-test
  95. -->
  96. <html xmlns="http://www.w3.org/1999/xhtml">
  97. <head/>
  98. <body>
  99. <form method="post" name="configform" id="configform">
  100. <input type="text" name="setlogin" value=""/>
  101. <input type="password" name="setpassword" />
  102. <input type="text" name="title" value=""/>
  103. <input type="submit" name="Save" value="Save config" />
  104. </form>
  105. </body>
  106. </html>
  107. `, string(b), "aha")
  108. fi, _ := os.Stat(fileFeedStorage)
  109. assert.Equal(t, int64(1019), fi.Size(), "uhu")
  110. _, err := os.Stat(filepath.Join("tpl", "tools.html"))
  111. assert.Equal(t, true, os.IsNotExist(err), "oje")
  112. }
  113. func TestGetConfigScraped(t *testing.T) {
  114. defer prepTeardown(t)()
  115. pi := "/config/"
  116. os.Setenv("PATH_INFO", pi)
  117. ts := httptest.NewServer(handleMux(&sync.WaitGroup{}))
  118. defer ts.Close()
  119. c := http.Client{Timeout: time.Second}
  120. r, _ := c.Get(ts.URL + pi)
  121. assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
  122. assert.Equal(t, "200 OK", r.Status, "aha")
  123. fo, _ := formValuesFromReader(r.Body, "configform")
  124. assert.Equal(t, url.Values(url.Values{
  125. "setlogin": []string{""},
  126. "setpassword": []string{""},
  127. "title": []string{""},
  128. "Save": []string{"Save config"},
  129. }), fo, "aha")
  130. }
  131. func TestHttpServer(t *testing.T) {
  132. defer prepTeardown(t)()
  133. var u *url.URL
  134. // Using this server doesn't result in absolute request urls as is the case running as CGI.
  135. // And Atom needs absolute urls.
  136. // Shaarligo relies on them, however.
  137. ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  138. fmt.Fprintf(w, "%s", u.String())
  139. }))
  140. defer ts.Close()
  141. u, _ = url.Parse(ts.URL)
  142. c := http.Client{Timeout: time.Second}
  143. re, _ := c.Get(ts.URL + "/uhu")
  144. b, _ := ioutil.ReadAll(re.Body)
  145. assert.Equal(t, ts.URL, string(b), "aha")
  146. }
  147. func _TestPostConfigG(t *testing.T) {
  148. defer prepTeardown(t)()
  149. cgi := "/sub/shaarligo.cgi"
  150. pi := "/config/"
  151. os.Setenv("SCRIPT_NAME", cgi)
  152. os.Setenv("PATH_INFO", pi)
  153. ts := httptest.NewServer(handleMux(&sync.WaitGroup{}))
  154. defer ts.Close()
  155. c := http.Client{Timeout: time.Second}
  156. r, err := c.PostForm(ts.URL+cgi+pi, url.Values{
  157. "title": []string{"A"},
  158. "setlogin": []string{"B"},
  159. "setpassword": []string{"123456789012"},
  160. "import_shaarli_url": []string{""},
  161. "import_shaarli_setlogin": []string{""},
  162. "import_shaarli_setpassword": []string{""},
  163. })
  164. assert.Equal(t, nil, err, "aha")
  165. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  166. assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"], "aha")
  167. body, _ := ioutil.ReadAll(r.Body)
  168. assert.Equal(t, "", string(body), "soso")
  169. cfg, err := ioutil.ReadFile(filepath.Join(dirApp, "config.yaml"))
  170. assert.Nil(t, err, "aha")
  171. assert.True(t, strings.HasPrefix(string(cfg), "title: A\nuid: B\n"), string(cfg))
  172. // assert.Equal(t, 1, len(r.Header["Set-Cookie"]), "naja")
  173. // stat, _ := os.Stat(uriPub)
  174. // assert.Equal(t, 0755, int(stat.Mode()&os.ModePerm), "ach, wieso?")
  175. }
  176. func doHttp(method, path_info string) (*http.Response, error) {
  177. cgi := "shaarligo.cgi"
  178. os.Setenv("SCRIPT_NAME", "/sub/"+cgi)
  179. os.Setenv("SERVER_PROTOCOL", "HTTP/1.1")
  180. os.Setenv("HTTP_HOST", "example.com")
  181. os.Setenv("REQUEST_METHOD", method)
  182. os.Setenv("PATH_INFO", path_info)
  183. fname := "stdout"
  184. old := os.Stdout
  185. temp, _ := os.Create(fname)
  186. os.Stdout = temp
  187. defer func() { temp.Close(); os.Stdout = old }()
  188. fmt.Print("HTTP/1.1 600 Overwrite me asap.\r\n")
  189. fmt.Print("Server: go-test\r\n")
  190. main()
  191. temp.Close()
  192. if f, err := os.Open(fname); err == nil {
  193. if ret, err := http.ReadResponse(bufio.NewReader(f), nil); err == nil {
  194. ret.Status = ret.Header["Status"][0]
  195. if i, err := strconv.Atoi(strings.SplitN(ret.Status, " ", 2)[0]); err == nil {
  196. delete(ret.Header, "Status")
  197. ret.StatusCode = i
  198. return ret, err
  199. } else {
  200. return nil, err
  201. }
  202. } else {
  203. return nil, err
  204. }
  205. } else {
  206. return nil, err
  207. }
  208. }
  209. func doGet(path_info string) (*http.Response, error) {
  210. return doHttp("GET", path_info)
  211. }
  212. func doPost(path_info string, body []byte) (*http.Response, error) {
  213. fname := "stdin"
  214. if err := ioutil.WriteFile(fname, body, 0600); err != nil {
  215. panic(err)
  216. }
  217. old := os.Stdin
  218. temp, err := os.Open(fname)
  219. if err != nil {
  220. panic(err)
  221. }
  222. os.Stdin = temp
  223. defer func() { temp.Close(); os.Stdin = old }()
  224. os.Setenv("CONTENT_LENGTH", fmt.Sprintf("%d", len(body)))
  225. os.Setenv("CONTENT_TYPE", "application/x-www-form-urlencoded")
  226. ret, err := doHttp("POST", path_info)
  227. return ret, err
  228. }
  229. func TestPostConfig(t *testing.T) {
  230. defer prepTeardown(t)()
  231. r, err := doPost("/config/", []byte(`title=A&setlogin=B&setpassword=123456789012&import_shaarli_url=&import_shaarli_setlogin=&import_shaarli_setpassword=`))
  232. assert.Nil(t, err, "aha")
  233. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  234. assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"][0], "aha")
  235. body, err := ioutil.ReadAll(r.Body)
  236. assert.Nil(t, err, "aha")
  237. assert.Equal(t, 0, len(body), "soso")
  238. cfg, err := ioutil.ReadFile(filepath.Join(dirApp, "config.yaml"))
  239. assert.Nil(t, err, "aha")
  240. assert.True(t, strings.HasPrefix(string(cfg), "title: A\nuid: B\n"), string(cfg))
  241. assert.Equal(t, 1, len(r.Header["Set-Cookie"]), "naja")
  242. // stat, _ := os.Stat(uriPub)
  243. // assert.Equal(t, 0755, int(stat.Mode()&os.ModePerm), "ach, wieso?")
  244. }
  245. func TestGetLoginWithoutRedir(t *testing.T) {
  246. defer prepTeardown(t)()
  247. r, err := doPost("/config/", []byte(`title=A&setlogin=B&setpassword=123456789012&import_shaarli_url=&import_shaarli_setlogin=&import_shaarli_setpassword=`))
  248. assert.Nil(t, err, "aha")
  249. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  250. assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"][0], "aha")
  251. os.Setenv("QUERY_STRING", "do=login")
  252. r, err = doGet("")
  253. assert.Nil(t, err, "aha")
  254. assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
  255. root, err := html.Parse(r.Body)
  256. assert.Nil(t, err, "aha")
  257. assert.NotNil(t, root, "aha")
  258. inputs := scrape.FindAll(root, func(n *html.Node) bool { return atom.Input == n.DataAtom })
  259. assert.Equal(t, 6, len(inputs), "aha")
  260. r, err = doPost("", []byte(`login=B&password=123456789012&token=foo`))
  261. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  262. assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"][0], "aha")
  263. // cook := r.Header["Set-Cookie"][0]
  264. // assert.True(t, strings.HasPrefix(cook, "ShaarliGo=MTU"), cook)
  265. }
  266. func TestGetLoginWithRedir(t *testing.T) {
  267. defer prepTeardown(t)()
  268. os.Unsetenv("COOKIE")
  269. r, err := doPost("/config/", []byte(`title=A&setlogin=B&setpassword=123456789012&import_shaarli_url=&import_shaarli_setlogin=&import_shaarli_setpassword=`))
  270. assert.Nil(t, err, "aha")
  271. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  272. assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"][0], "aha")
  273. returnurl := "/sub/" + uriPubPosts + "anyid/?foo=bar#baz"
  274. os.Setenv("QUERY_STRING", "do=login&returnurl="+url.QueryEscape(returnurl))
  275. r, err = doGet("")
  276. assert.Nil(t, err, "aha")
  277. assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
  278. root, err := html.Parse(r.Body)
  279. assert.Nil(t, err, "aha")
  280. assert.NotNil(t, root, "aha")
  281. inputs := scrape.FindAll(root, func(n *html.Node) bool { return atom.Input == n.DataAtom })
  282. assert.Equal(t, 6, len(inputs), "aha")
  283. r, err = doPost("", []byte(`login=B&password=123456789012&token=foo&returnurl=/sub/`+uriPubPosts+`anyid/?foo=bar#baz`))
  284. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  285. assert.Equal(t, returnurl, r.Header["Location"][0], "aha")
  286. // cook := r.Header["Set-Cookie"][0]
  287. // assert.True(t, strings.HasPrefix(cook, "ShaarliGo=MTU"), cook)
  288. }
  289. func _TestGetPostNew(t *testing.T) {
  290. defer prepTeardown(t)()
  291. r, err := doPost("/config", []byte(`title=A&setlogin=B&setpassword=123456789012&import_shaarli_url=&import_shaarli_setlogin=&import_shaarli_setpassword=`))
  292. assert.Nil(t, err, "aha")
  293. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  294. assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"][0], "aha")
  295. 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"))
  296. os.Setenv("QUERY_STRING", purl)
  297. r, err = doGet("")
  298. assert.Nil(t, err, "aha")
  299. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  300. assert.Equal(t, "/sub/shaarligo.cgi?do=login", r.Header["Location"], "aha")
  301. r, err = doGet(fmt.Sprintf("?do=login&returnurl=/sub/shaarligo.cgi%s", url.QueryEscape(purl)))
  302. assert.Nil(t, err, "aha")
  303. assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
  304. cook := r.Header["Set-Cookie"][0]
  305. assert.True(t, strings.HasPrefix(cook, "ShaarliGo=MTU"), cook)
  306. os.Setenv("COOKIE", cook)
  307. root, err := html.Parse(r.Body)
  308. assert.Nil(t, err, "aha")
  309. assert.NotNil(t, root, "aha")
  310. assert.Equal(t, 4, len(scrape.FindAll(root, func(n *html.Node) bool { return atom.Input == n.DataAtom })), "aha")
  311. r, err = doPost(fmt.Sprintf("?do=login&returnurl=/sub/shaarligo.cgi%s", url.QueryEscape(purl)), []byte(`login=B&password=123456789012`))
  312. os.Setenv("COOKIE", r.Header["Set-Cookie"][0])
  313. r, err = doGet(purl)
  314. assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
  315. os.Setenv("COOKIE", r.Header["Set-Cookie"][0])
  316. root, err = html.Parse(r.Body)
  317. r, err = doPost(purl, nil)
  318. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  319. assert.Equal(t, "/sub/"+uriPubPosts+"?#foo", r.Header["Location"], "aha")
  320. }
  321. func BenchmarkHello(b *testing.B) {
  322. for i := 0; i < b.N; i++ {
  323. s := fmt.Sprintf("hello")
  324. assert.NotNil(b, s, "aha")
  325. }
  326. }
  327. func fileIOPayload(idx int) {
  328. strFile := filepath.Join("testdata", strconv.Itoa(idx))
  329. if f, err := os.Create(strFile); err == nil {
  330. f.WriteString(strFile)
  331. f.Close()
  332. os.Remove(strFile)
  333. } else {
  334. panic(err)
  335. }
  336. }
  337. func BenchmarkFileCreateDeleteSequential(b *testing.B) {
  338. for i := 0; i < b.N; i++ {
  339. fileIOPayload(i)
  340. }
  341. }
  342. func _BenchmarkFileCreateDeleteParallel(b *testing.B) {
  343. var wg sync.WaitGroup
  344. for i := 0; i < b.N; i++ {
  345. go func(ii int) {
  346. wg.Add(1)
  347. defer wg.Done()
  348. fileIOPayload(ii)
  349. }(i)
  350. }
  351. wg.Wait()
  352. }
  353. func BenchmarkFileCreateDeleteParallelChannel(b *testing.B) {
  354. var wg sync.WaitGroup
  355. worker := func(id int, jobs <-chan int) {
  356. for j := range jobs {
  357. func() {
  358. wg.Add(1)
  359. defer wg.Done()
  360. fileIOPayload(j)
  361. }()
  362. }
  363. }
  364. jobs := make(chan int, 10)
  365. for w := 0; w < 5; w++ {
  366. go worker(w, jobs)
  367. }
  368. for j := 0; j < b.N; j++ {
  369. jobs <- j
  370. }
  371. close(jobs)
  372. wg.Wait()
  373. }