api_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. package web
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "io/ioutil"
  10. "net/http"
  11. "net/url"
  12. "os"
  13. "path/filepath"
  14. "testing"
  15. "time"
  16. "github.com/kellegous/go/context"
  17. "github.com/syndtr/goleveldb/leveldb"
  18. )
  19. type urlReq struct {
  20. URL string `json:"url"`
  21. }
  22. type env struct {
  23. mux *http.ServeMux
  24. dir string
  25. ctx *context.Context
  26. }
  27. func (e *env) destroy() {
  28. os.RemoveAll(e.dir)
  29. }
  30. func (e *env) get(path string) (*mockResponse, error) {
  31. return e.call("GET", path, nil)
  32. }
  33. func (e *env) post(path string, body interface{}) (*mockResponse, error) {
  34. return e.callWithJSON("POST", path, body)
  35. }
  36. func (e *env) callWithJSON(method, path string, body interface{}) (*mockResponse, error) {
  37. var r io.Reader
  38. if body != nil {
  39. var buf bytes.Buffer
  40. if err := json.NewEncoder(&buf).Encode(body); err != nil {
  41. return nil, err
  42. }
  43. r = &buf
  44. }
  45. return e.call(method, path, r)
  46. }
  47. func (e *env) call(method, path string, body io.Reader) (*mockResponse, error) {
  48. req, err := http.NewRequest(method, path, body)
  49. if err != nil {
  50. return nil, err
  51. }
  52. res := &mockResponse{
  53. header: map[string][]string{},
  54. }
  55. e.mux.ServeHTTP(res, req)
  56. return res, nil
  57. }
  58. func newEnv() (*env, error) {
  59. dir, err := ioutil.TempDir("", "")
  60. if err != nil {
  61. return nil, err
  62. }
  63. ctx, err := context.Open(filepath.Join(dir, "data"))
  64. if err != nil {
  65. os.RemoveAll(dir)
  66. return nil, err
  67. }
  68. mux := http.NewServeMux()
  69. Setup(mux, ctx)
  70. return &env{
  71. mux: mux,
  72. dir: dir,
  73. ctx: ctx,
  74. }, nil
  75. }
  76. func needEnv(t *testing.T) *env {
  77. e, err := newEnv()
  78. if err != nil {
  79. t.Fatal(err)
  80. }
  81. return e
  82. }
  83. type mockResponse struct {
  84. header http.Header
  85. bytes.Buffer
  86. status int
  87. }
  88. func (r *mockResponse) Header() http.Header {
  89. return r.header
  90. }
  91. func (r *mockResponse) WriteHeader(status int) {
  92. r.status = status
  93. }
  94. func mustBeSameNamedRoute(t *testing.T, a, b *routeWithName) {
  95. if a.Name != b.Name || a.URL != b.URL || a.Time.UnixNano() != b.Time.UnixNano() {
  96. t.Fatalf("routes are not same: %v vs %v", a, b)
  97. }
  98. }
  99. func mustBeRouteOf(t *testing.T, rt *context.Route, url string) {
  100. if rt == nil {
  101. t.Fatal("route is nil")
  102. }
  103. if rt.URL != url {
  104. t.Fatalf("expected url of %s, got %s", url, rt.URL)
  105. }
  106. if rt.Time.IsZero() {
  107. t.Fatal("route time is empty")
  108. }
  109. }
  110. func mustBeNamedRouteOf(t *testing.T, rt *routeWithName, name, url string) {
  111. mustBeRouteOf(t, rt.Route, url)
  112. if rt.Name != name {
  113. t.Fatalf("expected name of %s, got %s", name, rt.Name)
  114. }
  115. }
  116. func mustBeOk(t *testing.T, ok bool) {
  117. if !ok {
  118. t.Fatal("response is not ok")
  119. }
  120. }
  121. func mustBeErr(t *testing.T, m *msgErr) {
  122. if m.Ok {
  123. t.Fatal("response is ok, should be err")
  124. }
  125. if m.Error == "" {
  126. t.Fatal("expected an Error, but it is empty")
  127. }
  128. }
  129. func mustHaveStatus(t *testing.T, res *mockResponse, status int) {
  130. if res.status != status {
  131. t.Fatalf("expected response status %d, got %d", status, res.status)
  132. }
  133. }
  134. func TestAPIGetNotFound(t *testing.T) {
  135. e := needEnv(t)
  136. defer e.destroy()
  137. names := map[string]int{
  138. "": http.StatusBadRequest,
  139. "nothing": http.StatusNotFound,
  140. "nothing/there": http.StatusNotFound,
  141. }
  142. for name, status := range names {
  143. res, err := e.get(fmt.Sprintf("/api/url/%s", name))
  144. if err != nil {
  145. t.Fatal(err)
  146. }
  147. mustHaveStatus(t, res, status)
  148. var m msgErr
  149. if err := json.NewDecoder(res).Decode(&m); err != nil {
  150. t.Fatal(err)
  151. }
  152. mustBeErr(t, &m)
  153. }
  154. }
  155. func TestAPIPutThenGet(t *testing.T) {
  156. e := needEnv(t)
  157. defer e.destroy()
  158. res, err := e.post("/api/url/xxx", &urlReq{
  159. URL: "http://ex.com/",
  160. })
  161. if err != nil {
  162. t.Fatal(err)
  163. }
  164. mustHaveStatus(t, res, http.StatusOK)
  165. var pm msgRoute
  166. if err := json.NewDecoder(res).Decode(&pm); err != nil {
  167. t.Fatal(err)
  168. }
  169. mustBeOk(t, pm.Ok)
  170. mustBeNamedRouteOf(t, pm.Route, "xxx", "http://ex.com/")
  171. res, err = e.get("/api/url/xxx")
  172. if err != nil {
  173. t.Fatal(err)
  174. }
  175. mustHaveStatus(t, res, http.StatusOK)
  176. var gm msgRoute
  177. if err := json.NewDecoder(res).Decode(&gm); err != nil {
  178. t.Fatal(err)
  179. }
  180. mustBeOk(t, gm.Ok)
  181. mustBeNamedRouteOf(t, pm.Route, "xxx", "http://ex.com/")
  182. }
  183. func TestBadPuts(t *testing.T) {
  184. e := needEnv(t)
  185. defer e.destroy()
  186. var m msgErr
  187. res, err := e.call("POST", "/api/url/yyy", bytes.NewBufferString("not json"))
  188. if err != nil {
  189. t.Fatal(err)
  190. }
  191. mustHaveStatus(t, res, http.StatusBadRequest)
  192. if err := json.NewDecoder(res).Decode(&m); err != nil {
  193. t.Fatal(err)
  194. }
  195. mustBeErr(t, &m)
  196. res, err = e.post("/api/url/yyy", &urlReq{})
  197. if err != nil {
  198. t.Fatal(err)
  199. }
  200. mustHaveStatus(t, res, http.StatusBadRequest)
  201. if err := json.NewDecoder(res).Decode(&m); err != nil {
  202. t.Fatal(err)
  203. }
  204. mustBeErr(t, &m)
  205. res, err = e.post("/api/url/yyy", &urlReq{"not a URL"})
  206. if err != nil {
  207. t.Fatal(err)
  208. }
  209. mustHaveStatus(t, res, http.StatusBadRequest)
  210. if err := json.NewDecoder(res).Decode(&m); err != nil {
  211. t.Fatal(err)
  212. }
  213. mustBeErr(t, &m)
  214. }
  215. func TestAPIDel(t *testing.T) {
  216. e := needEnv(t)
  217. defer e.destroy()
  218. if err := e.ctx.Put("xxx", &context.Route{
  219. URL: "http://ex.com/",
  220. Time: time.Now(),
  221. }); err != nil {
  222. t.Fatal(err)
  223. }
  224. res, err := e.call("DELETE", "/api/url/xxx", nil)
  225. if err != nil {
  226. t.Fatal(err)
  227. }
  228. mustHaveStatus(t, res, http.StatusOK)
  229. var m msg
  230. if err := json.NewDecoder(res).Decode(&m); err != nil {
  231. t.Fatal(err)
  232. }
  233. mustBeOk(t, m.Ok)
  234. if _, err := e.ctx.Get("xxx"); err != leveldb.ErrNotFound {
  235. t.Fatal("expected xxx to be deleted")
  236. }
  237. }
  238. func TestAPIPutThenGetAuto(t *testing.T) {
  239. e := needEnv(t)
  240. defer e.destroy()
  241. res, err := e.post("/api/url/", &urlReq{URL: "http://b.com/"})
  242. if err != nil {
  243. t.Fatal(err)
  244. }
  245. mustHaveStatus(t, res, http.StatusOK)
  246. var am msgRoute
  247. if err := json.NewDecoder(res).Decode(&am); err != nil {
  248. t.Fatal(err)
  249. }
  250. mustBeOk(t, am.Ok)
  251. mustBeRouteOf(t, am.Route.Route, "http://b.com/")
  252. res, err = e.get(fmt.Sprintf("/api/url/%s", am.Route.Name))
  253. if err != nil {
  254. t.Fatal(err)
  255. }
  256. mustHaveStatus(t, res, http.StatusOK)
  257. var bm msgRoute
  258. if err := json.NewDecoder(res).Decode(&bm); err != nil {
  259. t.Fatal(err)
  260. }
  261. mustBeOk(t, bm.Ok)
  262. mustBeNamedRouteOf(t, bm.Route, am.Route.Name, "http://b.com/")
  263. }
  264. func getInPages(e *env, params url.Values) ([][]*routeWithName, error) {
  265. var pages [][]*routeWithName
  266. for {
  267. res, err := e.get("/api/urls/?" + params.Encode())
  268. if err != nil {
  269. return nil, err
  270. }
  271. if res.status != http.StatusOK {
  272. return nil, fmt.Errorf("HTTP status: %d", res.status)
  273. }
  274. var m msgRoutes
  275. if err := json.NewDecoder(res).Decode(&m); err != nil {
  276. return nil, err
  277. }
  278. if !m.Ok {
  279. return nil, errors.New("response is not ok")
  280. }
  281. pages = append(pages, m.Routes)
  282. if m.Next == "" {
  283. return pages, nil
  284. }
  285. params.Set("cursor", m.Next)
  286. }
  287. }
  288. type listTest struct {
  289. Params url.Values
  290. Pages [][]*routeWithName
  291. }
  292. func TestAPIList(t *testing.T) {
  293. e := needEnv(t)
  294. defer e.destroy()
  295. rts := []*routeWithName{
  296. &routeWithName{
  297. Name: "0",
  298. Route: &context.Route{
  299. URL: "http://0.com/",
  300. Time: time.Now(),
  301. },
  302. },
  303. &routeWithName{
  304. Name: "1",
  305. Route: &context.Route{
  306. URL: "http://1.com/",
  307. Time: time.Now(),
  308. },
  309. },
  310. &routeWithName{
  311. Name: ":a",
  312. Route: &context.Route{
  313. URL: "http://ga.com/",
  314. Time: time.Now(),
  315. },
  316. },
  317. &routeWithName{
  318. Name: ":b",
  319. Route: &context.Route{
  320. URL: "http://gb.com/",
  321. Time: time.Now(),
  322. },
  323. },
  324. &routeWithName{
  325. Name: "a",
  326. Route: &context.Route{
  327. URL: "http://a.com/",
  328. Time: time.Now(),
  329. },
  330. },
  331. &routeWithName{
  332. Name: "b",
  333. Route: &context.Route{
  334. URL: "http://b.com/",
  335. Time: time.Now(),
  336. },
  337. },
  338. }
  339. for _, rt := range rts {
  340. if err := e.ctx.Put(rt.Name, rt.Route); err != nil {
  341. t.Fatal(err)
  342. }
  343. }
  344. tests := []*listTest{
  345. &listTest{
  346. Params: url.Values(map[string][]string{}),
  347. Pages: [][]*routeWithName{
  348. []*routeWithName{rts[0], rts[1], rts[4], rts[5]},
  349. },
  350. },
  351. &listTest{
  352. Params: url.Values(map[string][]string{
  353. "include-generated-names": {"true"},
  354. }),
  355. Pages: [][]*routeWithName{rts},
  356. },
  357. &listTest{
  358. Params: url.Values(map[string][]string{
  359. "include-generated-names": {"false"},
  360. }),
  361. Pages: [][]*routeWithName{
  362. []*routeWithName{rts[0], rts[1], rts[4], rts[5]},
  363. },
  364. },
  365. &listTest{
  366. Params: url.Values(map[string][]string{
  367. "limit": {"2"},
  368. }),
  369. Pages: [][]*routeWithName{
  370. []*routeWithName{rts[0], rts[1]},
  371. []*routeWithName{rts[4], rts[5]},
  372. },
  373. },
  374. &listTest{
  375. Params: url.Values(map[string][]string{
  376. "limit": {"2"},
  377. "include-generated-names": {"true"},
  378. }),
  379. Pages: [][]*routeWithName{
  380. []*routeWithName{rts[0], rts[1]},
  381. []*routeWithName{rts[2], rts[3]},
  382. []*routeWithName{rts[4], rts[5]},
  383. },
  384. },
  385. &listTest{
  386. Params: url.Values(map[string][]string{
  387. "limit": {"2"},
  388. "cursor": {base64.URLEncoding.EncodeToString([]byte{':'})},
  389. }),
  390. Pages: [][]*routeWithName{
  391. []*routeWithName{rts[4], rts[5]},
  392. },
  393. },
  394. &listTest{
  395. Params: url.Values(map[string][]string{
  396. "limit": {"3"},
  397. "include-generated-names": {"true"},
  398. "cursor": {base64.URLEncoding.EncodeToString([]byte{':'})},
  399. }),
  400. Pages: [][]*routeWithName{
  401. []*routeWithName{rts[2], rts[3], rts[4]},
  402. []*routeWithName{rts[5]},
  403. },
  404. },
  405. &listTest{
  406. Params: url.Values(map[string][]string{
  407. "limit": {"1"},
  408. }),
  409. Pages: [][]*routeWithName{
  410. []*routeWithName{rts[0]},
  411. []*routeWithName{rts[1]},
  412. []*routeWithName{rts[4]},
  413. []*routeWithName{rts[5]},
  414. },
  415. },
  416. &listTest{
  417. Params: url.Values(map[string][]string{
  418. "cursor": {base64.URLEncoding.EncodeToString([]byte{'z'})},
  419. }),
  420. Pages: [][]*routeWithName{nil},
  421. },
  422. }
  423. for _, test := range tests {
  424. t.Logf("running tests for ?%s", test.Params.Encode())
  425. pages, err := getInPages(e, test.Params)
  426. if err != nil {
  427. t.Fatal(err)
  428. }
  429. if len(pages) != len(test.Pages) {
  430. t.Fatalf("number of pages mismatch %d vs %d", len(pages), len(test.Pages))
  431. }
  432. for i, n := 0, len(pages); i < n; i++ {
  433. page := pages[i]
  434. expected := test.Pages[i]
  435. if len(page) != len(expected) {
  436. t.Fatalf("page %d, length mismatch expected %d got %d", i, len(expected), len(page))
  437. }
  438. for j, m := 0, len(page); j < m; j++ {
  439. mustBeSameNamedRoute(t, page[j], expected[j])
  440. }
  441. }
  442. }
  443. }
  444. func TestBadList(t *testing.T) {
  445. e := needEnv(t)
  446. defer e.destroy()
  447. tests := map[string]int{
  448. url.Values{
  449. "cursor": {"not a cursor"},
  450. }.Encode(): http.StatusBadRequest,
  451. url.Values{
  452. "limit": {"0"},
  453. }.Encode(): http.StatusBadRequest,
  454. url.Values{
  455. "limit": {"not a limit"},
  456. }.Encode(): http.StatusBadRequest,
  457. url.Values{
  458. "limit": {"100000"},
  459. }.Encode(): http.StatusBadRequest,
  460. url.Values{
  461. "include-generated-names": {"butter"},
  462. }.Encode(): http.StatusBadRequest,
  463. }
  464. for params, status := range tests {
  465. res, err := e.get("/api/urls/?" + params)
  466. if err != nil {
  467. t.Fatal(err)
  468. }
  469. mustHaveStatus(t, res, status)
  470. var m msgErr
  471. if err := json.NewDecoder(res).Decode(&m); err != nil {
  472. t.Fatal(err)
  473. }
  474. mustBeErr(t, &m)
  475. }
  476. }