diff --git a/public/swagger/swagger-who.yaml b/public/swagger/swagger-who.yaml index e08f735..4acec67 100644 --- a/public/swagger/swagger-who.yaml +++ b/public/swagger/swagger-who.yaml @@ -78,10 +78,11 @@ paths: delete: tags: - who - summary: "Delete the specified entity, which is assumed to be a leaf" + summary: "Delete the specified entity, which is assumed to be a leaf, or only its connection if specified" parameters: - $ref: "#/components/parameters/id" - $ref: "#/components/parameters/namespace" + - $ref: "#/components/parameters/connection" responses: 200: $ref: "#/components/schemas/ok" @@ -98,6 +99,13 @@ components: namespace: $ref: "./swagger.yaml#/components/parameters/namespace" + connection: + name: connection + in: query + description: "An entity's connection's name" + schema: + type: string + light: name: light in: query diff --git a/view/who.go b/view/who.go index 85ce58a..63c72bf 100644 --- a/view/who.go +++ b/view/who.go @@ -152,6 +152,11 @@ func whoDelete(namespace string, g storage.Graph, w http.ResponseWriter, r *http 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 } @@ -159,6 +164,31 @@ func whoDelete(namespace string, g storage.Graph, w http.ResponseWriter, r *http return json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } +func whoDeleteConnections(namespace string, g storage.Graph, 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.Graph, w http.ResponseWriter, r *http.Request) error { id, err := getID(r) if err != nil { diff --git a/view/who_test.go b/view/who_test.go index 39fccf1..1f0bded 100644 --- a/view/who_test.go +++ b/view/who_test.go @@ -38,11 +38,20 @@ func TestWho(t *testing.T) { ones := fillDB(t, g) want := ones[len(ones)-1] + reset := func() { + if err := g.Delete(context.TODO(), "col", map[string]string{}); err != nil { + t.Fatal(err) + } + ones = fillDB(t, g) + want = ones[len(ones)-1] + } + handler := jsonHandler(g) t.Log(handler, want) t.Run("get no namespace is 404", func(t *testing.T) { + reset() iwant := want r := httptest.NewRequest(http.MethodGet, "/who?id="+iwant.Name, nil) w := httptest.NewRecorder() @@ -53,6 +62,7 @@ func TestWho(t *testing.T) { }) t.Run("get fake", func(t *testing.T) { + reset() iwant := want r := httptest.NewRequest(http.MethodGet, "/who?namespace=col&id=FAKER"+iwant.Name, nil) w := httptest.NewRecorder() @@ -63,6 +73,7 @@ func TestWho(t *testing.T) { }) t.Run("get real", func(t *testing.T) { + reset() iwant := want r := httptest.NewRequest(http.MethodGet, "/who?namespace=col&id="+iwant.Name, nil) w := httptest.NewRecorder() @@ -91,6 +102,7 @@ func TestWho(t *testing.T) { }) t.Run("get real light", func(t *testing.T) { + reset() iwant := want r := httptest.NewRequest(http.MethodGet, "/who?namespace=col&light&id="+iwant.Name, nil) w := httptest.NewRecorder() @@ -124,6 +136,7 @@ func TestWho(t *testing.T) { }) t.Run("get real but case is wrong", func(t *testing.T) { + reset() iwant := want iwantName := strings.ToUpper(iwant.Name) r := httptest.NewRequest(http.MethodGet, "/who?namespace=col&id="+iwantName, nil) @@ -153,6 +166,7 @@ func TestWho(t *testing.T) { }) t.Run("put fake", func(t *testing.T) { + reset() iwant := want r := httptest.NewRequest(http.MethodPut, "/who?namespace=col&id=FAKER"+iwant.Name, strings.NewReader(`{"title":"this should fail to find someone"}`)) w := httptest.NewRecorder() @@ -163,6 +177,7 @@ func TestWho(t *testing.T) { }) t.Run("put real", func(t *testing.T) { + reset() iwant := want r := httptest.NewRequest(http.MethodPut, "/who?namespace=col&id="+iwant.Name, strings.NewReader(`{"title":"this should work"}`)) w := httptest.NewRecorder() @@ -185,6 +200,7 @@ func TestWho(t *testing.T) { }) t.Run("post exists", func(t *testing.T) { + reset() iwant := want iwant.Name = "" r := httptest.NewRequest(http.MethodPost, "/who?namespace=col&id="+want.Name, strings.NewReader(`{"title":"this should fail to insert"}`)) @@ -196,6 +212,7 @@ func TestWho(t *testing.T) { }) t.Run("post real", func(t *testing.T) { + reset() iwant := want iwant.Name = "" b, err := json.Marshal(iwant) @@ -223,6 +240,7 @@ func TestWho(t *testing.T) { }) t.Run("delete real", func(t *testing.T) { + reset() r := httptest.NewRequest(http.MethodDelete, "/who?namespace=col&id=NEWBIE"+want.Name, nil) w := httptest.NewRecorder() handler.ServeHTTP(w, r) @@ -238,6 +256,7 @@ func TestWho(t *testing.T) { }) t.Run("delete fake", func(t *testing.T) { + reset() r := httptest.NewRequest(http.MethodDelete, "/who?namespace=col&id=FAKER"+want.Name, nil) w := httptest.NewRecorder() handler.ServeHTTP(w, r) @@ -247,6 +266,7 @@ func TestWho(t *testing.T) { }) t.Run("delete regexp should be sanitized", func(t *testing.T) { + reset() r := httptest.NewRequest(http.MethodDelete, "/who?namespace=col&id=.*", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, r) @@ -271,6 +291,7 @@ func TestWho(t *testing.T) { }) t.Run("patch fake", func(t *testing.T) { + reset() r := httptest.NewRequest(http.MethodPatch, "/who?namespace=col&id=FAKER"+want.Name, nil) w := httptest.NewRecorder() handler.ServeHTTP(w, r) @@ -281,6 +302,7 @@ func TestWho(t *testing.T) { want = ones[4] t.Run("patch real against existing", func(t *testing.T) { + reset() from := ones[4] push := ones[10].Peer() push.Relationship = "spawn" @@ -327,6 +349,7 @@ func TestWho(t *testing.T) { want = ones[2] t.Run("patch real", func(t *testing.T) { + reset() iwant := want iwant.Relationship = "spawn" iwant.Name = "child of " + want.Name @@ -361,6 +384,7 @@ func TestWho(t *testing.T) { }) t.Run("trace fake", func(t *testing.T) { + reset() r := httptest.NewRequest(http.MethodTrace, "/who?namespace=notcol", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, r) @@ -370,6 +394,7 @@ func TestWho(t *testing.T) { }) t.Run("trace real", func(t *testing.T) { + reset() r := httptest.NewRequest(http.MethodTrace, "/who?namespace=col", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, r) @@ -387,6 +412,7 @@ func TestWho(t *testing.T) { }) t.Run("get without id == trace real", func(t *testing.T) { + reset() r := httptest.NewRequest(http.MethodGet, "/who?namespace=col", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, r) @@ -404,6 +430,7 @@ func TestWho(t *testing.T) { }) t.Run("trace real sorted asc/desc name", func(t *testing.T) { + reset() for _, order := range []string{"1", "-1"} { r := httptest.NewRequest(http.MethodTrace, "/who?namespace=col&sort=name&order="+order, nil) w := httptest.NewRecorder() @@ -428,6 +455,137 @@ func TestWho(t *testing.T) { } } }) + + t.Run("delete connection 1 of 0 noop but ok", func(t *testing.T) { + reset() + want := ones[0] + 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()) + } + 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) { + reset() + r := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/who?namespace=col&id=%s&connection=%s", want.Name, want.Peers()[0]), 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 _, ok := o.Connections[want.Peers()[0]]; ok { + t.Fatal(want.Peers()[0], o.Connections) + } + }) + + t.Run("delete connection 1 of 4 ok", func(t *testing.T) { + reset() + + 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()) + } + 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 + } + if fmt.Sprint(o) != fmt.Sprint(want) { + t.Fatalf("GET put != expected: want:\n%+v, got \n%+v", want, o) + } + + r = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/who?namespace=col&id=%s&connection=%s", want.Name, want.Peers()[0]), 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 _, ok := o.Connections[want.Peers()[0]]; ok { + t.Fatal(want.Peers()[0], o.Connections) + } + if len(o.Connections) != len(put.Connections)-1 { + t.Fatal(o.Connections) + } + }) } func fillDB(t *testing.T, g storage.Graph) []entity.One {