impl entities except for PATCH
This commit is contained in:
44
.view/.aes/main.go
Normal file
44
.view/.aes/main.go
Normal 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
176
.view/auth.go
Normal 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
164
.view/auth_test.go
Normal 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
120
.view/files.go
Normal 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
172
.view/files_test.go
Normal 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
150
.view/json.go
Normal 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
159
.view/json_test.go
Normal 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
10
.view/markdown.go
Normal 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
82
.view/port.go
Normal 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
125
.view/port_test.go
Normal 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
46
.view/register.go
Normal 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
123
.view/register_test.go
Normal 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
355
.view/who.go
Normal 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
963
.view/who_test.go
Normal 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],
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user