impl entities and test

master
breel 2020-08-09 10:41:37 -06:00
parent 64772166cc
commit 468e5bedd5
4 changed files with 448 additions and 14 deletions

View File

@ -11,6 +11,8 @@ import (
"path"
"strings"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson/primitive"
"gopkg.in/mgo.v2/bson"
)
@ -20,12 +22,30 @@ type shortEntity struct {
}
func (rest *REST) entities(w http.ResponseWriter, r *http.Request) {
if scope, err := rest.entityScope(r); err != nil {
scope, err := rest.entityScope(r)
if err != nil {
rest.respNotFound(w)
return
} else if r.Method != http.MethodGet && len(scope) > 1 {
}
if r.Method == http.MethodGet {
} else if r.Method == http.MethodPost && len(scope) != 0 {
rest.respNotFound(w)
return
} else if len(scope) > 1 {
if r.Method == http.MethodDelete {
q := r.URL.Query()
q.Set("delete", "")
r.URL.RawQuery = q.Encode()
}
r.Method = http.MethodPatch
}
if r.Method == http.MethodPatch && len(scope) == 1 {
r.Method = http.MethodPut
}
if r.Method == http.MethodPatch && len(scope) == 0 {
rest.respNotFound(w)
return
}
switch r.Method {
case http.MethodPut:
rest.entitiesReplace(w, r)
@ -44,17 +64,17 @@ func (rest *REST) entities(w http.ResponseWriter, r *http.Request) {
func (rest *REST) entitiesCreate(w http.ResponseWriter, r *http.Request) {
scope := rest.scope(r)
entityScope, _ := rest.entityScope(r)
one, err := rest.entityParse(r.Body)
if err != nil {
rest.respBadRequest(w, err.Error())
return
}
one.ID = entityScope[0]
one.ID = uuid.New().String()
if err := rest.g.Insert(r.Context(), scope.Namespace, one); err != nil {
rest.respError(w, err)
return
}
r.URL.Path += "/" + one.ID
rest.entitiesGet(w, r)
}
@ -71,7 +91,7 @@ func (rest *REST) entitiesGet(w http.ResponseWriter, r *http.Request) {
entityScope, _ := rest.entityScope(r)
switch len(entityScope) {
case 0:
rest.entitiesList(w, r)
rest.entitiesGetN(w, r)
case 1:
rest.entitiesGetOne(w, r)
default:
@ -87,6 +107,11 @@ func (rest *REST) entitiesGetOne(w http.ResponseWriter, r *http.Request) {
rest.respNotFound(w)
return
}
if _, ok := r.URL.Query()["light"]; !ok {
log.Println("TODO need to get all connections")
//http.Error(w, "not impl", http.StatusNotImplemented)
//return
}
rest.respMap(w, entityScope[0], one)
}
@ -128,7 +153,7 @@ func (rest *REST) entitiesGetOneSub(w http.ResponseWriter, r *http.Request) {
rest.respMap(w, scope.EntityID, m[entityScope[0]])
}
func (rest *REST) entitiesList(w http.ResponseWriter, r *http.Request) {
func (rest *REST) entitiesGetN(w http.ResponseWriter, r *http.Request) {
scope := rest.scope(r)
entities, err := rest.g.List(r.Context(), scope.Namespace)
if err != nil {
@ -163,9 +188,44 @@ func (rest *REST) entitiesReplace(w http.ResponseWriter, r *http.Request) {
}
func (rest *REST) entitiesUpdate(w http.ResponseWriter, r *http.Request) {
log.Println(r.Method)
log.Println(rest.entityScope(r))
http.Error(w, "not impl", http.StatusNotImplemented)
entityScope, _ := rest.entityScope(r)
scope := rest.scope(r)
_, del := r.URL.Query()["delete"]
var m interface{}
if !del {
err := json.NewDecoder(r.Body).Decode(&m)
if err != nil {
rest.respBadRequest(w, err.Error())
return
}
if mm, ok := m.(primitive.M); ok {
delete(mm, entity.ID)
m = mm
}
}
key := strings.Join(entityScope[1:], ".")
var operation interface{}
operation = operator.Set{Key: key, Value: m}
if del {
operation = operator.Unset(key)
}
err := rest.g.Update(
r.Context(),
scope.Namespace,
bson.M{entity.ID: scope.EntityID},
operation,
)
if err != nil {
rest.respError(w, err)
return
}
r.URL.Path = path.Join("/", scope.EntityID)
rest.entitiesGet(w, r)
}
func (rest *REST) entityParse(r io.Reader) (entity.One, error) {
@ -177,7 +237,7 @@ func (rest *REST) entityParse(r io.Reader) (entity.One, error) {
func (rest *REST) entityScope(r *http.Request) ([]string, error) {
p := r.URL.Path
if path.Dir(p) == path.Base(p) {
if r.Method == http.MethodGet {
if r.Method == http.MethodGet || r.Method == http.MethodPost {
return []string{}, nil
}
return nil, errors.New("nothing specified")

368
server/entities_test.go Normal file
View File

@ -0,0 +1,368 @@
package server
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"local/dndex/storage/entity"
"net/http"
"net/http/httptest"
"path"
"strings"
"testing"
)
func TestEntities(t *testing.T) {
rest, authit, clean := testREST(t)
defer clean()
t.Run("create+get1+delete+404", func(t *testing.T) {
w := testEntitiesMethod(t, authit, rest, http.MethodPost, "/", `{"name":"myname"}`)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
id := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Name == "myname"
})
w = testEntitiesMethod(t, authit, rest, http.MethodGet, "/"+id, ``)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
id2 := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Name == "myname"
})
if id2 != id {
t.Fatal(id, id2)
}
w = testEntitiesMethod(t, authit, rest, http.MethodDelete, "/"+id, ``)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
w = testEntitiesMethod(t, authit, rest, http.MethodGet, "/"+id, ``)
if w.Code != http.StatusNotFound {
t.Fatal(w.Code)
}
})
t.Run("get1 404", func(t *testing.T) {
w := testEntitiesMethod(t, authit, rest, http.MethodGet, "/abc123", ``)
if w.Code != http.StatusNotFound {
t.Fatal(w.Code)
}
})
t.Run("create+get0", func(t *testing.T) {
w := testEntitiesMethod(t, authit, rest, http.MethodPost, "/", `{"name":"myname", "attachments": {"abc": {}}, "connections": {"def": {"relationship": "ghi"}}}`)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
id := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Name == "myname" && len(one.Attachments) == 1 && len(one.Connections) == 1 && one.Connections["def"].Relationship == "ghi"
})
w = testEntitiesMethod(t, authit, rest, http.MethodGet, "/", ``)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
testEntitiesGetNResponse(t, w.Body, func(one shortEntity) bool {
return one.Name == "myname" && one.ID == id
})
})
t.Run("create+replace+get1", func(t *testing.T) {
check := func(one entity.One) bool {
return one.Name == "myname"
}
w := testEntitiesMethod(t, authit, rest, http.MethodPost, "/", `{"name":"myname"}`)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
id := testEntitiesGetOneResponse(t, w.Body, check)
w = testEntitiesMethod(t, authit, rest, http.MethodPut, "/"+id, `{"name":"newname"}`)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
w = testEntitiesMethod(t, authit, rest, http.MethodGet, "/"+id, ``)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
id2 := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Name == "newname"
})
if id2 != id {
t.Fatal(id, id2)
}
})
t.Run("create+get.title", func(t *testing.T) {
w := testEntitiesMethod(t, authit, rest, http.MethodPost, "/", `{"name":"myname", "title": "mytitle"}`)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
id := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Name == "myname" && one.Title == "mytitle"
})
w = testEntitiesMethod(t, authit, rest, http.MethodGet, "/"+id+"/title", ``)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
testEntitiesGetOneSubResponse(t, w.Body, func(v interface{}) bool {
return fmt.Sprint(v) == "mytitle"
})
})
t.Run("create+update/replace", func(t *testing.T) {
for _, method := range []string{http.MethodPut, http.MethodPatch} {
w := testEntitiesMethod(t, authit, rest, http.MethodPost, "/", `{"name":"myname", "title": "mytitle"}`)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
id := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Name == "myname" && one.Title == "mytitle"
})
w = testEntitiesMethod(t, authit, rest, method, "/"+id, `{"name": "newname"}`)
if w.Code != http.StatusOK {
t.Fatalf("%v: %s", w.Code, w.Body.Bytes())
}
id2 := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Name == "newname" && one.Title == ""
})
if id != id2 {
t.Fatal(id, id2)
}
}
})
t.Run("create+update/replace.name", func(t *testing.T) {
for _, method := range []string{http.MethodPut, http.MethodPatch} {
w := testEntitiesMethod(t, authit, rest, http.MethodPost, "/", `{"name":"myname", "title": "mytitle"}`)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
id := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Name == "myname" && one.Title == "mytitle"
})
w = testEntitiesMethod(t, authit, rest, method, "/"+id+"/name", `"newname"`)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
id2 := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Name == "newname" && one.Title == "mytitle"
})
if id != id2 {
t.Fatal(id, id2)
}
}
})
t.Run("create+update/replace.connection.abc.relationship", func(t *testing.T) {
for _, method := range []string{http.MethodPut, http.MethodPatch} {
w := testEntitiesMethod(t, authit, rest, http.MethodPost, "/", `{"connections": {"abc": {"relationship": "def"}}}`)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
id := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Connections["abc"].Relationship == "def"
})
w = testEntitiesMethod(t, authit, rest, method, "/"+id+"/connections/abc/relationship", `"new"`)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
id2 := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Connections["abc"].Relationship == "new"
})
if id != id2 {
t.Fatal(id, id2)
}
}
})
t.Run("create+update/replace.connection.abc", func(t *testing.T) {
for _, method := range []string{http.MethodPut, http.MethodPatch} {
w := testEntitiesMethod(t, authit, rest, http.MethodPost, "/", `{"connections": {"abc": {"relationship": "def"}}}`)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
id := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Connections["abc"].Relationship == "def"
})
w = testEntitiesMethod(t, authit, rest, method, "/"+id+"/connections/abc", `{"relationship": "new"}`)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
id2 := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Connections["abc"].Relationship == "new"
})
if id != id2 {
t.Fatal(id, id2)
}
}
})
t.Run("create+delete.name", func(t *testing.T) {
w := testEntitiesMethod(t, authit, rest, http.MethodPost, "/", `{"name":"myname", "title": "mytitle"}`)
if w.Code != http.StatusOK {
t.Fatal(w.Code)
}
id := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Name == "myname" && one.Title == "mytitle"
})
w = testEntitiesMethod(t, authit, rest, http.MethodDelete, "/"+id+"/name", ``)
if w.Code != http.StatusOK {
t.Fatalf("%v: %s", w.Code, w.Body.Bytes())
}
id2 := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Name == "" && one.Title == "mytitle"
})
if id != id2 {
t.Fatal(id, id2)
}
})
t.Run("create w connection.abc+delete.connections.abc", func(t *testing.T) {
w := testEntitiesMethod(t, authit, rest, http.MethodPost, "/", `{"name":"myname", "title": "mytitle", "connections": {"abc": {"relationship": "good"}}}`)
if w.Code != http.StatusOK {
t.Fatalf("%v: %s", w.Code, w.Body.Bytes())
}
id := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
abc, ok := one.Connections["abc"]
return one.Name == "myname" && one.Title == "mytitle" && ok && abc.Relationship == "good"
})
w = testEntitiesMethod(t, authit, rest, http.MethodDelete, "/"+id+"/connections/abc", ``)
if w.Code != http.StatusOK {
t.Fatalf("%v: %s", w.Code, w.Body.Bytes())
}
id2 := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return one.Name == "myname" && one.Title == "mytitle" && len(one.Connections) == 0
})
if id != id2 {
t.Fatal(id, id2)
}
})
t.Run("create w connection.abc.relationship+delete.connections.abc.relationship", func(t *testing.T) {
w := testEntitiesMethod(t, authit, rest, http.MethodPost, "/", `{"connections": {"abc": {"relationship": "good"}}}`)
if w.Code != http.StatusOK {
t.Fatalf("%v: %s", w.Code, w.Body.Bytes())
}
id := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
abc, ok := one.Connections["abc"]
return ok && abc.Relationship == "good"
})
w = testEntitiesMethod(t, authit, rest, http.MethodDelete, "/"+id+"/connections/abc/relationship", ``)
if w.Code != http.StatusOK {
t.Fatalf("%v: %s", w.Code, w.Body.Bytes())
}
id2 := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
return len(one.Connections) == 1 && one.Connections["abc"].Relationship == ""
})
if id != id2 {
t.Fatal(id, id2)
}
})
}
func testEntitiesMethod(t *testing.T, authit func(*http.Request), rest *REST, method, p, body string) *httptest.ResponseRecorder {
r := httptest.NewRequest(method, p, strings.NewReader(body))
if !strings.HasPrefix(r.URL.Path, "/entities") {
r.URL.Path = path.Join("/entities", r.URL.Path)
}
w := httptest.NewRecorder()
authit(r)
rest.scoped(rest.shift(rest.entities))(w, r)
return w
}
func testEntitiesGetNResponse(t *testing.T, body io.Reader, check func(shortEntity) bool) {
var resp map[string][]shortEntity
if err := json.NewDecoder(body).Decode(&resp); err != nil {
t.Fatal(err)
}
if len(resp) != 1 {
t.Fatal("excess found in db")
}
for k := range resp {
contents := resp[k]
if len(contents) < 1 {
t.Fatal(len(contents))
}
for i := range contents {
if check(contents[i]) {
return
}
}
t.Fatal(contents)
}
}
func testEntitiesGetOneResponse(t *testing.T, body io.Reader, check func(entity.One) bool) string {
b, err := ioutil.ReadAll(body)
if err != nil {
t.Fatal(err)
}
var resp map[string]entity.One
if err := json.Unmarshal(b, &resp); err != nil {
t.Fatalf("%v: %s", err, b)
}
if len(resp) != 1 {
t.Fatal(len(resp))
}
for k := range resp {
one := resp[k]
if one.ID != k {
t.Fatal(k, one.ID)
}
if one.Modified == 0 {
t.Fatal(one.Modified)
}
if !check(one) {
t.Fatal(one)
}
return one.ID
}
panic("somehow no keys found")
}
func testEntitiesGetOneSubResponse(t *testing.T, body io.Reader, check func(interface{}) bool) {
b, err := ioutil.ReadAll(body)
if err != nil {
t.Fatal(err)
}
var resp map[string]interface{}
if err := json.Unmarshal(b, &resp); err != nil {
t.Fatalf("%v: %s", err, b)
}
if len(resp) != 1 {
t.Fatal(len(resp))
}
for k := range resp {
one := resp[k]
if !check(one) {
t.Fatal(one)
}
return
}
panic("somehow no keys found")
}

View File

@ -310,6 +310,14 @@ func applySet(doc, operator bson.M) (bson.M, error) {
if k == entity.ID {
continue
}
if k == "." {
m, ok := v.(bson.M)
if !ok {
return nil, errors.New("cannot assign non-map to doc")
}
doc = m
return doc, nil
}
nesting := strings.Split(k, ".")
if len(nesting) > 1 {
mInterface, ok := doc[nesting[0]]

View File

@ -74,10 +74,8 @@ func (g Graph) Insert(ctx context.Context, namespace string, one entity.One) err
if one.ID == "" {
return errors.New("cannot create document without id")
}
if ones, err := g.ListCaseInsensitive(ctx, namespace, one.ID); err != nil {
return err
} else if len(ones) > 0 {
return fmt.Errorf("collision on primary key when case insensitive: cannot create %q because %+v exists", one.ID, ones)
if one, err := g.Get(ctx, namespace, one.ID); err == nil {
return fmt.Errorf("collision on primary key when case insensitive: cannot create %q because %+v exists", one.ID, one)
}
return g.driver.Insert(ctx, namespace, one)
}