api_test.go 12 KB

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