| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577 |
- package web
- import (
- "bytes"
- "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "net/http"
- "net/url"
- "os"
- "path/filepath"
- "testing"
- "time"
- "github.com/kellegous/go/context"
- "github.com/syndtr/goleveldb/leveldb"
- )
- type urlReq struct {
- URL string `json:"url"`
- }
- type env struct {
- mux *http.ServeMux
- dir string
- ctx *context.Context
- }
- func (e *env) destroy() {
- os.RemoveAll(e.dir)
- }
- func (e *env) get(path string) (*mockResponse, error) {
- return e.call("GET", path, nil)
- }
- func (e *env) post(path string, body interface{}) (*mockResponse, error) {
- return e.callWithJSON("POST", path, body)
- }
- func (e *env) callWithJSON(method, path string, body interface{}) (*mockResponse, error) {
- var r io.Reader
- if body != nil {
- var buf bytes.Buffer
- if err := json.NewEncoder(&buf).Encode(body); err != nil {
- return nil, err
- }
- r = &buf
- }
- return e.call(method, path, r)
- }
- func (e *env) call(method, path string, body io.Reader) (*mockResponse, error) {
- req, err := http.NewRequest(method, path, body)
- if err != nil {
- return nil, err
- }
- res := &mockResponse{
- header: map[string][]string{},
- }
- e.mux.ServeHTTP(res, req)
- return res, nil
- }
- func newEnv() (*env, error) {
- dir, err := ioutil.TempDir("", "")
- if err != nil {
- return nil, err
- }
- ctx, err := context.Open(filepath.Join(dir, "data"))
- if err != nil {
- os.RemoveAll(dir)
- return nil, err
- }
- mux := http.NewServeMux()
- Setup(mux, ctx)
- return &env{
- mux: mux,
- dir: dir,
- ctx: ctx,
- }, nil
- }
- func needEnv(t *testing.T) *env {
- e, err := newEnv()
- if err != nil {
- t.Fatal(err)
- }
- return e
- }
- type mockResponse struct {
- header http.Header
- bytes.Buffer
- status int
- }
- func (r *mockResponse) Header() http.Header {
- return r.header
- }
- func (r *mockResponse) WriteHeader(status int) {
- r.status = status
- }
- func mustBeSameNamedRoute(t *testing.T, a, b *routeWithName) {
- if a.Name != b.Name || a.URL != b.URL || a.Time.UnixNano() != b.Time.UnixNano() {
- t.Fatalf("routes are not same: %v vs %v", a, b)
- }
- }
- func mustBeRouteOf(t *testing.T, rt *context.Route, url string) {
- if rt == nil {
- t.Fatal("route is nil")
- }
- if rt.URL != url {
- t.Fatalf("expected url of %s, got %s", url, rt.URL)
- }
- if rt.Time.IsZero() {
- t.Fatal("route time is empty")
- }
- }
- func mustBeNamedRouteOf(t *testing.T, rt *routeWithName, name, url string) {
- mustBeRouteOf(t, rt.Route, url)
- if rt.Name != name {
- t.Fatalf("expected name of %s, got %s", name, rt.Name)
- }
- }
- func mustBeOk(t *testing.T, ok bool) {
- if !ok {
- t.Fatal("response is not ok")
- }
- }
- func mustBeErr(t *testing.T, m *msgErr) {
- if m.Ok {
- t.Fatal("response is ok, should be err")
- }
- if m.Error == "" {
- t.Fatal("expected an Error, but it is empty")
- }
- }
- func mustHaveStatus(t *testing.T, res *mockResponse, status int) {
- if res.status != status {
- t.Fatalf("expected response status %d, got %d", status, res.status)
- }
- }
- func TestAPIGetNotFound(t *testing.T) {
- e := needEnv(t)
- defer e.destroy()
- names := map[string]int{
- "": http.StatusBadRequest,
- "nothing": http.StatusNotFound,
- "nothing/there": http.StatusNotFound,
- }
- for name, status := range names {
- res, err := e.get(fmt.Sprintf("/api/url/%s", name))
- if err != nil {
- t.Fatal(err)
- }
- mustHaveStatus(t, res, status)
- var m msgErr
- if err := json.NewDecoder(res).Decode(&m); err != nil {
- t.Fatal(err)
- }
- mustBeErr(t, &m)
- }
- }
- func TestAPIPutThenGet(t *testing.T) {
- e := needEnv(t)
- defer e.destroy()
- res, err := e.post("/api/url/xxx", &urlReq{
- URL: "http://ex.com/",
- })
- if err != nil {
- t.Fatal(err)
- }
- mustHaveStatus(t, res, http.StatusOK)
- var pm msgRoute
- if err := json.NewDecoder(res).Decode(&pm); err != nil {
- t.Fatal(err)
- }
- mustBeOk(t, pm.Ok)
- mustBeNamedRouteOf(t, pm.Route, "xxx", "http://ex.com/")
- res, err = e.get("/api/url/xxx")
- if err != nil {
- t.Fatal(err)
- }
- mustHaveStatus(t, res, http.StatusOK)
- var gm msgRoute
- if err := json.NewDecoder(res).Decode(&gm); err != nil {
- t.Fatal(err)
- }
- mustBeOk(t, gm.Ok)
- mustBeNamedRouteOf(t, pm.Route, "xxx", "http://ex.com/")
- }
- func TestBadPuts(t *testing.T) {
- e := needEnv(t)
- defer e.destroy()
- var m msgErr
- res, err := e.call("POST", "/api/url/yyy", bytes.NewBufferString("not json"))
- if err != nil {
- t.Fatal(err)
- }
- mustHaveStatus(t, res, http.StatusBadRequest)
- if err := json.NewDecoder(res).Decode(&m); err != nil {
- t.Fatal(err)
- }
- mustBeErr(t, &m)
- res, err = e.post("/api/url/yyy", &urlReq{})
- if err != nil {
- t.Fatal(err)
- }
- mustHaveStatus(t, res, http.StatusBadRequest)
- if err := json.NewDecoder(res).Decode(&m); err != nil {
- t.Fatal(err)
- }
- mustBeErr(t, &m)
- res, err = e.post("/api/url/yyy", &urlReq{"not a URL"})
- if err != nil {
- t.Fatal(err)
- }
- mustHaveStatus(t, res, http.StatusBadRequest)
- if err := json.NewDecoder(res).Decode(&m); err != nil {
- t.Fatal(err)
- }
- mustBeErr(t, &m)
- }
- func TestAPIDel(t *testing.T) {
- e := needEnv(t)
- defer e.destroy()
- if err := e.ctx.Put("xxx", &context.Route{
- URL: "http://ex.com/",
- Time: time.Now(),
- }); err != nil {
- t.Fatal(err)
- }
- res, err := e.call("DELETE", "/api/url/xxx", nil)
- if err != nil {
- t.Fatal(err)
- }
- mustHaveStatus(t, res, http.StatusOK)
- var m msg
- if err := json.NewDecoder(res).Decode(&m); err != nil {
- t.Fatal(err)
- }
- mustBeOk(t, m.Ok)
- if _, err := e.ctx.Get("xxx"); err != leveldb.ErrNotFound {
- t.Fatal("expected xxx to be deleted")
- }
- }
- func TestAPIPutThenGetAuto(t *testing.T) {
- e := needEnv(t)
- defer e.destroy()
- res, err := e.post("/api/url/", &urlReq{URL: "http://b.com/"})
- if err != nil {
- t.Fatal(err)
- }
- mustHaveStatus(t, res, http.StatusOK)
- var am msgRoute
- if err := json.NewDecoder(res).Decode(&am); err != nil {
- t.Fatal(err)
- }
- mustBeOk(t, am.Ok)
- mustBeRouteOf(t, am.Route.Route, "http://b.com/")
- res, err = e.get(fmt.Sprintf("/api/url/%s", am.Route.Name))
- if err != nil {
- t.Fatal(err)
- }
- mustHaveStatus(t, res, http.StatusOK)
- var bm msgRoute
- if err := json.NewDecoder(res).Decode(&bm); err != nil {
- t.Fatal(err)
- }
- mustBeOk(t, bm.Ok)
- mustBeNamedRouteOf(t, bm.Route, am.Route.Name, "http://b.com/")
- }
- func getInPages(e *env, params url.Values) ([][]*routeWithName, error) {
- var pages [][]*routeWithName
- for {
- res, err := e.get("/api/urls/?" + params.Encode())
- if err != nil {
- return nil, err
- }
- if res.status != http.StatusOK {
- return nil, fmt.Errorf("HTTP status: %d", res.status)
- }
- var m msgRoutes
- if err := json.NewDecoder(res).Decode(&m); err != nil {
- return nil, err
- }
- if !m.Ok {
- return nil, errors.New("response is not ok")
- }
- pages = append(pages, m.Routes)
- if m.Next == "" {
- return pages, nil
- }
- params.Set("cursor", m.Next)
- }
- }
- type listTest struct {
- Params url.Values
- Pages [][]*routeWithName
- }
- func TestAPIList(t *testing.T) {
- e := needEnv(t)
- defer e.destroy()
- rts := []*routeWithName{
- &routeWithName{
- Name: "0",
- Route: &context.Route{
- URL: "http://0.com/",
- Time: time.Now(),
- },
- },
- &routeWithName{
- Name: "1",
- Route: &context.Route{
- URL: "http://1.com/",
- Time: time.Now(),
- },
- },
- &routeWithName{
- Name: ":a",
- Route: &context.Route{
- URL: "http://ga.com/",
- Time: time.Now(),
- },
- },
- &routeWithName{
- Name: ":b",
- Route: &context.Route{
- URL: "http://gb.com/",
- Time: time.Now(),
- },
- },
- &routeWithName{
- Name: "a",
- Route: &context.Route{
- URL: "http://a.com/",
- Time: time.Now(),
- },
- },
- &routeWithName{
- Name: "b",
- Route: &context.Route{
- URL: "http://b.com/",
- Time: time.Now(),
- },
- },
- }
- for _, rt := range rts {
- if err := e.ctx.Put(rt.Name, rt.Route); err != nil {
- t.Fatal(err)
- }
- }
- tests := []*listTest{
- &listTest{
- Params: url.Values(map[string][]string{}),
- Pages: [][]*routeWithName{
- []*routeWithName{rts[0], rts[1], rts[4], rts[5]},
- },
- },
- &listTest{
- Params: url.Values(map[string][]string{
- "include-generated-names": {"true"},
- }),
- Pages: [][]*routeWithName{rts},
- },
- &listTest{
- Params: url.Values(map[string][]string{
- "include-generated-names": {"false"},
- }),
- Pages: [][]*routeWithName{
- []*routeWithName{rts[0], rts[1], rts[4], rts[5]},
- },
- },
- &listTest{
- Params: url.Values(map[string][]string{
- "limit": {"2"},
- }),
- Pages: [][]*routeWithName{
- []*routeWithName{rts[0], rts[1]},
- []*routeWithName{rts[4], rts[5]},
- },
- },
- &listTest{
- Params: url.Values(map[string][]string{
- "limit": {"2"},
- "include-generated-names": {"true"},
- }),
- Pages: [][]*routeWithName{
- []*routeWithName{rts[0], rts[1]},
- []*routeWithName{rts[2], rts[3]},
- []*routeWithName{rts[4], rts[5]},
- },
- },
- &listTest{
- Params: url.Values(map[string][]string{
- "limit": {"2"},
- "cursor": {base64.URLEncoding.EncodeToString([]byte{':'})},
- }),
- Pages: [][]*routeWithName{
- []*routeWithName{rts[4], rts[5]},
- },
- },
- &listTest{
- Params: url.Values(map[string][]string{
- "limit": {"3"},
- "include-generated-names": {"true"},
- "cursor": {base64.URLEncoding.EncodeToString([]byte{':'})},
- }),
- Pages: [][]*routeWithName{
- []*routeWithName{rts[2], rts[3], rts[4]},
- []*routeWithName{rts[5]},
- },
- },
- &listTest{
- Params: url.Values(map[string][]string{
- "limit": {"1"},
- }),
- Pages: [][]*routeWithName{
- []*routeWithName{rts[0]},
- []*routeWithName{rts[1]},
- []*routeWithName{rts[4]},
- []*routeWithName{rts[5]},
- },
- },
- &listTest{
- Params: url.Values(map[string][]string{
- "cursor": {base64.URLEncoding.EncodeToString([]byte{'z'})},
- }),
- Pages: [][]*routeWithName{nil},
- },
- }
- for _, test := range tests {
- t.Logf("running tests for ?%s", test.Params.Encode())
- pages, err := getInPages(e, test.Params)
- if err != nil {
- t.Fatal(err)
- }
- if len(pages) != len(test.Pages) {
- t.Fatalf("number of pages mismatch %d vs %d", len(pages), len(test.Pages))
- }
- for i, n := 0, len(pages); i < n; i++ {
- page := pages[i]
- expected := test.Pages[i]
- if len(page) != len(expected) {
- t.Fatalf("page %d, length mismatch expected %d got %d", i, len(expected), len(page))
- }
- for j, m := 0, len(page); j < m; j++ {
- mustBeSameNamedRoute(t, page[j], expected[j])
- }
- }
- }
- }
- func TestBadList(t *testing.T) {
- e := needEnv(t)
- defer e.destroy()
- tests := map[string]int{
- url.Values{
- "cursor": {"not a cursor"},
- }.Encode(): http.StatusBadRequest,
- url.Values{
- "limit": {"0"},
- }.Encode(): http.StatusBadRequest,
- url.Values{
- "limit": {"not a limit"},
- }.Encode(): http.StatusBadRequest,
- url.Values{
- "limit": {"100000"},
- }.Encode(): http.StatusBadRequest,
- url.Values{
- "include-generated-names": {"butter"},
- }.Encode(): http.StatusBadRequest,
- }
- for params, status := range tests {
- res, err := e.get("/api/urls/?" + params)
- if err != nil {
- t.Fatal(err)
- }
- mustHaveStatus(t, res, status)
- var m msgErr
- if err := json.NewDecoder(res).Decode(&m); err != nil {
- t.Fatal(err)
- }
- mustBeErr(t, &m)
- }
- }
|