ShaarliGo_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. //
  2. // Copyright (C) 2017-2018 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. "fmt"
  21. "io/ioutil"
  22. "net/http"
  23. "net/url"
  24. "os"
  25. "path/filepath"
  26. "strconv"
  27. "strings"
  28. "sync"
  29. "github.com/yhat/scrape"
  30. "golang.org/x/net/html"
  31. "golang.org/x/net/html/atom"
  32. "github.com/stretchr/testify/assert"
  33. "testing"
  34. )
  35. const dirTmp = "go-test~"
  36. // https://stackoverflow.com/a/42310257
  37. func setupTest(t *testing.T) func(t *testing.T) {
  38. // t.Log("sub test")
  39. assert.Nil(t, os.RemoveAll(dirTmp), "aha")
  40. assert.Nil(t, os.MkdirAll(dirTmp, 0700), "aha")
  41. cwd, _ := os.Getwd()
  42. os.Chdir(dirTmp)
  43. return func(t *testing.T) {
  44. // t.Log("sub test")
  45. os.Chdir(cwd)
  46. // assert.Nil(t, os.RemoveAll(dirTmp), "aha")
  47. }
  48. }
  49. func TestQueryParse(t *testing.T) {
  50. t.Parallel()
  51. u := mustParseURL("http://example.com/a/shaarligo.cgi?do=login&foo=bar&do=auch")
  52. assert.Equal(t, "http://example.com/a/shaarligo.cgi?do=login&foo=bar&do=auch", u.String(), "ach")
  53. assert.Equal(t, "do=login&foo=bar&do=auch", u.RawQuery, "ach")
  54. v := u.Query()
  55. assert.Equal(t, 2, len(v["do"]), "omg")
  56. assert.Equal(t, "login", v["do"][0], "omg")
  57. {
  58. parts := strings.Split("", "/")
  59. assert.Equal(t, 1, len(parts), "ja, genau")
  60. assert.Equal(t, "", parts[0], "ja, genau")
  61. }
  62. {
  63. parts := strings.Split("/config", "/")
  64. assert.Equal(t, 2, len(parts), "ja, genau")
  65. assert.Equal(t, "", parts[0], "ja, genau")
  66. assert.Equal(t, "config", parts[1], "ja, genau")
  67. }
  68. }
  69. // non-Ascii paths and Cookies...
  70. func TestUrlParseµ(t *testing.T) {
  71. t.Parallel()
  72. u := mustParseURL("http://example.com/µ/")
  73. assert.Equal(t, "/µ/", u.Path, "omg")
  74. assert.Equal(t, "/%C2%B5/", u.EscapedPath(), "omg")
  75. }
  76. func doHttp(method, path_info string) (*http.Response, error) {
  77. cgi := "shaarligo.cgi"
  78. os.Setenv("SCRIPT_NAME", "/sub/"+cgi)
  79. os.Setenv("SERVER_PROTOCOL", "HTTP/1.1")
  80. os.Setenv("HTTP_HOST", "example.com")
  81. os.Setenv("REQUEST_METHOD", method)
  82. os.Setenv("PATH_INFO", path_info)
  83. fname := "stdout"
  84. old := os.Stdout
  85. temp, _ := os.Create(fname)
  86. os.Stdout = temp
  87. defer func() { temp.Close(); os.Stdout = old }()
  88. fmt.Print("HTTP/1.1 600 Overwrite me asap.\r\n")
  89. fmt.Print("Server: go-test\r\n")
  90. main()
  91. temp.Close()
  92. if f, err := os.Open(fname); err == nil {
  93. if ret, err := http.ReadResponse(bufio.NewReader(f), nil); err == nil {
  94. ret.Status = ret.Header["Status"][0]
  95. if i, err := strconv.Atoi(strings.SplitN(ret.Status, " ", 2)[0]); err == nil {
  96. delete(ret.Header, "Status")
  97. ret.StatusCode = i
  98. return ret, err
  99. } else {
  100. return nil, err
  101. }
  102. } else {
  103. return nil, err
  104. }
  105. } else {
  106. return nil, err
  107. }
  108. }
  109. func doGet(path_info string) (*http.Response, error) {
  110. return doHttp("GET", path_info)
  111. }
  112. func doPost(path_info string, body []byte) (*http.Response, error) {
  113. fname := "stdin"
  114. if err := ioutil.WriteFile(fname, body, 0600); err != nil {
  115. panic(err)
  116. }
  117. old := os.Stdin
  118. temp, err := os.Open(fname)
  119. if err != nil {
  120. panic(err)
  121. }
  122. os.Stdin = temp
  123. defer func() { temp.Close(); os.Stdin = old }()
  124. os.Setenv("CONTENT_LENGTH", fmt.Sprintf("%d", len(body)))
  125. os.Setenv("CONTENT_TYPE", "application/x-www-form-urlencoded")
  126. ret, err := doHttp("POST", path_info)
  127. return ret, err
  128. }
  129. func TestGetConfigRaw(t *testing.T) {
  130. teardownTest := setupTest(t)
  131. defer teardownTest(t)
  132. r, err := doGet("/config/")
  133. assert.Nil(t, err, "aha")
  134. assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
  135. assert.Equal(t, "200 OK", r.Status, "aha")
  136. assert.Equal(t, "go-test", r.Header["Server"][0], "aha")
  137. assert.Nil(t, r.Header["Status"], "aha")
  138. body, err := ioutil.ReadAll(r.Body)
  139. assert.Nil(t, err, "aha")
  140. assert.Equal(t, `<?xml version='1.0' encoding='UTF-8'?>
  141. <?xml-stylesheet type='text/xsl' href='../../assets/default/de/config.xslt'?>
  142. <!--
  143. The html you see here is for compatibilty with vanilla shaarli.
  144. The main reason is backward compatibility for e.g. http://app.mro.name/ShaarliOS and
  145. https://github.com/dimtion/Shaarlier as tested via
  146. https://code.mro.name/mro/Shaarli-API-test
  147. -->
  148. <html xmlns="http://www.w3.org/1999/xhtml">
  149. <head/>
  150. <body>
  151. <form method="post" name="installform" id="installform">
  152. <input type="text" name="setlogin" value=""/>
  153. <input type="password" name="setpassword" />
  154. <input type="text" name="title" value=""/>
  155. <input type="submit" name="Save" value="Save config" />
  156. </form>
  157. </body>
  158. </html>
  159. `, string(body), "aha")
  160. }
  161. func TestGetConfigScraped(t *testing.T) {
  162. teardownTest := setupTest(t)
  163. defer teardownTest(t)
  164. r, err := doGet("/config/")
  165. assert.Nil(t, err, "aha")
  166. assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
  167. assert.Equal(t, "200 OK", r.Status, "aha")
  168. assert.Equal(t, "go-test", r.Header["Server"][0], "aha")
  169. assert.Nil(t, r.Header["Status"], "aha")
  170. root, err := html.Parse(r.Body)
  171. assert.Nil(t, err, "aha")
  172. assert.NotNil(t, root, "aha")
  173. all := scrape.FindAll(root, func(n *html.Node) bool { return atom.Input == n.DataAtom })
  174. assert.Equal(t, 4, len(all), "aha")
  175. assert.Equal(t, "setlogin", scrape.Attr(all[0], "name"), "aha")
  176. assert.Equal(t, "setpassword", scrape.Attr(all[1], "name"), "aha")
  177. assert.Equal(t, "title", scrape.Attr(all[2], "name"), "aha")
  178. assert.Equal(t, "Save", scrape.Attr(all[3], "name"), "aha")
  179. }
  180. func TestPostConfig(t *testing.T) {
  181. teardownTest := setupTest(t)
  182. defer teardownTest(t)
  183. r, err := doPost("/config/", []byte(`title=A&setlogin=B&setpassword=123456789012&import_shaarli_url=&import_shaarli_setlogin=&import_shaarli_setpassword=`))
  184. assert.Nil(t, err, "aha")
  185. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  186. assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"][0], "aha")
  187. body, err := ioutil.ReadAll(r.Body)
  188. assert.Nil(t, err, "aha")
  189. assert.Equal(t, 0, len(body), "soso")
  190. cfg, err := ioutil.ReadFile(filepath.Join(dirApp, "config.yaml"))
  191. assert.Nil(t, err, "aha")
  192. assert.True(t, strings.HasPrefix(string(cfg), "title: A\nuid: B\n"), string(cfg))
  193. assert.Equal(t, 1, len(r.Header["Set-Cookie"]), "naja")
  194. // stat, _ := os.Stat(uriPub)
  195. // assert.Equal(t, 0755, int(stat.Mode()&os.ModePerm), "ach, wieso?")
  196. }
  197. func TestGetLoginWithoutRedir(t *testing.T) {
  198. teardownTest := setupTest(t)
  199. defer teardownTest(t)
  200. r, err := doPost("/config/", []byte(`title=A&setlogin=B&setpassword=123456789012&import_shaarli_url=&import_shaarli_setlogin=&import_shaarli_setpassword=`))
  201. assert.Nil(t, err, "aha")
  202. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  203. assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"][0], "aha")
  204. os.Setenv("QUERY_STRING", "do=login")
  205. r, err = doGet("")
  206. assert.Nil(t, err, "aha")
  207. assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
  208. root, err := html.Parse(r.Body)
  209. assert.Nil(t, err, "aha")
  210. assert.NotNil(t, root, "aha")
  211. inputs := scrape.FindAll(root, func(n *html.Node) bool { return atom.Input == n.DataAtom })
  212. assert.Equal(t, 6, len(inputs), "aha")
  213. r, err = doPost("", []byte(`login=B&password=123456789012&token=foo`))
  214. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  215. assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"][0], "aha")
  216. cook := r.Header["Set-Cookie"][0]
  217. assert.True(t, strings.HasPrefix(cook, "ShaarliGo=MTU"), cook)
  218. }
  219. func TestGetLoginWithRedir(t *testing.T) {
  220. teardownTest := setupTest(t)
  221. defer teardownTest(t)
  222. os.Unsetenv("COOKIE")
  223. r, err := doPost("/config/", []byte(`title=A&setlogin=B&setpassword=123456789012&import_shaarli_url=&import_shaarli_setlogin=&import_shaarli_setpassword=`))
  224. assert.Nil(t, err, "aha")
  225. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  226. assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"][0], "aha")
  227. returnurl := "/sub/" + uriPubPosts + "anyid/?foo=bar#baz"
  228. os.Setenv("QUERY_STRING", "do=login&returnurl="+url.QueryEscape(returnurl))
  229. r, err = doGet("")
  230. assert.Nil(t, err, "aha")
  231. assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
  232. root, err := html.Parse(r.Body)
  233. assert.Nil(t, err, "aha")
  234. assert.NotNil(t, root, "aha")
  235. inputs := scrape.FindAll(root, func(n *html.Node) bool { return atom.Input == n.DataAtom })
  236. assert.Equal(t, 6, len(inputs), "aha")
  237. r, err = doPost("", []byte(`login=B&password=123456789012&token=foo&returnurl=/sub/`+uriPubPosts+`anyid/?foo=bar#baz`))
  238. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  239. assert.Equal(t, returnurl, r.Header["Location"][0], "aha")
  240. cook := r.Header["Set-Cookie"][0]
  241. assert.True(t, strings.HasPrefix(cook, "ShaarliGo=MTU"), cook)
  242. }
  243. func _TestGetPostNew(t *testing.T) {
  244. teardownTest := setupTest(t)
  245. defer teardownTest(t)
  246. r, err := doPost("/config", []byte(`title=A&setlogin=B&setpassword=123456789012&import_shaarli_url=&import_shaarli_setlogin=&import_shaarli_setpassword=`))
  247. assert.Nil(t, err, "aha")
  248. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  249. assert.Equal(t, "/sub/"+uriPubPosts, r.Header["Location"][0], "aha")
  250. 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"))
  251. os.Setenv("QUERY_STRING", purl)
  252. r, err = doGet("")
  253. assert.Nil(t, err, "aha")
  254. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  255. assert.Equal(t, "/sub/shaarligo.cgi?do=login", r.Header["Location"], "aha")
  256. r, err = doGet(fmt.Sprintf("?do=login&returnurl=/sub/shaarligo.cgi%s", url.QueryEscape(purl)))
  257. assert.Nil(t, err, "aha")
  258. assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
  259. cook := r.Header["Set-Cookie"][0]
  260. assert.True(t, strings.HasPrefix(cook, "ShaarliGo=MTU"), cook)
  261. os.Setenv("COOKIE", cook)
  262. root, err := html.Parse(r.Body)
  263. assert.Nil(t, err, "aha")
  264. assert.NotNil(t, root, "aha")
  265. assert.Equal(t, 4, len(scrape.FindAll(root, func(n *html.Node) bool { return atom.Input == n.DataAtom })), "aha")
  266. r, err = doPost(fmt.Sprintf("?do=login&returnurl=/sub/shaarligo.cgi%s", url.QueryEscape(purl)), []byte(`login=B&password=123456789012`))
  267. os.Setenv("COOKIE", r.Header["Set-Cookie"][0])
  268. r, err = doGet(purl)
  269. assert.Equal(t, http.StatusOK, r.StatusCode, "aha")
  270. os.Setenv("COOKIE", r.Header["Set-Cookie"][0])
  271. root, err = html.Parse(r.Body)
  272. r, err = doPost(purl, nil)
  273. assert.Equal(t, http.StatusFound, r.StatusCode, "aha")
  274. assert.Equal(t, "/sub/"+uriPubPosts+"?#foo", r.Header["Location"], "aha")
  275. }
  276. func BenchmarkHello(b *testing.B) {
  277. for i := 0; i < b.N; i++ {
  278. s := fmt.Sprintf("hello")
  279. assert.NotNil(b, s, "aha")
  280. }
  281. }
  282. func fileIOPayload(idx int) {
  283. strFile := filepath.Join("testdata", strconv.Itoa(idx))
  284. if f, err := os.Create(strFile); err == nil {
  285. f.WriteString(strFile)
  286. f.Close()
  287. os.Remove(strFile)
  288. } else {
  289. panic(err)
  290. }
  291. }
  292. func BenchmarkFileCreateDeleteSequential(b *testing.B) {
  293. for i := 0; i < b.N; i++ {
  294. fileIOPayload(i)
  295. }
  296. }
  297. func _BenchmarkFileCreateDeleteParallel(b *testing.B) {
  298. var wg sync.WaitGroup
  299. for i := 0; i < b.N; i++ {
  300. go func(ii int) {
  301. wg.Add(1)
  302. defer wg.Done()
  303. fileIOPayload(ii)
  304. }(i)
  305. }
  306. wg.Wait()
  307. }
  308. func BenchmarkFileCreateDeleteParallelChannel(b *testing.B) {
  309. var wg sync.WaitGroup
  310. worker := func(id int, jobs <-chan int) {
  311. for j := range jobs {
  312. func() {
  313. wg.Add(1)
  314. defer wg.Done()
  315. fileIOPayload(j)
  316. }()
  317. }
  318. }
  319. jobs := make(chan int, 10)
  320. for w := 0; w < 5; w++ {
  321. go worker(w, jobs)
  322. }
  323. for j := 0; j < b.N; j++ {
  324. jobs <- j
  325. }
  326. close(jobs)
  327. wg.Wait()
  328. }