impl entities except for PATCH

This commit is contained in:
breel
2020-08-08 12:01:49 -06:00
parent 1655a9b83a
commit f88ade0d73
25 changed files with 195 additions and 33 deletions

44
.view/.aes/main.go Normal file
View File

@@ -0,0 +1,44 @@
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"errors"
"log"
"os"
"strings"
)
func main() {
key := os.Args[1]
value := os.Args[2]
log.Println(aesDec(key, value))
}
func aesDec(key, payload string) (string, error) {
if len(key) == 0 {
return "", errors.New("key required")
}
key = strings.Repeat(key, 32)[:32]
ciphertext, err := base64.StdEncoding.DecodeString(payload)
if err != nil {
return "", err
}
block, err := aes.NewCipher([]byte(key))
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
log.Println(gcm.NonceSize())
if len(ciphertext) < gcm.NonceSize() {
return "", errors.New("short ciphertext")
}
b, err := gcm.Open(nil, ciphertext[:gcm.NonceSize()], ciphertext[gcm.NonceSize():], nil)
return string(b), err
}

176
.view/auth.go Normal file
View File

@@ -0,0 +1,176 @@
package view
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"io"
"local/dndex/config"
"local/dndex/storage"
"local/dndex/storage/entity"
"net/http"
"strings"
"time"
"github.com/google/uuid"
)
const (
AuthKey = "DnDex-Auth"
NewAuthKey = "New-" + AuthKey
UserKey = "DnDex-User"
)
func Auth(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
if !config.New().Auth {
return nil
}
if err := auth(g, w, r); err != nil {
json.NewEncoder(w).Encode(map[string]interface{}{"error": "error when authorizing: " + err.Error()})
return err
}
return nil
}
func auth(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
if isPublic(g, r) {
return nil
}
if !hasAuth(r) {
return requestAuth(g, w, r)
}
return checkAuth(g, w, r)
}
func isPublic(g storage.RateLimitedGraph, r *http.Request) bool {
namespace, err := getAuthNamespace(r)
if err != nil {
return false
}
ones, err := g.List(r.Context(), namespace, UserKey)
if err != nil {
return false
}
if len(ones) == 0 {
return false
}
return ones[0].Title == ""
}
func hasAuth(r *http.Request) bool {
_, err := r.Cookie(AuthKey)
return err == nil
}
func checkAuth(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
namespace, err := getAuthNamespace(r)
if err != nil {
return err
}
token, _ := r.Cookie(AuthKey)
results, err := g.List(r.Context(), namespace, token.Value)
if err != nil {
return err
}
if len(results) != 1 {
return requestAuth(g, w, r)
}
modified := time.Unix(0, results[0].Modified)
if time.Since(modified) > config.New().AuthLifetime {
return requestAuth(g, w, r)
}
return nil
}
func requestAuth(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
namespace, err := getAuthNamespace(r)
if err != nil {
http.Error(w, `{"error": "namespace required"}`, http.StatusBadRequest)
return err
}
ones, err := g.List(r.Context(), namespace, UserKey)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return err
}
if len(ones) != 1 {
http.NotFound(w, r)
return errors.New("namespace not established")
}
userKey := ones[0]
id := uuid.New().String()
token := entity.One{
ID: id,
Name: id,
Title: namespace,
}
if err := g.Insert(r.Context(), namespace, token); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return err
}
encodedToken, err := aesEnc(userKey.Title, token.Name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return err
}
http.SetCookie(w, &http.Cookie{Name: NewAuthKey, Value: encodedToken})
http.Redirect(w, r, r.URL.String(), http.StatusSeeOther)
return errors.New("auth requested")
}
func aesEnc(key, payload string) (string, error) {
if len(key) == 0 {
return "", errors.New("key required")
}
key = strings.Repeat(key, 32)[:32]
block, err := aes.NewCipher([]byte(key))
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
b := gcm.Seal(nonce, nonce, []byte(payload), nil)
return base64.StdEncoding.EncodeToString(b), nil
}
func aesDec(key, payload string) (string, error) {
if len(key) == 0 {
return "", errors.New("key required")
}
key = strings.Repeat(key, 32)[:32]
ciphertext, err := base64.StdEncoding.DecodeString(payload)
if err != nil {
return "", err
}
block, err := aes.NewCipher([]byte(key))
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
if len(ciphertext) < gcm.NonceSize() {
return "", errors.New("short ciphertext")
}
b, err := gcm.Open(nil, ciphertext[:gcm.NonceSize()], ciphertext[gcm.NonceSize():], nil)
return string(b), err
}

164
.view/auth_test.go Normal file
View File

