Преглед изворни кода

Cleanup API to make it easier to add /api/urls

Kelly Norton пре 8 година
родитељ
комит
098bd533cb
6 измењених фајлова са 527 додато и 457 уклоњено
  1. 196 0
      web/api.go
  2. 224 0
      web/api_test.go
  3. 63 0
      web/json.go
  4. 25 0
      web/name.go
  5. 18 239
      web/web.go
  6. 1 218
      web/web_test.go

+ 196 - 0
web/api.go

@@ -0,0 +1,196 @@
+package web
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+	"time"
+
+	"github.com/kellegous/go/context"
+	"github.com/syndtr/goleveldb/leveldb"
+)
+
+const (
+	alpha = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+)
+
+var (
+	errInvalidURL   = errors.New("Invalid URL")
+	errRedirectLoop = errors.New(" I'm sorry, Dave. I'm afraid I can't do that")
+)
+
+// A very simple encoding of numeric ids. This is simply a base62 encoding
+// prefixed with ":"
+func encodeID(id uint64) string {
+	n := uint64(len(alpha))
+	b := make([]byte, 0, 8)
+	if id == 0 {
+		return "0"
+	}
+
+	b = append(b, ':')
+
+	for id > 0 {
+		b = append(b, alpha[id%n])
+		id /= n
+	}
+
+	return string(b)
+}
+
+// Check that the given URL is suitable as a shortcut link.
+func validateURL(r *http.Request, s string) error {
+	u, err := url.Parse(s)
+	if err != nil {
+		return errInvalidURL
+	}
+
+	switch u.Scheme {
+	case "http", "https", "mailto", "ftp":
+		break
+	default:
+		return errInvalidURL
+	}
+
+	if r.Host == u.Host {
+		return errRedirectLoop
+	}
+
+	return nil
+}
+
+func apiURLPost(ctx *context.Context, w http.ResponseWriter, r *http.Request) {
+	p := parseName("/api/url/", r.URL.Path)
+
+	var req struct {
+		URL string `json:"url"`
+	}
+
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		writeJSONError(w, "invalid json")
+		return
+	}
+
+	// Handle delete requests
+	if req.URL == "" {
+		if p == "" {
+			writeJSONError(w, "url required")
+			return
+		}
+
+		if err := ctx.Del(p); err != nil {
+			writeJSONBackendError(w, err)
+			return
+		}
+
+		writeJSONOk(w)
+		return
+	}
+
+	if err := validateURL(r, req.URL); err != nil {
+		writeJSONError(w, err.Error())
+		return
+	}
+
+	// If no name is specified, an ID must be generate.
+	if p == "" {
+		id, err := ctx.NextID()
+		if err != nil {
+			writeJSONBackendError(w, err)
+			return
+		}
+		p = encodeID(id)
+	}
+
+	rt := context.Route{
+		URL:  req.URL,
+		Time: time.Now(),
+	}
+
+	if err := ctx.Put(p, &rt); err != nil {
+		writeJSONBackendError(w, err)
+		return
+	}
+
+	writeJSONRoute(w, p, &rt)
+
+}
+
+func apiURLGet(ctx *context.Context, w http.ResponseWriter, r *http.Request) {
+	p := parseName("/api/url/", r.URL.Path)
+
+	if p == "" {
+		writeJSONOk(w)
+		return
+	}
+
+	rt, err := ctx.Get(p)
+	if err == leveldb.ErrNotFound {
+		writeJSONOk(w)
+		return
+	} else if err != nil {
+		writeJSONBackendError(w, err)
+		return
+	}
+
+	writeJSONRoute(w, p, rt)
+}
+
+func apiURLDelete(ctx *context.Context, w http.ResponseWriter, r *http.Request) {
+	p := parseName("/api/url/", r.URL.Path)
+
+	if p == "" {
+		writeJSONError(w, "name required")
+		return
+	}
+
+	if err := ctx.Del(p); err == leveldb.ErrNotFound {
+		writeJSONError(w, fmt.Sprintf("%s not found", p))
+		return
+	} else if err != nil {
+		writeJSONBackendError(w, err)
+		return
+	}
+
+	writeJSONOk(w)
+}
+
+func apiURLsGet(ctx *context.Context, w http.ResponseWriter, r *http.Request) {
+	// TODO(knorton): This will allow enumeration of the routes.
+	writeJSONError(w, http.StatusText(http.StatusNotImplemented))
+}
+
+func apiURL(ctx *context.Context, w http.ResponseWriter, r *http.Request) {
+	switch r.Method {
+	case "POST":
+		apiURLPost(ctx, w, r)
+	case "GET":
+		apiURLGet(ctx, w, r)
+	case "DELETE":
+		apiURLDelete(ctx, w, r)
+	default:
+		writeJSONError(w, http.StatusText(http.StatusMethodNotAllowed))
+	}
+}
+
+func apiURLs(ctx *context.Context, w http.ResponseWriter, r *http.Request) {
+	switch r.Method {
+	case "GET":
+		apiURLsGet(ctx, w, r)
+	default:
+		writeJSONError(w, http.StatusText(http.StatusMethodNotAllowed))
+	}
+}
+
+// Setup ...
+func Setup(m *http.ServeMux, ctx *context.Context) {
+	m.HandleFunc("/api/url/", func(w http.ResponseWriter, r *http.Request) {
+		apiURL(ctx, w, r)
+	})
+
+	m.HandleFunc("/api/urls/", func(w http.ResponseWriter, r *http.Request) {
+		apiURLs(ctx, w, r)
+	})
+}

