Build image / build-and-push (push) Successful in 5s
Co-authored-by: Steve Dudenhoeffer <[email protected]>
154 lines
5.8 KiB
Go
154 lines
5.8 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"testing"
|
|
)
|
|
|
|
func historyPath(gardenID int64) string {
|
|
return "/api/v1/gardens/" + strconv.FormatInt(gardenID, 10) + "/history"
|
|
}
|
|
|
|
func revertPath(changeSetID int64) string {
|
|
return "/api/v1/change-sets/" + strconv.FormatInt(changeSetID, 10) + "/revert"
|
|
}
|
|
|
|
// TestHistoryAndRevertAPI walks the whole loop over HTTP: a mutation shows up in
|
|
// history without anyone asking for it, reverting it answers 201 with the new
|
|
// change set, and the change is actually gone from /full.
|
|
func TestHistoryAndRevertAPI(t *testing.T) {
|
|
r := authEngine(t, localCfg())
|
|
cookie := registerAndCookie(t, r, "[email protected]")
|
|
gid := createGardenAPI(t, r, cookie, "G")
|
|
|
|
w := doJSON(t, r, http.MethodPost, objectsPath(gid), map[string]any{
|
|
"kind": "bed", "name": "North Bed", "xCm": 100, "yCm": 100, "widthCm": 100, "heightCm": 100,
|
|
}, cookie)
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("create object: status %d, body %s", w.Code, w.Body.String())
|
|
}
|
|
// The create landed in history with no explicit change set anywhere.
|
|
w = doJSON(t, r, http.MethodGet, historyPath(gid), nil, cookie)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("history: status %d, body %s", w.Code, w.Body.String())
|
|
}
|
|
body := decodeMap(t, w.Body.Bytes())
|
|
sets, _ := body["changeSets"].([]any)
|
|
if len(sets) != 1 {
|
|
t.Fatalf("got %d change sets, want 1: %s", len(sets), w.Body.String())
|
|
}
|
|
if body["hasMore"].(bool) {
|
|
t.Error("hasMore should be false for a single-entry history")
|
|
}
|
|
entry := sets[0].(map[string]any)
|
|
if entry["summary"] != "Added North Bed" {
|
|
t.Errorf("summary = %v", entry["summary"])
|
|
}
|
|
if entry["source"] != "ui" || entry["actorName"] == "" {
|
|
t.Errorf("unexpected entry: %+v", entry)
|
|
}
|
|
counts, _ := entry["counts"].([]any)
|
|
if len(counts) != 1 {
|
|
t.Fatalf("counts = %+v", entry["counts"])
|
|
}
|
|
|
|
// Revert it: 201, and the new change set points back at the original.
|
|
csID := int64(entry["id"].(float64))
|
|
w = doJSON(t, r, http.MethodPost, revertPath(csID), nil, cookie)
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("revert: status %d, body %s", w.Code, w.Body.String())
|
|
}
|
|
rev := decodeMap(t, w.Body.Bytes())
|
|
cs := rev["changeSet"].(map[string]any)
|
|
if int64(cs["revertsId"].(float64)) != csID {
|
|
t.Errorf("revertsId = %v, want %d", cs["revertsId"], csID)
|
|
}
|
|
if conflicts, _ := rev["conflicts"].([]any); len(conflicts) != 0 {
|
|
t.Errorf("unexpected conflicts: %+v", conflicts)
|
|
}
|
|
|
|
// The object is gone from the editor payload.
|
|
w = doJSON(t, r, http.MethodGet, fullPath(gid), nil, cookie)
|
|
full := decodeMap(t, w.Body.Bytes())
|
|
if objects, _ := full["objects"].([]any); len(objects) != 0 {
|
|
t.Errorf("%d objects survived the revert", len(objects))
|
|
}
|
|
}
|
|
|
|
// TestRevertConflictAPI: a partial revert answers 409 and names what it skipped,
|
|
// because "2 of 3 undone, the north bed was edited since" is the only useful
|
|
// thing to say — a bare failure would be a lie about what happened.
|
|
func TestRevertConflictAPI(t *testing.T) {
|
|
r := authEngine(t, localCfg())
|
|
cookie := registerAndCookie(t, r, "[email protected]")
|
|
gid := createGardenAPI(t, r, cookie, "G")
|
|
|
|
w := doJSON(t, r, http.MethodPost, objectsPath(gid), map[string]any{
|
|
"kind": "bed", "name": "Bed", "xCm": 100, "yCm": 100, "widthCm": 100, "heightCm": 100,
|
|
}, cookie)
|
|
obj := decodeMap(t, w.Body.Bytes())
|
|
objectID := int64(obj["id"].(float64))
|
|
version := int64(obj["version"].(float64))
|
|
|
|
// Move it (change set #2), then edit it again (change set #3).
|
|
w = doJSON(t, r, http.MethodPatch, objectPath(objectID), map[string]any{
|
|
"xCm": 300, "version": version,
|
|
}, cookie)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("move: status %d, body %s", w.Code, w.Body.String())
|
|
}
|
|
version = int64(decodeMap(t, w.Body.Bytes())["version"].(float64))
|
|
w = doJSON(t, r, http.MethodPatch, objectPath(objectID), map[string]any{
|
|
"name": "Renamed", "version": version,
|
|
}, cookie)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("rename: status %d, body %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Undoing the move now conflicts: the row changed after it.
|
|
w = doJSON(t, r, http.MethodGet, historyPath(gid), nil, cookie)
|
|
sets := decodeMap(t, w.Body.Bytes())["changeSets"].([]any)
|
|
moveID := int64(sets[1].(map[string]any)["id"].(float64)) // newest first: rename, move, create
|
|
|
|
w = doJSON(t, r, http.MethodPost, revertPath(moveID), nil, cookie)
|
|
if w.Code != http.StatusConflict {
|
|
t.Fatalf("revert: status %d, want 409, body %s", w.Code, w.Body.String())
|
|
}
|
|
conflicts := decodeMap(t, w.Body.Bytes())["conflicts"].([]any)
|
|
if len(conflicts) != 1 {
|
|
t.Fatalf("conflicts = %+v", conflicts)
|
|
}
|
|
c := conflicts[0].(map[string]any)
|
|
if c["reason"] != "changed" || c["name"] != "Renamed" {
|
|
t.Errorf("conflict = %+v", c)
|
|
}
|
|
|
|
// The object was left exactly alone.
|
|
w = doJSON(t, r, http.MethodGet, fullPath(gid), nil, cookie)
|
|
objects := decodeMap(t, w.Body.Bytes())["objects"].([]any)
|
|
o := objects[0].(map[string]any)
|
|
if o["xCm"].(float64) != 300 || o["name"] != "Renamed" {
|
|
t.Errorf("conflicted object was modified: %+v", o)
|
|
}
|
|
}
|
|
|
|
// TestHistoryRequiresAccess — an unauthenticated caller can't read history, and a
|
|
// stranger gets 404 rather than a hint that the garden exists.
|
|
func TestHistoryRequiresAccess(t *testing.T) {
|
|
r := authEngine(t, localCfg())
|
|
owner := registerAndCookie(t, r, "[email protected]")
|
|
gid := createGardenAPI(t, r, owner, "G")
|
|
stranger := registerAndCookie(t, r, "[email protected]")
|
|
|
|
if w := doJSON(t, r, http.MethodGet, historyPath(gid), nil, nil); w.Code != http.StatusUnauthorized {
|
|
t.Errorf("anonymous history status = %d, want 401", w.Code)
|
|
}
|
|
if w := doJSON(t, r, http.MethodGet, historyPath(gid), nil, stranger); w.Code != http.StatusNotFound {
|
|
t.Errorf("stranger history status = %d, want 404", w.Code)
|
|
}
|
|
if w := doJSON(t, r, http.MethodPost, revertPath(1), nil, stranger); w.Code != http.StatusNotFound {
|
|
t.Errorf("stranger revert status = %d, want 404", w.Code)
|
|
}
|
|
}
|