@@ -0,0 +1,164 @@
package view
import (
"context"
"fmt"
"local/dndex/storage"
"local/dndex/storage/entity"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/google/uuid"
)
func TestAuth(t *testing.T) {
os.Args = os.Args[:1]
os.Setenv("AUTH", "true")
defer os.Setenv("AUTH", "false")
g := storage.NewRateLimitedGraph()
handler := jsonHandler(g)
if err := g.Insert(context.TODO(), "col."+AuthKey, entity.One{ID: UserKey, Name: UserKey, Title: "password"}); err != nil {
t.Fatal(err)
}
t.Run("auth: no namespace", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/who", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusBadRequest {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("auth: bad provided", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/who?namespace=col", nil)
r.Header.Set("Cookie", fmt.Sprintf("%s=not-a-real-token", AuthKey))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusSeeOther {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("auth: expired provided", func(t *testing.T) {
os.Setenv("AUTHLIFETIME", "1ms")
defer os.Setenv("AUTHLIFETIME", "1h")
one := entity.One{ID: uuid.New().String(), Name: uuid.New().String(), Title: "title"}
if err := g.Insert(context.TODO(), "col", one); err != nil {
t.Fatal(err)
}
time.Sleep(time.Millisecond * 50)
r := httptest.NewRequest(http.MethodGet, "/who?namespace=col", nil)
r.Header.Set("Cookie", fmt.Sprintf("%s=%s", AuthKey, one.ID))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusSeeOther {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("auth: none provided: who", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/who?namespace=col", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusSeeOther {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("auth: none provided: files", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/__files__/col/myfile", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusSeeOther {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("auth: provided", func(t *testing.T) {
os.Setenv("AUTHLIFETIME", "1h")
one := entity.One{ID: uuid.New().String(), Name: uuid.New().String(), Title: "title"}
if err := g.Insert(context.TODO(), "col."+AuthKey, one); err != nil {
t.Fatal(err)
}
r := httptest.NewRequest(http.MethodTrace, "/who?namespace=col", nil)
r.Header.Set("Cookie", fmt.Sprintf("%s=%s", AuthKey, one.Name))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("auth: request unknown namespace", func(t *testing.T) {
os.Setenv("AUTHLIFETIME", "1h")
r := httptest.NewRequest(http.MethodTrace, "/who?namespace=not-col", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusNotFound {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("auth: request", func(t *testing.T) {
os.Setenv("AUTHLIFETIME", "1h")
r := httptest.NewRequest(http.MethodTrace, "/who?namespace=col", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusSeeOther {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
rawtoken := getCookie(NewAuthKey, w.Header())
if rawtoken == "" {
t.Fatal(w.Header())
}
token, err := aesDec("password", rawtoken)
if err != nil {
t.Fatal(err)
}
r = httptest.NewRequest(http.MethodTrace, "/who?namespace=col", nil)
w = httptest.NewRecorder()
r.Header.Set("Cookie", fmt.Sprintf("%s=%s", AuthKey, token))
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
r = httptest.NewRequest(http.MethodTrace, "/__files__/col/myfile", nil)
w = httptest.NewRecorder()
r.Header.Set("Cookie", fmt.Sprintf("%s=%s", AuthKey, token))
handler.ServeHTTP(w, r)
if w.Code != http.StatusNotFound {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
}
func TestAES(t *testing.T) {
for _, plaintext := range []string{"", "payload!", "a really long payload here"} {
key := "password"
enc, err := aesEnc(key, plaintext)
if err != nil {
t.Fatal("cannot enc:", err)
}
if enc == plaintext {
t.Fatal(enc)
}
dec, err := aesDec(key, enc)
if err != nil {
t.Fatal("cannot dec:", err)
}
if dec != plaintext {
t.Fatalf("want decrypted %q, got %q", plaintext, dec)
}
}
}

120
.view/files.go Normal file
View File

@@ -0,0 +1,120 @@
package view
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"local/dndex/config"
"local/dndex/storage"
"local/simpleserve/simpleserve"
"net/http"
"net/url"
"os"
"path"
"strings"
)
func files(_ storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
namespace, err := getNamespace(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return nil
}
r.URL.Path = strings.TrimPrefix(r.URL.Path, config.New().FilePrefix)
if len(strings.TrimPrefix("/"+namespace, r.URL.Path)) < 2 {
http.NotFound(w, r)
return nil
}
simpleserve.SetContentTypeIfMedia(w, r)
switch r.Method {
case http.MethodGet:
return filesGet(w, r)
case http.MethodPost:
return filesPost(w, r)
default:
http.NotFound(w, r)
return nil
}
}
func filesGet(w http.ResponseWriter, r *http.Request) error {
http.ServeFile(w, r, toLocalPath(r.URL.Path))
return nil
}
func filesPost(w http.ResponseWriter, r *http.Request) error {
p := toLocalPath(r.URL.Path)
if err := os.MkdirAll(path.Dir(p), os.ModePerm); err != nil {
return err
}
switch r.URL.Query().Get("direct") {
case "true":
return filesPostFromDirectLink(w, r)
default:
return filesPostFromUpload(w, r)
}
}
func filesPostFromDirectLink(w http.ResponseWriter, r *http.Request) error {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
return err
}
url, err := url.Parse(string(b))
if err != nil {
return err
}
resp, err := http.Get(url.String())
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status from direct: %v", resp.StatusCode)
}
path := toLocalPath(r.URL.Path)
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
}
func filesPostFromUpload(w http.ResponseWriter, r *http.Request) error {
p := toLocalPath(r.URL.Path)
if err := os.MkdirAll(path.Dir(p), os.ModePerm); err != nil {
return err
}
if fi, err := os.Stat(p); err != nil && !os.IsNotExist(err) {
return err
} else if err == nil && fi.IsDir() {
return errors.New("path is a directory")
}
f, err := os.Create(p)
if err != nil {
return err
}
defer f.Close()
megabyte := 1 << 20
chunkSize := 10 * megabyte
r.ParseMultipartForm(int64(chunkSize))
file, _, err := r.FormFile("file")
if err != nil {
return err
}
defer file.Close()
if _, err := io.Copy(f, file); err != nil {
return err
}
return json.NewEncoder(w).Encode(map[string]interface{}{"status": "ok"})
}
func toLocalPath(p string) string {
return path.Join(config.New().FileRoot, p)
}

172
.view/files_test.go Normal file
View File

@@ -0,0 +1,172 @@
package view
import (
"bytes"
"fmt"
"io/ioutil"
"local/dndex/config"
"local/dndex/storage"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path"
"strings"
"testing"
)
func TestFiles(t *testing.T) {
os.Args = os.Args[:1]
d, err := ioutil.TempDir(os.TempDir(), "pattern*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(d)
if err := os.MkdirAll(path.Join(d, "col"), os.ModePerm); err != nil {
t.Fatal(err)
}
os.Setenv("FILEROOT", d)
t.Logf("config: %+v", config.New())
handler := jsonHandler(storage.RateLimitedGraph{})
prefix := path.Join(config.New().FilePrefix, "col")
t.Run("has qparam, doesnt fix namespace for files prefix", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, fmt.Sprintf("%s/?namespace=col", config.New().FilePrefix), nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusBadRequest {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("get fake file 404", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, fmt.Sprintf("%s/fake", prefix), nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusNotFound {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("get typed files", func(t *testing.T) {
cases := map[string]string{
"txt": "text/plain",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"gif": "image/gif",
"mkv": "video/x-matroska",
}
for ext, ct := range cases {
for _, extC := range []string{strings.ToLower(ext), strings.ToUpper(ext)} {
f, err := ioutil.TempFile(path.Join(d, "col"), "*."+extC)
t.Logf("tempFile(%q/col, *.%s) = %q", d, extC, f.Name())
if err != nil {
t.Fatal(err)
}
f.Write([]byte("hello, world"))
f.Close()
defer os.Remove(f.Name())
r := httptest.NewRequest(http.MethodGet, fmt.Sprintf("%s?direct=true", path.Join(prefix, path.Base(f.Name()))), nil)
w := httptest.NewRecorder()
t.Logf("URL = %q", r.URL.String())
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
if contentType, ok := w.Header()["Content-Type"]; !ok {
t.Fatal(w.Header())
} else if len(contentType) < 1 || !strings.HasPrefix(contentType[0], ct) {
t.Fatal(contentType, ", want:", ct)
}
t.Logf("%+v", w)
}
}
})
t.Run("post file: direct link", func(t *testing.T) {
f, err := ioutil.TempFile(os.TempDir(), "*.html")
if err != nil {
t.Fatal(err)
}
f.Write([]byte("hello, world"))
f.Close()
defer os.Remove(f.Name())
name := path.Base(f.Name())
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi"))
}))
defer s.Close()
r := httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s?direct=true", path.Join(prefix, name)), strings.NewReader(s.URL))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
r = httptest.NewRequest(http.MethodGet, path.Join(prefix, name), nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
if body := string(w.Body.Bytes()); body != "hi" {
t.Fatal(body)
}
})
t.Run("post file: bad direct link", func(t *testing.T) {
f, err := ioutil.TempFile(os.TempDir(), "*.txt")
if err != nil {
t.Fatal(err)
}
f.Write([]byte("hello, world"))
f.Close()
defer os.Remove(f.Name())
r := httptest.NewRequest(http.MethodPost, path.Join(prefix, path.Base(f.Name())), strings.NewReader(`bad link teehee`))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code == http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("post file: form file", func(t *testing.T) {
f, err := ioutil.TempFile(os.TempDir(), "*.html")
if err != nil {
t.Fatal(err)
}
f.Close()
defer os.Remove(f.Name())
name := path.Base(f.Name())
b := bytes.NewBuffer(nil)
writer := multipart.NewWriter(b)
w2, _ := writer.CreateFormFile("file", name)
w2.Write([]byte("hello, world"))
writer.Close()
r := httptest.NewRequest(http.MethodPost, path.Join(prefix, name), b)
r.Header.Set("Content-Type", writer.FormDataContentType())
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
r = httptest.NewRequest(http.MethodGet, path.Join(prefix, name), nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
if body := string(w.Body.Bytes()); body != "hello, world" {
t.Fatal(body)
}
})
}

150
.view/json.go Normal file
View File

@@ -0,0 +1,150 @@
package view
import (
"encoding/json"
"errors"
"fmt"
"io"
"local/dndex/config"
"local/dndex/storage"
"local/gziphttp"
"log"
"net/http"
"strings"
"time"
"golang.org/x/time/rate"
)
var GitCommit string
func JSON(g storage.RateLimitedGraph) error {
port := config.New().Port
log.Println("listening on", port)
err := http.ListenAndServe(fmt.Sprintf(":%d", port), jsonHandler(g))
return err
}
func jsonHandler(g storage.RateLimitedGraph) http.Handler {
mux := http.NewServeMux()
routes := []struct {
path string
foo func(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error
noauth bool
}{
{
path: "/port",
foo: port,
},
{
path: "/who",
foo: who,
},
{
path: "/register",
foo: register,
noauth: true,
},
{
path: config.New().FilePrefix + "/",
foo: files,
},
{
path: "/version",
foo: version,
noauth: true,
},
}
for _, route := range routes {
foo := route.foo
auth := !route.noauth
mux.HandleFunc(route.path, func(w http.ResponseWriter, r *http.Request) {
if err := delay(w, r); err != nil {
http.Error(w, err.Error(), 499)
}
if auth {
if err := Auth(g, w, r); err != nil {
return
}
}
if err := foo(g, w, r); err != nil {
status := http.StatusInternalServerError
if strings.Contains(err.Error(), "collision") {
status = http.StatusConflict
}
b, _ := json.Marshal(map[string]string{"error": err.Error()})
http.Error(w, string(b), status)
}
})
}
return rateLimited(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if gziphttp.Can(r) {
gz := gziphttp.New(w)
defer gz.Close()
w = gz
}
r.Body = struct {
io.Reader
io.Closer
}{
Reader: io.LimitReader(r.Body, config.New().MaxFileSize),
Closer: r.Body,
}
mux.ServeHTTP(w, r)
}))
}
func rateLimited(foo http.HandlerFunc) http.HandlerFunc {
sysRPS := config.New().SysRPS
limiter := rate.NewLimiter(rate.Limit(sysRPS), sysRPS)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := limiter.Wait(r.Context()); err != nil {
http.Error(w, err.Error(), http.StatusTooManyRequests)
}
foo(w, r)
})
}
func getAuthNamespace(r *http.Request) (string, error) {
namespace, err := getNamespace(r)
return strings.Join([]string{namespace, AuthKey}, "."), err
}
func getNamespace(r *http.Request) (string, error) {
if strings.HasPrefix(r.URL.Path, config.New().FilePrefix) {
path := strings.TrimPrefix(r.URL.Path, config.New().FilePrefix+"/")
if path == r.URL.Path {
return "", errors.New("no namespace on files")
}
path = strings.Split(path, "/")[0]
if path == "" {
return "", errors.New("empty namespace on files")
}
return path, nil
}
namespace := r.URL.Query().Get("namespace")
if len(namespace) == 0 {
return "", errors.New("no namespace found")
}
return namespace, nil
}
func version(_ storage.RateLimitedGraph, w http.ResponseWriter, _ *http.Request) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(map[string]string{"version": GitCommit})
}
func delay(w http.ResponseWriter, r *http.Request) error {
select {
case <-time.After(config.New().Delay):
case <-r.Context().Done():
return errors.New("client DCd")
}
return nil
}

159
.view/json_test.go Normal file
View File

@@ -0,0 +1,159 @@
package view
import (
"context"
"encoding/json"
"local/dndex/config"
"local/dndex/storage"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
)
func TestGetNamespace(t *testing.T) {
os.Args = os.Args[:1]
cases := map[string]struct {
url string
want string
invalid bool
}{
"no query param, not files, should fail": {
url: "/",
invalid: true,
},
"empty query param, not files, should fail": {
url: "/a?namespace=",
invalid: true,
},
"query param, not files": {
url: "/a?namespace=OK",
want: "OK",
},
"files, no query param": {
url: config.New().FilePrefix + "/OK",
want: "OK",
},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
c.url = "http://host.tld:80" + c.url
uri, err := url.Parse(c.url)
if err != nil {
t.Fatal(err)
}
ns, err := getNamespace(&http.Request{URL: uri})
if err != nil && !c.invalid {
t.Fatal(c.invalid, err)
} else if err == nil && ns != c.want {
t.Fatal(c.want, ns)
}
authns, err := getAuthNamespace(&http.Request{URL: uri})
if err != nil && !c.invalid {
t.Fatal(c.invalid, err)
} else if err == nil && authns != c.want+"."+AuthKey {
t.Fatal(c.want+"."+AuthKey, authns)
}
})
}
}
func TestRateLimited(t *testing.T) {
os.Args = os.Args[:1]
os.Setenv("SYS_RPS", "10")
foo := rateLimited(func(w http.ResponseWriter, r *http.Request) {})
ok := 0
tooMany := 0
for i := 0; i < 20; i++ {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil)
ctx, can := context.WithTimeout(r.Context(), time.Millisecond*50)
defer can()
r = r.WithContext(ctx)
foo(w, r)
switch w.Code {
case http.StatusOK:
ok += 1
case http.StatusTooManyRequests:
tooMany += 1
default:
t.Fatal("unexpected status", w.Code)
}
}
if ok < 9 || ok > 11 {
t.Fatal(ok, tooMany)
}
}
func TestVersion(t *testing.T) {
t.Run("no version set", func(t *testing.T) {
w := httptest.NewRecorder()
if err := version(storage.RateLimitedGraph{}, w, nil); err != nil {
t.Fatal(err)
}
var resp struct {
Version string `json:"version"`
}
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
t.Fatal(err)
} else if resp.Version != "" {
t.Fatal(resp)
}
})
t.Run("version set", func(t *testing.T) {
GitCommit = "my-git-commit"
w := httptest.NewRecorder()
if err := version(storage.RateLimitedGraph{}, w, nil); err != nil {
t.Fatal(err)
}
var resp struct {
Version string `json:"version"`
}
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
t.Fatal(err)
} else if resp.Version != "my-git-commit" {
t.Fatal(resp)
}
})
}
func TestDelay(t *testing.T) {
defer func() {
os.Setenv("DELAY", "0ms")
}()
os.Args = os.Args[:1]
os.Setenv("DELAY", "100ms")
r := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
start := time.Now()
if err := delay(w, r); err != nil {
t.Fatal(err)
}
if since := time.Since(start); since < time.Millisecond*100 {
t.Fatal(since)
}
r = httptest.NewRequest("GET", "/", nil)
ctx, can := context.WithCancel(r.Context())
can()
r = r.WithContext(ctx)
w = httptest.NewRecorder()
start = time.Now()
if err := delay(w, r); err == nil {
t.Fatal(err)
}
if since := time.Since(start); since > time.Millisecond*99 {
t.Fatal(since)
}
}

10
.view/markdown.go Normal file
View File

@@ -0,0 +1,10 @@
package view
const (
markdownHead = `<div>
<style scoped>
@charset"UTF-8";button,input,textarea{transition:background-color 0.1s linear, border-color 0.1s linear, color 0.1s linear, box-shadow 0.1s linear, transform 0.1s ease}h1{font-size:2.2em;margin-top:0}h1,h2,h3,h4,h5,h6{margin-bottom:12px}h1,h2,h3,h4,h5,h6,strong{color:#000000}b,h1,h2,h3,h4,h5,h6,strong,th{font-weight:600}blockquote{border-left:4px solid #0096bfab;margin:1.5em 0;padding:0.5em 1em;font-style:italic}blockquote > footer{margin-top:10px;font-style:normal}blockquote cite{font-style:normal}address{font-style:normal}a[href^='mailto']::before{content:'📧 '}a[href^='tel']::before{content:'📞 '}a[href^='sms']::before{content:'💬 '}button,input[type='submit'],input[type='button'],input[type='checkbox']{cursor:pointer}input:not([type='checkbox']):not([type='radio']),select{display:block}button,input,select,textarea{color:#000000;background-color:#efefef;font-family:inherit;font-size:inherit;margin-right:6px;margin-bottom:6px;padding:10px;border:none;border-radius:6px;outline:none}input:not([type='checkbox']):not([type='radio']),select,button,textarea{-webkit-appearance:none}textarea{margin-right:0;width:100%;box-sizing:border-box;resize:vertical}button,input[type='submit'],input[type='button']{padding-right:30px;padding-left:30px}button:hover,input[type='submit']:hover,input[type='button']:hover{background:#dddddd}button:focus,input:focus,select:focus,textarea:focus{box-shadow:0 0 0 2px #0096bfab}input[type='checkbox']:active,input[type='radio']:active,input[type='submit']:active,input[type='button']:active,button:active{transform:translateY(2px)}button:disabled,input:disabled,select:disabled,textarea:disabled{cursor:not-allowed;opacity:0.5}::-webkit-input-placeholder{color:#949494}:-ms-input-placeholder{color:#949494}::-ms-input-placeholder{color:#949494}::placeholder{color:#949494}a{text-decoration:none;color:#0076d1}a:hover{text-decoration:underline}code,kbd{background:#efefef;color:#000000;padding:5px;border-radius:6px}pre > code{padding:10px;display:block;overflow-x:auto}img{max-width:100%}hr{border:none;border-top:1px solid #dbdbdb}table{border-collapse:collapse;margin-bottom:10px;width:100%}td,th{padding:6px;text-align:left}th{border-bottom:1px solid #dbdbdb}tbody tr:nth-child(even){background-color:#efefef}::-webkit-scrollbar{height:10px;width:10px}::-webkit-scrollbar-track{background:#efefef;border-radius:6px}::-webkit-scrollbar-thumb{background:#d5d5d5;border-radius:6px}::-webkit-scrollbar-thumb:hover{background:#c4c4c4}
</style>
`
markdownTail = `</div>`
)

82
.view/port.go Normal file
View File

@@ -0,0 +1,82 @@
package view
import (
"encoding/json"
"io/ioutil"
"local/dndex/storage"
"local/dndex/storage/entity"
"net/http"
"github.com/buger/jsonparser"
)
func port(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
switch r.Method {
case http.MethodGet:
return portGet(g, w, r)
case http.MethodPost:
return portPost(g, w, r)
default:
http.NotFound(w, r)
return nil
}
}
func portGet(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
namespace, err := getNamespace(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return nil
}
ones, err := g.List(r.Context(), namespace)
if err != nil {
return err
}
return json.NewEncoder(w).Encode(map[string]interface{}{namespace: ones})
}
func portPost(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
namespace, err := getNamespace(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return nil
}
b, err := ioutil.ReadAll(r.Body)
if err != nil {
return err
}
inserted := 0
var errIn error
_, err = jsonparser.ArrayEach(b, func(b []byte, _ jsonparser.ValueType, _ int, err error) {
if err != nil {
errIn = err
return
}
o := entity.One{}
if err := json.Unmarshal(b, &o); err != nil {
errIn = err
return
}
if err := g.Insert(r.Context(), namespace, o); err != nil {
errIn = err
return
}
inserted += 1
}, namespace)
if err != nil {
return err
}
if errIn != nil {
return errIn
}
return json.NewEncoder(w).Encode(map[string]int{namespace: inserted})
}

125
.view/port_test.go Normal file
View File

@@ -0,0 +1,125 @@
package view
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"local/dndex/storage"
"local/dndex/storage/entity"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
)
func TestPort(t *testing.T) {
os.Args = os.Args[:1]
os.Setenv("AUTH", "false")
g := storage.NewRateLimitedGraph()
reset := func() {
if err := g.Delete(context.TODO(), "col", map[string]string{}); err != nil {
t.Fatal(err)
}
for _, name := range []string{"A", "B", "C"} {
if err := g.Insert(context.TODO(), "col", entity.One{ID: "id-" + name, Name: name}); err != nil {
t.Fatal(err)
}
}
}
handler := jsonHandler(g)
t.Run("port: wrong path", func(t *testing.T) {
reset()
r := httptest.NewRequest(http.MethodGet, "/port/", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusNotFound {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("port: wrong method", func(t *testing.T) {
reset()
r := httptest.NewRequest(http.MethodPut, "/port", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusNotFound {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("port: post: no namespace", func(t *testing.T) {
reset()
r := httptest.NewRequest(http.MethodPost, "/port", strings.NewReader(``))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusBadRequest {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("port: get: no namespace", func(t *testing.T) {
reset()
r := httptest.NewRequest(http.MethodGet, "/port", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusBadRequest {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("port: ex => im", func(t *testing.T) {
reset()
r := httptest.NewRequest(http.MethodGet, "/port?namespace=col", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
b, err := ioutil.ReadAll(w.Body)
if err != nil {
t.Fatal(err)
}
if err := g.Delete(context.TODO(), "col", map[string]string{}); err != nil {
t.Fatal(err)
}
if ones, err := g.List(context.TODO(), "col"); err != nil {
t.Fatal(err)
} else if len(ones) != 0 {
t.Fatal(len(ones))
}
r = httptest.NewRequest(http.MethodPost, "/port?namespace=col", bytes.NewReader(b))
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var resp map[string]int
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
n := 0
for k, v := range resp {
if k != "col" || v == 0 {
t.Fatal(resp)
}
n = v
}
if n == 0 {
t.Fatal(n, resp, string(w.Body.Bytes()))
}
if ones, err := g.List(context.TODO(), "col"); err != nil {
t.Fatal(err)
} else if len(ones) != n {
t.Fatal(len(ones))
}
})
}

46
.view/register.go Normal file
View File

@@ -0,0 +1,46 @@
package view
import (
"encoding/json"
"local/dndex/storage"
"local/dndex/storage/entity"
"net/http"
"github.com/google/uuid"
)
func register(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
switch r.Method {
case http.MethodPost:
return registerPost(g, w, r)
default:
http.NotFound(w, r)
return nil
}
}
func registerPost(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
namespace, err := getAuthNamespace(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return nil
}
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return nil
}
password := r.FormValue("password")
if len(password) == 0 && r.URL.Query().Get("public") == "" {
http.Error(w, `{"error": "password required"}`, http.StatusBadRequest)
return nil
}
one := entity.One{
ID: uuid.New().String(),
Name: UserKey,
Title: password,
}
if err := g.Insert(r.Context(), namespace, one); err != nil {
return err
}
return json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

123
.view/register_test.go Normal file
View File

@@ -0,0 +1,123 @@
package view
import (
"fmt"
"local/dndex/storage"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/google/uuid"
)
func TestRegister(t *testing.T) {
os.Args = os.Args[:1]
os.Setenv("AUTH", "true")
defer os.Setenv("AUTH", "false")
g := storage.NewRateLimitedGraph()
handler := jsonHandler(g)
t.Run("register: wrong method", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/register", nil)
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusNotFound {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("register: no namespace", func(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/register", strings.NewReader(""))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusBadRequest {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("register: no password", func(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/register?namespace="+uuid.New().String(), strings.NewReader(""))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusBadRequest {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("register: public", func(t *testing.T) {
ns := uuid.New().String()
r := httptest.NewRequest(http.MethodPost, "/register?public=true&namespace="+ns, strings.NewReader(""))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
r = httptest.NewRequest(http.MethodTrace, "/who?namespace="+ns, nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("register", func(t *testing.T) {
ns := uuid.New().String()
r := httptest.NewRequest(http.MethodPost, "/register?namespace="+ns, strings.NewReader(`password=123`))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
t.Logf("/register: %s", w.Body.Bytes())
r = httptest.NewRequest(http.MethodTrace, "/who?namespace="+ns, nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusSeeOther {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
t.Logf("noauth /who: %s", w.Body.Bytes())
rawtoken := getCookie(NewAuthKey, w.Header())
token, err := aesDec("123", rawtoken)
if err != nil {
t.Fatal(err)
}
r = httptest.NewRequest(http.MethodTrace, "/who?namespace="+ns, nil)
r.Header.Set("Cookie", fmt.Sprintf("%s=%s", AuthKey, token))
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
t.Logf("auth /who: %s", w.Body.Bytes())
})
}
func getCookie(key string, header http.Header) string {
cookies, _ := header["Set-Cookie"]
if len(cookies) == 0 {
cookies, _ = header["Cookie"]
}
for i := range cookies {
value := strings.Split(cookies[i], ";")[0]
k := value[:strings.Index(value, "=")]
v := value[strings.Index(value, "=")+1:]
if k == key {
return v
}
}
return ""
}

355
.view/who.go Normal file
View File

@@ -0,0 +1,355 @@
package view
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"local/dndex/config"
"local/dndex/storage"
"local/dndex/storage/entity"
"local/dndex/storage/operator"
"net/http"
"net/url"
"path"
"regexp"
"sort"
"strings"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/google/uuid"
"github.com/iancoleman/orderedmap"
"go.mongodb.org/mongo-driver/bson"
)
const (
querySort = "sort"
queryOrder = "order"
)
func who(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
namespace, err := getNamespace(r)
if err != nil {
http.NotFound(w, r)
return nil
}
switch r.Method {
case http.MethodGet:
return whoGet(namespace, g, w, r)
case http.MethodPut:
return whoPut(namespace, g, w, r)
case http.MethodPost:
return whoPost(namespace, g, w, r)
case http.MethodDelete:
return whoDelete(namespace, g, w, r)
case http.MethodPatch:
return whoPatch(namespace, g, w, r)
case http.MethodTrace:
return whoTrace(namespace, g, w, r)
default:
http.NotFound(w, r)
return nil
}
}
func whoGet(namespace string, g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
id, err := getCleanID(r)
if err != nil {
return whoTrace(namespace, g, w, r)
}
_, light := r.URL.Query()["light"]
_, md := r.URL.Query()["md"]
ones, err := g.ListCaseInsensitive(r.Context(), namespace, id)
if err != nil {
return err
}
if len(ones) == 0 {
w.WriteHeader(http.StatusNotFound)
return json.NewEncoder(w).Encode(entity.One{})
}
if len(ones) > 1 {
return fmt.Errorf("more than one result found matching %q: %+v", id, ones)
}
one := ones[0]
baseUrl := *r.URL
if baseUrl.Scheme == "" {
baseUrl.Scheme = "http"
}
if baseUrl.Host == "" {
baseUrl.Host = r.Host
}
baseUrl.Path = ""
baseUrl.RawQuery = ""
for k := range one.Attachments {
if _, err := url.Parse(one.Attachments[k]); err != nil || !strings.Contains(one.Attachments[k], ":") {
one.Attachments[k] = baseUrl.String() + path.Join("/", config.New().FilePrefix, namespace, one.Attachments[k])
}
}
if !light && len(one.Connections) > 0 {
ones, err := g.ListCaseInsensitive(r.Context(), namespace, one.Peers()...)
if err != nil {
return err
}
for _, another := range ones {
another.Relationship = one.Connections[another.Name].Relationship
one.Connections[another.Name] = another
}
}
b, err := json.MarshalIndent(one, "", " ")
if err != nil {
return err
}
if _, ok := r.URL.Query()[querySort]; ok {
m := bson.M{}
if err := json.Unmarshal(b, &m); err != nil {
return err
}
ones := make([]entity.One, len(one.Connections))
i := 0
for _, v := range one.Connections {
ones[i] = v
i++
}
m[entity.Connections] = sortOnesObject(ones, r)
b, err = json.MarshalIndent(m, "", " ")
if err != nil {
return err
}
}
if md {
m := bson.M{}
if err := json.Unmarshal(b, &m); err != nil {
return err
}
renderer := html.NewRenderer(html.RendererOptions{Flags: html.CommonFlags | html.TOC})
parser := parser.NewWithExtensions(parser.CommonExtensions | parser.HeadingIDs | parser.AutoHeadingIDs | parser.Titleblock)
m["md"] = markdownHead + string(markdown.ToHTML([]byte(one.Text), parser, renderer)) + markdownTail
b, err = json.MarshalIndent(m, "", " ")
if err != nil {
return err
}
}
_, err = io.Copy(w, bytes.NewReader(b))
return err
}
func whoPut(namespace string, g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
id, err := getID(r)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return err
}
reader := bytes.NewReader(body)
operation := entity.One{}
if err := json.NewDecoder(reader).Decode(&operation); err != nil {
return err
}
if operation.Name != "" && operation.Name != id {
http.Error(w, `{"error":"names differ between URL and request body"}`, http.StatusBadRequest)
return nil
}
if operation.Modified != 0 {
http.Error(w, `{"error":"cannot specify modified in request body"}`, http.StatusBadRequest)
return nil
}
op := bson.M{}
if err := json.Unmarshal(body, &op); err != nil {
return err
}
if err := g.Update(r.Context(), namespace, entity.One{Name: id}, operator.SetMany{Value: op}); err != nil {
return err
}
return whoGet(namespace, g, w, r)
}
func whoPost(namespace string, g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
id, err := getID(r)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
}
one := entity.One{}
if err := json.NewDecoder(r.Body).Decode(&one); err != nil {
return err
}
if one.Name != "" && one.Name != id {
http.Error(w, `{"error":"names differ between URL and request body"}`, http.StatusBadRequest)
return nil
}
one.Name = id
if one.ID != "" {
http.Error(w, `{"error":"cannot specify ID in body"}`, http.StatusBadRequest)
return nil
}
one.ID = uuid.New().String()
if err := g.Insert(r.Context(), namespace, one); err != nil {
return err
}
return whoGet(namespace, g, w, r)
}
func whoDelete(namespace string, g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
id, err := getCleanID(r)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
}
_, ok := r.URL.Query()["connection"]
if ok {
return whoDeleteConnections(namespace, g, w, r)
}
if err := g.Delete(r.Context(), namespace, operator.CaseInsensitive{Key: entity.Name, Value: id}); err != nil {
return err
}
return json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func whoDeleteConnections(namespace string, g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
id, err := getID(r)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
}
connections, ok := r.URL.Query()["connection"]
if !ok {
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{"error": "must provide connections to delete"})
}
one := entity.One{Name: id}
for _, connection := range connections {
path := fmt.Sprintf("%s.%s", entity.Connections, connection)
if err := g.Update(r.Context(), namespace, one.Query(), operator.Unset(path)); err != nil {
return err
}
}
return whoGet(namespace, g, w, r)
}
func whoPatch(namespace string, g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
id, err := getID(r)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
}
one := entity.One{}
if err := json.NewDecoder(r.Body).Decode(&one); err != nil {
return err
}
if one.Name == "" {
http.Error(w, `{"error":"no name provided"}`, http.StatusBadRequest)
return nil
}
relationship := one.Relationship
one.Relationship = ""
if err := g.Insert(r.Context(), namespace, one); err != nil && !strings.Contains(err.Error(), "ollision") {
return err
}
one.Relationship = relationship
if err := g.Update(r.Context(), namespace, entity.One{Name: id}, operator.Set{Key: fmt.Sprintf("%s.%s", entity.Connections, one.Name), Value: one.Peer()}); err != nil {
return err
}
return whoGet(namespace, g, w, r)
}
func whoTrace(namespace string, g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
ones, err := g.ListCaseInsensitive(r.Context(), namespace)
if err != nil {
return err
}
ones = sortOnes(ones, r)
names := make([]string, len(ones))
for i := range ones {
names[i] = ones[i].Name
}
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(names)
}
func getCleanID(r *http.Request) (string, error) {
id, err := getID(r)
return sanitize(id), err
}
func getID(r *http.Request) (string, error) {
id := r.URL.Query().Get("id")
if id == "" {
return "", errors.New("no id provided")
}
return id, nil
}
func sortOnesObject(ones []entity.One, r *http.Request) interface{} {
ones = sortOnes(ones, r)
m := orderedmap.New()
for _, one := range ones {
m.Set(one.Name, one)
}
return m
}
func sortOnes(ones []entity.One, r *http.Request) []entity.One {
sorting := sanitize(r.URL.Query().Get(querySort))
if sorting == "" {
sorting = entity.Modified
}
order := sanitize(r.URL.Query().Get(queryOrder))
if order == "" {
order = "-1"
}
asc := order != "-1"
sort.Slice(ones, func(i, j int) bool {
if sorting == entity.Name {
return (asc && ones[i].Name < ones[j].Name) || (!asc && ones[i].Name > ones[j].Name)
}
ib, _ := json.Marshal(ones[i])
jb, _ := json.Marshal(ones[j])
var im, jm bson.M
json.Unmarshal(ib, &im)
json.Unmarshal(jb, &jm)
iv, _ := im[sorting]
jv, _ := jm[sorting]
is := fmt.Sprint(iv)
js := fmt.Sprint(jv)
return (asc && is < js) || (!asc && is > js)
})
return ones
}
func sanitize(s string) string {
re := regexp.MustCompile(`[^a-zA-Z0-9- _]`)
s = re.ReplaceAllString(s, `.`)
return s
}

963
.view/who_test.go Normal file
View File

@@ -0,0 +1,963 @@
package view
import (
"bytes"
"context"
"encoding/json"
"fmt"
"local/dndex/config"
"local/dndex/storage"
"local/dndex/storage/entity"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path"
"regexp"
"sort"
"strings"
"testing"
"time"
"github.com/buger/jsonparser"
"github.com/google/uuid"
)
func TestWho(t *testing.T) {
t.Parallel()
os.Args = os.Args[:1]
t.Run("get no namespace is 404", func(t *testing.T) {
handler, _, iwant, _, can := fresh(t)
defer can()
r := httptest.NewRequest(http.MethodGet, "/who?id="+iwant.Name, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusNotFound {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("get fake", func(t *testing.T) {
handler, _, iwant, _, can := fresh(t)
defer can()
r := httptest.NewRequest(http.MethodGet, "/who?namespace=col&id=FAKER"+iwant.Name, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusNotFound {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("get real", func(t *testing.T) {
handler, _, iwant, _, can := fresh(t)
defer can()
r := httptest.NewRequest(http.MethodGet, "/who?namespace=col&id="+iwant.Name, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var o entity.One
if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil {
t.Fatal(err)
}
if fmt.Sprint(o) == fmt.Sprint(iwant) {
t.Fatal(o, iwant)
}
if len(o.Connections) != len(iwant.Connections) {
t.Fatal(len(o.Connections), len(iwant.Connections))
}
if len(o.Attachments) != len(iwant.Attachments) {
t.Fatal(len(o.Attachments), len(iwant.Attachments))
}
for k := range o.Attachments {
if _, ok := iwant.Attachments[k]; !ok {
t.Fatal(k, ok)
}
if o.Attachments[k] == iwant.Attachments[k] {
t.Fatal(k, o.Attachments[k], iwant.Attachments[k])
}
if !strings.HasSuffix(o.Attachments[k], path.Join(config.New().FilePrefix, "col", iwant.Attachments[k])) {
t.Fatal(k, o.Attachments[k], iwant.Attachments[k])
}
if !strings.HasPrefix(o.Attachments[k], "http://") {
t.Fatal(k, o.Attachments[k], iwant.Attachments[k])
}
}
iwant.Attachments = o.Attachments
iwant.Connections = o.Connections
iwant.Modified = 0
o.Modified = 0
if fmt.Sprint(o) != fmt.Sprint(iwant) {
t.Fatalf("after resolving connections and modified, iwant and got differ: \nwant %+v\n got %+v", iwant, o)
}
b, _ := json.MarshalIndent(o, "", " ")
t.Logf("POST GET:\n%s", b)
})
t.Run("get real md", func(t *testing.T) {
handler, _, iwant, _, can := fresh(t)
defer can()
r := httptest.NewRequest(http.MethodGet, "/who?namespace=col&md&id="+iwant.Name, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var o entity.One
if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil {
t.Fatal(err)
}
if fmt.Sprint(o) == fmt.Sprint(iwant) {
t.Fatal(o, iwant)
}
if len(o.Connections) != len(iwant.Connections) {
t.Fatal(len(o.Connections), len(iwant.Connections))
}
iwant.Connections = o.Connections
iwant.Attachments = o.Attachments
iwant.Modified = 0
o.Modified = 0
if fmt.Sprint(o) != fmt.Sprint(iwant) {
t.Fatalf("after resolving connections and modified, iwant and got differ: \nwant %+v\n got %+v", iwant, o)
}
if md, err := jsonparser.GetString(w.Body.Bytes(), "md"); err != nil {
t.Fatal(err)
} else if !strings.HasPrefix(strings.TrimSpace(md), "<") || !strings.Contains(md, iwant.Text) {
t.Fatal(iwant.Text, md)
}
b, _ := json.MarshalIndent(o, "", " ")
t.Logf("POST GET:\n%s", b)
})
t.Run("get real light", func(t *testing.T) {
handler, _, iwant, _, can := fresh(t)
defer can()
r := httptest.NewRequest(http.MethodGet, "/who?namespace=col&light&id="+iwant.Name, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var o entity.One
if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil {
t.Fatal(err)
}
if fmt.Sprint(o) == fmt.Sprint(iwant) {
t.Fatal(o, iwant)
}
if len(o.Connections) != len(iwant.Connections) {
t.Fatal(len(o.Connections), len(iwant.Connections))
}
iwant.Connections = o.Connections
iwant.Attachments = o.Attachments
iwant.Modified = 0
o.Modified = 0
if fmt.Sprint(o) != fmt.Sprint(iwant) {
t.Fatalf("after resolving connections and modified, iwant and got differ: \nwant %+v\n got %+v", iwant, o)
}
for _, connection := range iwant.Connections {
if len(connection.Connections) != 0 {
t.Fatal(connection)
}
}
b, _ := json.MarshalIndent(o, "", " ")
t.Logf("POST GET:\n%s", b)
})
t.Run("get real but case is wrong", func(t *testing.T) {
handler, _, iwant, _, can := fresh(t)
defer can()
r := httptest.NewRequest(http.MethodGet, "/who?namespace=col&id="+strings.ToUpper(iwant.Name), nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var o entity.One
if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil {
t.Fatal(err)
}
if fmt.Sprint(o) == fmt.Sprint(iwant) {
t.Fatal(o, iwant)
}
if len(o.Connections) != len(iwant.Connections) {
t.Fatal(len(o.Connections), len(iwant.Connections))
}
iwant.Connections = o.Connections
iwant.Attachments = o.Attachments
iwant.Modified = 0
o.Modified = 0
if fmt.Sprint(o) != fmt.Sprint(iwant) {
t.Fatalf("after resolving connections and modified, iwant and got differ: \nwant %+v\n got %+v", iwant, o)
}
b, _ := json.MarshalIndent(o, "", " ")
t.Logf("POST GET:\n%s", b)
})
t.Run("get sorted type asc/desc", func(t *testing.T) {
for _, order := range []string{"1", "-1"} {
handler, ones, _, _, can := fresh(t)
defer can()
want := ones[len(ones)-1]
r := httptest.NewRequest(http.MethodGet, "/who?namespace=col&light&sort=type&order="+order+"&id="+want.Name, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var o entity.One
if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil {
t.Fatal(err)
}
if len(o.Connections) < 5 {
t.Fatal(len(o.Connections))
}
if len(o.Connections) != len(want.Connections) {
t.Fatal(len(want.Connections), len(o.Connections))
}
titles := []string{}
for _, v := range want.Connections {
if len(v.Type) == 0 {
t.Fatal(v.Type)
}
titles = append(titles, v.Type)
}
sort.Strings(titles)
if order == "-1" {
for i := 0; i < len(titles)/2; i++ {
tmp := titles[len(titles)-1-i]
titles[len(titles)-1-i] = titles[i]
titles[i] = tmp
}
}
pattern := strings.Join(titles, ".*")
pattern = strings.Replace(pattern, "-", ".", -1)
if !regexp.MustCompile(pattern).Match(bytes.Replace(w.Body.Bytes(), []byte("\n"), []byte(" "), -1)) {
t.Fatal(order, pattern, string(w.Body.Bytes()))
}
}
})
t.Run("put fake", func(t *testing.T) {
handler, _, iwant, _, can := fresh(t)
defer can()
r := httptest.NewRequest(http.MethodPut, "/who?namespace=col&id=FAKER"+iwant.Name, strings.NewReader(`{"title":"this should fail to find someone"}`))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusNotFound {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("put real", func(t *testing.T) {
handler, _, iwant, _, can := fresh(t)
defer can()
r := httptest.NewRequest(http.MethodPut, "/who?namespace=col&id="+iwant.Name, strings.NewReader(`{"title":"this should work"}`))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var o entity.One
if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil {
t.Fatal(err)
}
if len(o.Connections) != len(iwant.Connections) {
t.Fatalf("wrong number of connections returned: want %v, got %v", len(iwant.Connections), len(o.Connections))
}
if o.Title != "this should work" {
t.Fatalf("failed to PUT a new title: %+v", o)
}
b, _ := json.MarshalIndent(o, "", " ")
t.Logf("POST PUT:\n%s", b)
})
t.Run("post exists", func(t *testing.T) {
handler, _, iwant, _, can := fresh(t)
defer can()
want := iwant
want.ID = ""
r := httptest.NewRequest(http.MethodPost, "/who?namespace=col&id="+want.Name, strings.NewReader(`{"title":"this should fail to insert"}`))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusConflict {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("post real", func(t *testing.T) {
handler, _, want, _, can := fresh(t)
defer can()
iwant := want
iwant.Name = ""
iwant.ID = ""
b, err := json.Marshal(iwant)
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest(http.MethodPost, "/who?namespace=col&id=NEWBIE"+want.Name, bytes.NewReader(b))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var o entity.One
if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil {
t.Fatal(err)
}
if len(o.Connections) != len(iwant.Connections) {
t.Fatalf("wrong number of connections returned: want %v, got %v", len(iwant.Connections), len(o.Connections))
}
if o.Name != "NEWBIE"+want.Name {
t.Fatalf("failed to POST specified name: %+v", o)
}
b, _ = json.MarshalIndent(o, "", " ")
t.Logf("POST POST:\n%s", b)
})
t.Run("post real with spaces, #, and special chars in name", func(t *testing.T) {
handler, _, want, _, can := fresh(t)
defer can()
want.Name = "hello world #1 e ę"
want.ID = ""
want.Connections = nil
b, err := json.Marshal(want)
if err != nil {
t.Fatal(err)
}
url := url.URL{
Scheme: "http",
Host: "me.me.me",
Path: "/who",
RawQuery: (url.Values{"namespace": []string{"col"}, "id": []string{want.Name}}).Encode(),
}
r := httptest.NewRequest(http.MethodPost, url.String(), bytes.NewReader(b))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var o entity.One
if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil {
t.Fatal(err)
}
if o.Name != want.Name {
t.Fatalf("failed to POST specified name: %+v", o)
}
b, _ = json.MarshalIndent(o, "", " ")
t.Logf("POST POST:\n%q\n%s", url.String(), b)
})
t.Run("delete real", func(t *testing.T) {
handler, _, want, _, can := fresh(t)
defer can()
r := httptest.NewRequest(http.MethodDelete, "/who?namespace=col&id=NEWBIE"+want.Name, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
r = httptest.NewRequest(http.MethodGet, "/who?namespace=col&id=NEWBIE"+want.Name, nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusNotFound {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("delete real with %20%23 (' #')", func(t *testing.T) {
handler, _, want, _, can := fresh(t)
defer can()
want.Name = "hello world #4"
want.ID = ""
want.Connections = nil
b, err := json.Marshal(want)
if err != nil {
t.Fatal(err)
}
url := url.URL{
Scheme: "http",
Host: "me.me.me",
Path: "/who",
RawQuery: "namespace=col&id=hello%20world%20%234",
}
t.Logf("using url %s", url.String())
r := httptest.NewRequest(http.MethodPost, url.String(), bytes.NewReader(b))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("(%d) %s", w.Code, w.Body.Bytes())
}
r = httptest.NewRequest(http.MethodDelete, url.String(), nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
r = httptest.NewRequest(http.MethodGet, url.String(), nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusNotFound {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
t.Logf("can post+del %s", url.String())
})
t.Run("delete real with special chars", func(t *testing.T) {
handler, _, want, _, can := fresh(t)
defer can()
want.Name = "hello world #1 e ę"
want.ID = ""
want.Connections = nil
b, err := json.Marshal(want)
if err != nil {
t.Fatal(err)
}
url := url.URL{
Scheme: "http",
Host: "me.me.me",
Path: "/who",
RawQuery: (url.Values{"namespace": []string{"col"}, "id": []string{want.Name}}).Encode(),
}
r := httptest.NewRequest(http.MethodPost, url.String(), bytes.NewReader(b))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
r = httptest.NewRequest(http.MethodDelete, url.String(), nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
r = httptest.NewRequest(http.MethodGet, url.String(), nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusNotFound {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
t.Logf("can post+del %s", url.String())
})
t.Run("delete fake", func(t *testing.T) {
handler, _, want, _, can := fresh(t)
defer can()
r := httptest.NewRequest(http.MethodDelete, "/who?namespace=col&id=FAKER"+want.Name, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("delete regexp should be sanitized", func(t *testing.T) {
handler, _, _, _, can := fresh(t)
defer can()
r := httptest.NewRequest(http.MethodDelete, "/who?namespace=col&id=.*", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
r = httptest.NewRequest(http.MethodTrace, "/who?namespace=col", nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var v []string
if err := json.Unmarshal(w.Body.Bytes(), &v); err != nil {
t.Fatalf("%v: %s", err, w.Body.Bytes())
}
if len(v) < 5 {
t.Fatal(len(v))
}
t.Logf("%+v", v)
})
t.Run("patch fake", func(t *testing.T) {
handler, _, want, _, can := fresh(t)
defer can()
r := httptest.NewRequest(http.MethodPatch, "/who?namespace=col&id=FAKER"+want.Name, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code == http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("patch real against existing", func(t *testing.T) {
handler, ones, _, _, can := fresh(t)
defer can()
from := ones[4]
push := ones[10].Peer()
push.Relationship = "spawn"
push.ID = uuid.New().String()
b, err := json.Marshal(push)
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest(http.MethodPatch, "/who?namespace=col&id="+from.Name, bytes.NewReader(b))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var got entity.One
if err := json.NewDecoder(w.Body).Decode(&got); err != nil {
t.Fatal(err)
}
if fmt.Sprint(got) == fmt.Sprint(from) {
t.Fatal(got)
}
got.Modified = 0
from.Modified = 0
if len(got.Connections) != len(from.Connections)+1 {
t.Fatal(len(got.Connections), len(from.Connections)+1)
}
gotPush, ok := got.Connections[push.Name]
if !ok {
t.Fatal("cant find pushed connection from remote")
}
got.Connections = nil
from.Connections = nil
got.Attachments = nil
from.Attachments = nil
if fmt.Sprint(got) != fmt.Sprint(from) {
t.Fatalf("without connections and modified, got != want: want \n %+v, got \n %+v", from, got)
}
pushPeer := push.Peer()
gotPush = gotPush.Peer()
pushPeer.Modified = 0
gotPush.Modified = 0
if fmt.Sprint(gotPush) != fmt.Sprint(pushPeer) {
t.Fatal("\n", gotPush, "\n", pushPeer)
}
t.Logf("%s", w.Body.Bytes())
})
t.Run("patch real", func(t *testing.T) {
handler, _, want, _, can := fresh(t)
defer can()
iwant := want
iwant.Relationship = "spawn"
iwant.Name = "child of " + want.Name
b, err := json.Marshal(iwant)
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest(http.MethodPatch, "/who?namespace=col&id="+want.Name, bytes.NewReader(b))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var got entity.One
if err := json.NewDecoder(w.Body).Decode(&got); err != nil {
t.Fatal(err)
}
if fmt.Sprint(got) == fmt.Sprint(entity.One{}) {
t.Fatal(got)
}
iwant = want
if len(got.Connections) != len(want.Connections)+1 {
t.Fatal(len(got.Connections), len(want.Connections)+1)
}
got.Connections = want.Connections
got.Attachments = want.Attachments
got.Modified = 0
want.Modified = 0
if fmt.Sprint(got) != fmt.Sprint(want) {
t.Fatalf("without connections and modified, got != want: want \n %+v, got \n %+v", want, got)
}
t.Logf("%s", w.Body.Bytes())
})
t.Run("trace fake", func(t *testing.T) {
handler, _, _, _, can := fresh(t)
defer can()
r := httptest.NewRequest(http.MethodTrace, "/who?namespace=notcol", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("trace real", func(t *testing.T) {
handler, _, _, _, can := fresh(t)
defer can()
r := httptest.NewRequest(http.MethodTrace, "/who?namespace=col", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var v []string
if err := json.Unmarshal(w.Body.Bytes(), &v); err != nil {
t.Fatalf("%v: %s", err, w.Body.Bytes())
}
if len(v) < 5 {
t.Fatal(len(v))
}
t.Logf("%+v", v)
})
t.Run("get without id == trace real", func(t *testing.T) {
handler, _, _, _, can := fresh(t)
defer can()
r := httptest.NewRequest(http.MethodGet, "/who?namespace=col", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var v []string
if err := json.Unmarshal(w.Body.Bytes(), &v); err != nil {
t.Fatalf("%v: %s", err, w.Body.Bytes())
}
if len(v) < 5 {
t.Fatal(len(v))
}
t.Logf("%+v", v)
})
t.Run("trace real sorted asc/desc name", func(t *testing.T) {
handler, _, _, _, can := fresh(t)
defer can()
for _, order := range []string{"1", "-1"} {
r := httptest.NewRequest(http.MethodTrace, "/who?namespace=col&sort=name&order="+order, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var v []string
if err := json.Unmarshal(w.Body.Bytes(), &v); err != nil {
t.Fatalf("%v: %s", err, w.Body.Bytes())
}
if len(v) < 5 {
t.Fatal(len(v))
}
for i := range v {
if i == 0 {
continue
}
if (v[i] < v[i-1] && order == "1") || (v[i] > v[i-1] && order == "-1") {
t.Fatalf("not sorted: %s: %+v", order, v)
}
}
}
})
t.Run("delete connection 1 of 0 noop but ok", func(t *testing.T) {
handler, _, want, _, can := fresh(t)
defer can()
if len(want.Connections) > 0 {
t.Fatal(want)
}
r := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/who?namespace=col&id=%s&connection=%s", want.Name, "fake"), nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
r = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/who?namespace=col&id=%s", want.Name), nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var o entity.One
if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil {
t.Fatal(err)
}
if len(o.Connections) > 0 {
t.Fatal(o.Connections)
}
})
t.Run("delete connection 1 of 1 ok", func(t *testing.T) {
handler, ones, _, _, can := fresh(t)
defer can()
want := ones[5]
deleted := want.Peers()[0]
r := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/who?namespace=col&id=%s&connection=%s", want.Name, deleted), nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
r = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/who?namespace=col&id=%s", want.Name), nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var o entity.One
if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil {
t.Fatal(err)
}
if _, ok := o.Connections[deleted]; ok {
t.Fatal(deleted, o.Connections)
}
})
t.Run("delete connection 1 of 4 ok", func(t *testing.T) {
handler, ones, _, _, can := fresh(t)
defer can()
want := ones[0]
put := entity.One{
Name: want.Name,
Connections: map[string]entity.One{
ones[1].Name: ones[1].Peer(),
ones[2].Name: ones[2].Peer(),
ones[3].Name: ones[3].Peer(),
ones[4].Name: ones[4].Peer(),
},
}
b, err := json.Marshal(put)
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/who?namespace=col&id=%s", want.Name), bytes.NewReader(b))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
want.Connections = put.Connections
r = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/who?namespace=col&id=%s&light", want.Name), nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
var o entity.One
if err := json.NewDecoder(w.Body).Decode(&o); err != nil {
t.Fatal(err)
}
o.Modified = 0
want.Modified = 0
if len(o.Connections) != len(put.Connections) {
t.Fatal(o.Connections)
}
for k := range o.Connections {
a := want.Connections[k]
a.Modified = 0
want.Connections[k] = a
b := o.Connections[k]
b.Modified = 0
o.Connections[k] = b
}
o.Attachments = want.Attachments
if fmt.Sprint(o) != fmt.Sprint(want) {
wantjs, _ := json.MarshalIndent(want, "", " ")
ojs, _ := json.MarshalIndent(o, "", " ")
t.Fatalf("GET put != expected: want:\n%s, got \n%s", wantjs, ojs)
}
forget := want.Peers()[0]
r = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/who?namespace=col&id=%s&connection=%s", want.Name, forget), nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
r = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/who?namespace=col&id=%s", want.Name), nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
o = entity.One{}
if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil {
t.Fatal(err)
}
if len(o.Connections) != len(put.Connections)-1 {
t.Fatalf("should've deleted %q but got %+v", forget, o.Connections)
}
})
}
func TestSortOnes(t *testing.T) {
oneA := entity.One{Name: "A", Title: "c", Modified: 2}
oneB := entity.One{Name: "B", Title: "b", Modified: 1}
oneC := entity.One{Name: "C", Title: "a", Modified: 3}
cases := map[string]struct {
ones []entity.One
sort string
order string
test func(entity.One, entity.One) bool
}{
"nothing to sort": {},
"all the same": {
ones: []entity.One{oneA, oneA, oneA},
test: func(a, b entity.One) bool { return fmt.Sprint(a) == fmt.Sprint(b) },
},
"default: modified desc, but already ordered": {
ones: []entity.One{oneC, oneB, oneA},
test: func(a, b entity.One) bool { return a.Modified >= b.Modified },
},
"default: modified desc": {
ones: []entity.One{oneA, oneB, oneC},
test: func(a, b entity.One) bool { return a.Modified >= b.Modified },
},
"default: modified desc, custom entries": {
ones: []entity.One{
entity.One{Modified: 2},
entity.One{Modified: 5},
entity.One{Modified: 7},
entity.One{Modified: 3},
entity.One{Modified: 4},
entity.One{Modified: 1},
entity.One{Modified: 6},
},
test: func(a, b entity.One) bool { return a.Modified >= b.Modified },
},
"default=modified set=desc": {
ones: []entity.One{oneA, oneB, oneC},
order: "-1",
test: func(a, b entity.One) bool { return a.Modified >= b.Modified },
},
"set=modified default=desc": {
ones: []entity.One{oneA, oneB, oneC},
sort: entity.Modified,
test: func(a, b entity.One) bool { return a.Modified >= b.Modified },
},
"set=modified set=asc": {
ones: []entity.One{oneA, oneB, oneC},
sort: entity.Modified,
order: "1",
test: func(a, b entity.One) bool { return a.Modified <= b.Modified },
},
"set=title set=desc": {
ones: []entity.One{oneA, oneB, oneC},
sort: entity.Title,
order: "-1",
test: func(a, b entity.One) bool { return a.Title >= b.Title },
},
"set=title set=asc": {
ones: []entity.One{oneA, oneB, oneC},
sort: entity.Title,
order: "1",
test: func(a, b entity.One) bool { return a.Title <= b.Title },
},
"set=name set=desc": {
ones: []entity.One{oneA, oneB, oneC},
sort: entity.Name,
order: "-1",
test: func(a, b entity.One) bool { return a.Name >= b.Name },
},
"set=name set=asc": {
ones: []entity.One{oneA, oneB, oneC},
sort: entity.Name,
order: "1",
test: func(a, b entity.One) bool { return a.Name <= b.Name },
},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
q := url.Values{}
q.Set("sort", c.sort)
q.Set("order", c.order)
url := url.URL{
Path: "/",
RawQuery: q.Encode(),
}
r := httptest.NewRequest("GET", url.String(), nil)
ones := sortOnes(c.ones, r)
for i := range ones {
if i == 0 {
continue
}
if ok := c.test(ones[i-1], ones[i]); !ok {
t.Fatal(ok, ones[i-1], ones[i])
}
}
})
}
}
func fresh(t *testing.T) (http.Handler, []entity.One, entity.One, storage.RateLimitedGraph, func()) {
g := storage.NewRateLimitedGraph("")
if err := g.Delete(context.TODO(), "col", map[string]string{}); err != nil {
t.Fatal(err)
}
ones := fillDB(t, g)
handler := jsonHandler(g)
return handler, ones, ones[0], g, func() {
}
}
func fillDB(t *testing.T, g storage.RateLimitedGraph) []entity.One {
ones := make([]entity.One, 13)
for i := range ones {
ones[i] = randomOne()
for j := 0; j < i; j++ {
ones[i].Connections[ones[j].Name] = entity.One{
Name: ones[j].Name,
Type: ones[j].Type,
Relationship: ":D",
}
}
}
for i := range ones {
if err := g.Insert(context.TODO(), "col", ones[i]); err != nil {
t.Fatal(err)
}
if results, err := g.List(context.TODO(), "col", ones[i].Name); err != nil {
t.Fatal(err)
} else if len(results) != 1 {
t.Fatal(len(results))
} else if len(results[0].Connections) != len(ones[i].Connections) {
t.Fatal(len(results[0].Connections), len(ones[i].Connections))
} else if len(results[0].Connections) > 0 {
for k, v := range results[0].Connections {
if k == "" || v.Name == "" {
t.Fatalf("name is gone: %q:%+v", k, v)
}
}
}
}
return ones
}
func randomOne() entity.One {
return entity.One{
ID: "iddd-" + uuid.New().String()[:5],
Name: "name-" + uuid.New().String()[:5],
Type: "type-" + uuid.New().String()[:5],
Title: "titl-" + uuid.New().String()[:5],
Text: "text-" + uuid.New().String()[:5],
Modified: time.Now().UnixNano(),
Connections: map[string]entity.One{},
Attachments: map[string]string{
uuid.New().String()[:5]: uuid.New().String()[:5],
},
}
}