+ 224 - 0
web/api_test.go

@@ -0,0 +1,224 @@
+package web
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/kellegous/go/context"
+)
+
+type env struct {
+	mux *http.ServeMux
+	dir string
+	ctx *context.Context
+}
+
+func (e *env) destroy() {
+	os.RemoveAll(e.dir)
+}
+
+func (e *env) getAPI(m *msg, name string) error {
+	return e.callAPI(m, "GET", name, nil)
+}
+
+func (e *env) postAPI(m *msg, name, url string) error {
+	r := struct {
+		URL string `json:"url"`
+	}{
+		url,
+	}
+
+	var buf bytes.Buffer
+	if err := json.NewEncoder(&buf).Encode(&r); err != nil {
+		return err
+	}
+
+	return e.callAPI(m, "POST", name, &buf)
+}
+
+func (e *env) callAPI(m *msg, method, name string, body io.Reader) error {
+	req, err := http.NewRequest(method, fmt.Sprintf("/api/url/%s", name), body)
+	if err != nil {
+		return err
+	}
+
+	res := mockResponse{
+		header: map[string][]string{},
+	}
+
+	e.mux.ServeHTTP(&res, req)
+
+	if err := json.NewDecoder(&res).Decode(&m); err != nil {
+		return err
+	}
+
+	return 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 assertJustOk(t *testing.T, m *msg) {
+	if !m.Ok {
+		t.Fatal("expected OK message, but it's not OK")
+	}
+
+	if m.Error != "" {
+		t.Fatalf("expected no error, but got %s", m.Error)
+	}
+
+	if m.Route != nil {
+		t.Fatalf("expected no route, got %v", m.Route)
+	}
+}
+
+func assertOkWithRoute(t *testing.T, m *msg, url string) {
+	if !m.Ok {
+		t.Fatal("expected OK message, but it's not OK")
+	}
+
+	if m.Error != "" {
+		t.Fatalf("expected no error, but got %s", m.Error)
+	}
+
+	if m.Route == nil {
+		t.Fatalf("Route is nil, expected one with url of %s", url)
+	}
+
+	if m.Route.URL != url {
+		t.Fatalf("Expected url of %s, got %s", url, m.Route.URL)
+	}
+}
+
+func assertOkWithNamedRoute(t *testing.T, m *msg, name, url string) {
+	assertOkWithRoute(t, m, url)
+	if m.Route.Name != name {
+		t.Fatalf("expected name %s, got %s", name, m.Route.Name)
+	}
+}
+
+func TestAPIGetNotFound(t *testing.T) {
+	e := needEnv(t)
+	defer e.destroy()
+
+	var m msg
+	names := []string{"", "nothing", "nothing/there"}
+	for _, name := range names {
+		if err := e.getAPI(&m, name); err != nil {
+			t.Fatal(err)
+		}
+		assertJustOk(t, &m)
+	}
+}
+
+func TestAPIPutThenGet(t *testing.T) {
+	e := needEnv(t)
+	defer e.destroy()
+
+	var pm msg
+	if err := e.postAPI(&pm, "xxx", "http://ex.com/"); err != nil {
+		t.Fatal(err)
+	}
+	assertOkWithRoute(t, &pm, "http://ex.com/")
+
+	var gm msg
+	if err := e.getAPI(&gm, "xxx"); err != nil {
+		t.Fatal(err)
+	}
+	assertOkWithNamedRoute(t, &gm, "xxx", "http://ex.com/")
+}
+
+func TestAPIDel(t *testing.T) {
+	e := needEnv(t)
+	defer e.destroy()
+
+	var am msg
+	if err := e.postAPI(&am, "yyy", ""); err != nil {
+		t.Fatal(err)
+	}
+	assertJustOk(t, &am)
+
+	var bm msg
+	if err := e.postAPI(&bm, "yyy", "https://a.com/"); err != nil {
+		t.Fatal(err)
+	}
+	assertOkWithNamedRoute(t, &bm, "yyy", "https://a.com/")
+
+	var cm msg
+	if err := e.postAPI(&cm, "yyy", ""); err != nil {
+		t.Fatal(err)
+	}
+	assertJustOk(t, &cm)
+
+	var dm msg
+	if err := e.getAPI(&dm, "yyy"); err != nil {
+		t.Fatal(err)
+	}
+	assertJustOk(t, &dm)
+}
+
+func TestAPIPutThenGetAuto(t *testing.T) {
+	e := needEnv(t)
+	defer e.destroy()
+
+	var am msg
+	if err := e.postAPI(&am, "", "http://b.com/"); err != nil {
+		t.Fatal(err)
+	}
+	assertOkWithRoute(t, &am, "http://b.com/")
+
+	var bm msg
+	if err := e.getAPI(&bm, am.Route.Name); err != nil {
+		t.Fatal(err)
+	}
+	assertOkWithNamedRoute(t, &bm, am.Route.Name, "http://b.com/")
+}

+ 63 - 0
web/json.go

@@ -0,0 +1,63 @@
+package web
+
+import (
+	"encoding/json"
+	"log"
+	"net/http"
+
+	"github.com/kellegous/go/context"
+)
+
+// Used as an API response, this is a route with its associated shortcut name.
+type routeWithName struct {
+	Name string `json:"name"`
+	*context.Route
+}
+
+// The response type for all API responses.
+type msg struct {
+	Ok    bool           `json:"ok"`
+	Error string         `json:"error,omitempty"`
+	Route *routeWithName `json:"route,omitempty"`
+}
+
+// Encode the given data to JSON and send it to the client.
+func writeJSON(w http.ResponseWriter, data interface{}, status int) {
+	w.Header().Set("Content-Type", "application/json;charset=utf-8")
+	w.WriteHeader(status)
+	if err := json.NewEncoder(w).Encode(data); err != nil {
+		log.Panic(err)
+	}
+}
+
+// Encode a simple success msg and send it to the client.
+func writeJSONOk(w http.ResponseWriter) {
+	writeJSON(w, &msg{
+		Ok: true,
+	}, http.StatusOK)
+}
+
+// Encode an error response and send it to the client.
+func writeJSONError(w http.ResponseWriter, err string) {
+	writeJSON(w, &msg{
+		Ok:    false,
+		Error: err,
+	}, http.StatusOK)
+}
+
+// Encode a generic backend error and send it to the client.
+func writeJSONBackendError(w http.ResponseWriter, err error) {
+	log.Printf("[error] %s", err)
+	writeJSONError(w, "backend error")
+}
+
+// Encode the given named route as a msg and send it to the client.
+func writeJSONRoute(w http.ResponseWriter, name string, rt *context.Route) {
+	writeJSON(w, &msg{
+		Ok: true,
+		Route: &routeWithName{
+			Name:  name,
+			Route: rt,
+		},
+	}, http.StatusOK)
+}

+ 25 - 0
web/name.go

@@ -0,0 +1,25 @@
+package web
+
+import "strings"
+
+const encodedIDPrefix = ":"
+
+// Parse the shortcut name from the given URL path, given the base URL that is
+// handling the request.
+func parseName(base, path string) string {
+	t := path[len(base):]
+	ix := strings.Index(t, "/")
+	if ix == -1 {
+		return t
+	}
+	return t[:ix]
+}
+
+// Clean a shortcut name. Currently this just means stripping any leading
+// ":" to avoid collisions with auto generated names.
+func cleanName(name string) string {
+	for strings.HasPrefix(name, encodedIDPrefix) {
+		name = name[1:]
+	}
+	return name
+}

+ 18 - 239
web/web.go

@@ -2,122 +2,14 @@ package web
 
 import (
 	"bytes"
-	"encoding/json"
-	"errors"
 	"fmt"
 	"log"
 	"net/http"
-	"net/url"
-	"strings"
-	"time"
 
 	"github.com/kellegous/go/context"
 	"github.com/syndtr/goleveldb/leveldb"
 )
 
-const (
-	alpha  = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
-	prefix = ":"
-)
-
-var (
-	errInvalidURL   = errors.New("Invalid URL")
-	errRedirectLoop = errors.New(" I'm sorry, Dave. I'm afraid I can't do that")
-)
-
-// A very simple encoding of numeric ids. This is simply a base62 encoding
-// prefixed with ":"
-func encodeID(id uint64) string {
-	n := uint64(len(alpha))
-	b := make([]byte, 0, 8)
-	if id == 0 {
-		return "0"
-	}
-
-	b = append(b, ':')
-
-	for id > 0 {
-		b = append(b, alpha[id%n])
-		id /= n
-	}
-
-	return string(b)
-}
-
-// Clean a shortcut name. Currently this just means stripping any leading
-// ":" to avoid collisions with auto generated names.
-func cleanName(name string) string {
-	for strings.HasPrefix(name, prefix) {
-		name = name[1:]
-	}
-	return name
-}
-
-// Parse the shortcut name from the give URL path, given the base URL that is
-// handling the request.
-func parseName(base, path string) string {
-	t := path[len(base):]
-	ix := strings.Index(t, "/")
-	if ix == -1 {
-		return t
-	}
-	return t[:ix]
-}
-
-// Used as an API response, this is a route with its associated shortcut name.
-type routeWithName struct {
-	Name string `json:"name"`
-	*context.Route
-}
-
-// The response type for all API responses.
-type msg struct {
-	Ok    bool           `json:"ok"`
-	Error string         `json:"error,omitempty"`
-	Route *routeWithName `json:"route,omitempty"`
-}
-
-// Encode the given data to JSON and send it to the client.
-func writeJSON(w http.ResponseWriter, data interface{}, status int) {
-	w.Header().Set("Content-Type", "application/json;charset=utf-8")
-	w.WriteHeader(status)
-	if err := json.NewEncoder(w).Encode(data); err != nil {
-		log.Panic(err)
-	}
-}
-
-// Encode the given named route as a msg and send it to the client.
-func writeJSONRoute(w http.ResponseWriter, name string, rt *context.Route) {
-	writeJSON(w, &msg{
-		Ok: true,
-		Route: &routeWithName{
-			Name:  name,
-			Route: rt,
-		},
-	}, http.StatusOK)
-}
-
-// Encode a simple success msg and send it to the client.
-func writeJSONOk(w http.ResponseWriter) {
-	writeJSON(w, &msg{
-		Ok: true,
-	}, http.StatusOK)
-}
-
-// Encode an error response and send it to the client.
-func writeJSONError(w http.ResponseWriter, err string) {
-	writeJSON(w, &msg{
-		Ok:    false,
-		Error: err,
-	}, http.StatusOK)
-}
-
-// Encode a generic backend error and send it to the client.
-func writeJSONBackendError(w http.ResponseWriter, err error) {
-	log.Printf("[error] %s", err)
-	writeJSONError(w, "backend error")
-}
-
 // Serve a bundled asset over HTTP.
 func serveAsset(w http.ResponseWriter, r *http.Request, name string) {
 	n, err := AssetInfo(name)
@@ -135,135 +27,16 @@ func serveAsset(w http.ResponseWriter, r *http.Request, name string) {
 	http.ServeContent(w, r, n.Name(), n.ModTime(), bytes.NewReader(a))
 }
 
-// The handler that processes all API requests.
-type apiHandler struct {
-	ctx *context.Context
-}
-
-// Check that the given URL is suitable as a shortcut link.
-func validateURL(r *http.Request, s string) error {
-	u, err := url.Parse(s)
-	if err != nil {
-		return errInvalidURL
-	}
-
-	switch u.Scheme {
-	case "http", "https", "mailto", "ftp":
-		break
-	default:
-		return errInvalidURL
-	}
-
-	if r.Host == u.Host {
-		return errRedirectLoop
-	}
-
-	return nil
-}
-
-// Handle a POST request to the API.
-func apiPost(ctx *context.Context, w http.ResponseWriter, r *http.Request) {
-	p := parseName("/api/url/", r.URL.Path)
-
-	var req struct {
-		URL string `json:"url"`
-	}
-
-	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
-		writeJSONError(w, "invalid json")
-		return
-	}
-
-	// Handle delete requests
-	if req.URL == "" {
-		if p == "" {
-			writeJSONError(w, "url required")
-			return
-		}
-
-		if err := ctx.Del(p); err != nil {
-			writeJSONBackendError(w, err)
-			return
-		}
-
-		writeJSONOk(w)
-		return
-	}
-
-	if err := validateURL(r, req.URL); err != nil {
-		writeJSONError(w, err.Error())
-		return
-	}
-
-	// If no name is specified, an ID must be generate.
-	if p == "" {
-		id, err := ctx.NextID()
-		if err != nil {
-			writeJSONBackendError(w, err)
-			return
-		}
-		p = encodeID(id)
-	}
-
-	rt := context.Route{
-		URL:  req.URL,
-		Time: time.Now(),
-	}
-
-	if err := ctx.Put(p, &rt); err != nil {
-		writeJSONBackendError(w, err)
-		return
-	}
-
-	writeJSONRoute(w, p, &rt)
-}
-
-// Handle a GET request to the API.
-func apiGet(ctx *context.Context, w http.ResponseWriter, r *http.Request) {
-	p := parseName("/api/url/", r.URL.Path)
-
-	if p == "" {
-		writeJSONOk(w)
-		return
-	}
-
-	rt, err := ctx.Get(p)
-	if err == leveldb.ErrNotFound {
-		writeJSONOk(w)
-		return
-	} else if err != nil {
-		writeJSONBackendError(w, err)
-		return
-	}
-
-	writeJSONRoute(w, p, rt)
-}
-
-func (h *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	switch r.Method {
-	case "POST":
-		apiPost(h.ctx, w, r)
-	case "GET":
-		apiGet(h.ctx, w, r)
-	default:
-		writeJSONError(w, http.StatusText(http.StatusMethodNotAllowed))
-	}
-}
-
 // The default handler responds to most requests. It is responsible for the
 // shortcut redirects and for sending unmapped shortcuts to the edit page.
-type defaultHandler struct {
-	ctx *context.Context
-}
-
-func (h *defaultHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func getDefault(ctx *context.Context, w http.ResponseWriter, r *http.Request) {
 	p := parseName("/", r.URL.Path)
 	if p == "" {
 		http.Redirect(w, r, "/edit/", http.StatusTemporaryRedirect)
 		return
 	}
 
-	rt, err := h.ctx.Get(p)
+	rt, err := ctx.Get(p)
 	if err == leveldb.ErrNotFound {
 		http.Redirect(w, r,
 			fmt.Sprintf("/edit/%s", cleanName(p)),
@@ -276,13 +49,23 @@ func (h *defaultHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	http.Redirect(w, r,
 		rt.URL,
 		http.StatusTemporaryRedirect)
+
 }
 
-// Setup a Mux with all web routes.
-func allRoutes(ctx *context.Context, admin bool, version string) *http.ServeMux {
+// ListenAndServe sets up all web routes, binds the port and handles incoming
+// web requests.
+func ListenAndServe(addr string, admin bool, version string, ctx *context.Context) error {
 	mux := http.NewServeMux()
-	mux.Handle("/", &defaultHandler{ctx})
-	mux.Handle("/api/url/", &apiHandler{ctx})
+
+	mux.HandleFunc("/api/url/", func(w http.ResponseWriter, r *http.Request) {
+		apiURL(ctx, w, r)
+	})
+	mux.HandleFunc("/api/urls/", func(w http.ResponseWriter, r *http.Request) {
+		apiURLs(ctx, w, r)
+	})
+	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		getDefault(ctx, w, r)
+	})
 	mux.HandleFunc("/edit/", func(w http.ResponseWriter, r *http.Request) {
 		serveAsset(w, r, "index.html")
 	})
@@ -295,14 +78,10 @@ func allRoutes(ctx *context.Context, admin bool, version string) *http.ServeMux
 	mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
 		fmt.Fprintln(w, "OK")
 	})
+
 	if admin {
 		mux.Handle("/admin/", &adminHandler{ctx})
 	}
-	return mux
-}
 
-// ListenAndServe sets up all web routes, binds the port and handles incoming
-// web requests.
-func ListenAndServe(addr string, admin bool, version string, ctx *context.Context) error {
-	return http.ListenAndServe(addr, allRoutes(ctx, admin, version))
+	return http.ListenAndServe(addr, mux)
 }

+ 1 - 218
web/web_test.go

@@ -1,220 +1,3 @@
 package web
 
-import (
-	"bytes"
-	"encoding/json"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"net/http"
-	"os"
-	"path/filepath"
-	"testing"
-
-	"github.com/kellegous/go/context"
-)
-
-type env struct {
-	mux *http.ServeMux
-	dir string
-	ctx *context.Context
-}
-
-func (e *env) destroy() {
-	os.RemoveAll(e.dir)
-}
-
-func (e *env) getAPI(m *msg, name string) error {
-	return e.callAPI(m, "GET", name, nil)
-}
-
-func (e *env) postAPI(m *msg, name, url string) error {
-	r := struct {
-		URL string `json:"url"`
-	}{
-		url,
-	}
-
-	var buf bytes.Buffer
-	if err := json.NewEncoder(&buf).Encode(&r); err != nil {
-		return err
-	}
-
-	return e.callAPI(m, "POST", name, &buf)
-}
-
-func (e *env) callAPI(m *msg, method, name string, body io.Reader) error {
-	req, err := http.NewRequest(method, fmt.Sprintf("/api/url/%s", name), body)
-	if err != nil {
-		return err
-	}
-
-	res := mockResponse{
-		header: map[string][]string{},
-	}
-
-	e.mux.ServeHTTP(&res, req)
-
-	if err := json.NewDecoder(&res).Decode(&m); err != nil {
-		return err
-	}
-
-	return 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
-	}
-
-	return &env{
-		mux: allRoutes(ctx, ""),
-		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 assertJustOk(t *testing.T, m *msg) {
-	if !m.Ok {
-		t.Fatal("expected OK message, but it's not OK")
-	}
-
-	if m.Error != "" {
-		t.Fatalf("expected no error, but got %s", m.Error)
-	}
-
-	if m.Route != nil {
-		t.Fatalf("expected no route, got %v", m.Route)
-	}
-}
-
-func assertOkWithRoute(t *testing.T, m *msg, url string) {
-	if !m.Ok {
-		t.Fatal("expected OK message, but it's not OK")
-	}
-
-	if m.Error != "" {
-		t.Fatalf("expected no error, but got %s", m.Error)
-	}
-
-	if m.Route == nil {
-		t.Fatalf("Route is nil, expected one with url of %s", url)
-	}
-
-	if m.Route.URL != url {
-		t.Fatalf("Expected url of %s, got %s", url, m.Route.URL)
-	}
-}
-
-func assertOkWithNamedRoute(t *testing.T, m *msg, name, url string) {
-	assertOkWithRoute(t, m, url)
-	if m.Route.Name != name {
-		t.Fatalf("expected name %s, got %s", name, m.Route.Name)
-	}
-}
-
-func TestAPIGetNotFound(t *testing.T) {
-	e := needEnv(t)
-	defer e.destroy()
-
-	var m msg
-	names := []string{"", "nothing", "nothing/there"}
-	for _, name := range names {
-		if err := e.getAPI(&m, name); err != nil {
-			t.Fatal(err)
-		}
-		assertJustOk(t, &m)
-	}
-}
-
-func TestAPIPutThenGet(t *testing.T) {
-	e := needEnv(t)
-	defer e.destroy()
-
-	var pm msg
-	if err := e.postAPI(&pm, "xxx", "http://ex.com/"); err != nil {
-		t.Fatal(err)
-	}
-	assertOkWithRoute(t, &pm, "http://ex.com/")
-
-	var gm msg
-	if err := e.getAPI(&gm, "xxx"); err != nil {
-		t.Fatal(err)
-	}
-	assertOkWithNamedRoute(t, &gm, "xxx", "http://ex.com/")
-}
-
-func TestAPIDel(t *testing.T) {
-	e := needEnv(t)
-	defer e.destroy()
-
-	var am msg
-	if err := e.postAPI(&am, "yyy", ""); err != nil {
-		t.Fatal(err)
-	}
-	assertJustOk(t, &am)
-
-	var bm msg
-	if err := e.postAPI(&bm, "yyy", "https://a.com/"); err != nil {
-		t.Fatal(err)
-	}
-	assertOkWithNamedRoute(t, &bm, "yyy", "https://a.com/")
-
-	var cm msg
-	if err := e.postAPI(&cm, "yyy", ""); err != nil {
-		t.Fatal(err)
-	}
-	assertJustOk(t, &cm)
-
-	var dm msg
-	if err := e.getAPI(&dm, "yyy"); err != nil {
-		t.Fatal(err)
-	}
-	assertJustOk(t, &dm)
-}
-
-func TestAPIPutThenGetAuto(t *testing.T) {
-	e := needEnv(t)
-	defer e.destroy()
-
-	var am msg
-	if err := e.postAPI(&am, "", "http://b.com/"); err != nil {
-		t.Fatal(err)
-	}
-	assertOkWithRoute(t, &am, "http://b.com/")
-
-	var bm msg
-	if err := e.getAPI(&bm, am.Route.Name); err != nil {
-		t.Fatal(err)
-	}
-	assertOkWithNamedRoute(t, &bm, am.Route.Name, "http://b.com/")
-}
+// TODO: Add tests