139 lines
5.1 KiB
Go
139 lines
5.1 KiB
Go
package canvas
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"yingce/backend/internal/kernel"
|
|
"yingce/backend/internal/model"
|
|
)
|
|
|
|
func TestCanvasProjectPayloadReplacesMetadataAndPreservesDocument(t *testing.T) {
|
|
project := model.CanvasProject{
|
|
ID: "canvas-authoritative",
|
|
Title: "Current title",
|
|
ProjectID: "project-current",
|
|
Revision: 9,
|
|
CreatedAt: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC),
|
|
UpdatedAt: time.Date(2026, 2, 3, 4, 5, 6, 0, time.UTC),
|
|
PayloadJSON: `{"id":"stale","title":"stale","revision":1,"nodes":[ { "id": "node-1", "metadata": {"content":"large"} } ],"connections":[],"custom":{"preserve":true}}`,
|
|
}
|
|
raw, err := canvasProjectPayload(project)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var actual map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &actual); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for key, want := range map[string]any{
|
|
"id": project.ID, "title": project.Title, "projectId": project.ProjectID,
|
|
"revision": project.Revision, "createdAt": project.CreatedAt, "updatedAt": project.UpdatedAt,
|
|
} {
|
|
var got any
|
|
if err := json.Unmarshal(actual[key], &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
encodedWant, _ := json.Marshal(want)
|
|
var decodedWant any
|
|
_ = json.Unmarshal(encodedWant, &decodedWant)
|
|
if !reflect.DeepEqual(got, decodedWant) {
|
|
t.Errorf("%s = %#v, want %#v", key, got, decodedWant)
|
|
}
|
|
}
|
|
for _, key := range []string{"nodes", "connections", "custom"} {
|
|
if len(actual[key]) == 0 || !json.Valid(actual[key]) {
|
|
t.Errorf("original %s payload was not preserved: %s", key, actual[key])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCanvasHistoryRestore(t *testing.T) {
|
|
svc := newCanvasHistoryTestService(t)
|
|
actor := &model.User{ID: "owner"}
|
|
save := func(raw string) UserDataSummary {
|
|
t.Helper()
|
|
result, err := svc.UpsertUserCanvasProject(actor.ID, json.RawMessage(raw))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return result
|
|
}
|
|
save(`{"id":"canvas","revision":0,"title":"original","nodes":[{"id":"video"}],"connections":[]}`)
|
|
save(`{"id":"canvas","revision":1,"title":"original","nodes":[{"id":"video"}],"connections":[],"viewport":{"x":20}}`)
|
|
list, err := svc.CanvasHistory(actor.ID, "canvas")
|
|
if err != nil || len(list.Snapshots) != 0 {
|
|
t.Fatalf("no-op created snapshot: %#v %v", list, err)
|
|
}
|
|
save(`{"id":"canvas","revision":2,"title":"edited","nodes":[],"connections":[]}`)
|
|
list, err = svc.CanvasHistory(actor.ID, "canvas")
|
|
if err != nil || len(list.Snapshots) != 1 || list.CurrentRevision != 3 || list.Snapshots[0].NodeCount != 1 {
|
|
t.Fatalf("history: %#v %v", list, err)
|
|
}
|
|
entry := list.Snapshots[0]
|
|
if entry.PayloadJSON != "" {
|
|
t.Fatal("history list loaded full payload")
|
|
}
|
|
preview, err := svc.CanvasHistorySnapshot(actor.ID, "canvas", entry.ID)
|
|
if err != nil || !strings.Contains(preview.PayloadJSON, "video") {
|
|
t.Fatalf("preview = %#v %v", preview, err)
|
|
}
|
|
for _, scenario := range []struct {
|
|
actor *model.User
|
|
canvasID string
|
|
}{
|
|
{actor: &model.User{ID: "stranger"}, canvasID: "canvas"},
|
|
{actor: actor, canvasID: "missing-canvas"},
|
|
} {
|
|
var notFound *kernel.AppError
|
|
if _, err := svc.CanvasHistory(scenario.actor.ID, scenario.canvasID); !errors.As(err, ¬Found) || notFound.Status != http.StatusNotFound {
|
|
t.Fatalf("inaccessible history must return 404: %v", err)
|
|
}
|
|
if _, err := svc.CanvasHistorySnapshot(scenario.actor.ID, scenario.canvasID, entry.ID); !errors.As(err, ¬Found) || notFound.Status != http.StatusNotFound {
|
|
t.Fatalf("inaccessible preview must return 404: %v", err)
|
|
}
|
|
currentRevision := int64(3)
|
|
if _, err := svc.RestoreCanvasHistory(scenario.actor.ID, scenario.canvasID, entry.ID, ¤tRevision); !errors.As(err, ¬Found) || notFound.Status != http.StatusNotFound {
|
|
t.Fatalf("inaccessible restore must return 404: %v", err)
|
|
}
|
|
}
|
|
if _, err := svc.CanvasHistorySnapshot(actor.ID, "canvas", "other-canvas-snapshot"); err == nil {
|
|
t.Fatal("unrelated snapshot readable")
|
|
}
|
|
if _, err := svc.RestoreCanvasHistory(actor.ID, "canvas", entry.ID, nil); err == nil {
|
|
t.Fatal("missing revision accepted")
|
|
}
|
|
stale := int64(2)
|
|
_, err = svc.RestoreCanvasHistory(actor.ID, "canvas", entry.ID, &stale)
|
|
var appError *kernel.AppError
|
|
if !errors.As(err, &appError) || appError.Status != http.StatusConflict {
|
|
t.Fatalf("stale restore = %v", err)
|
|
}
|
|
current := int64(3)
|
|
result, err := svc.RestoreCanvasHistory(actor.ID, "canvas", entry.ID, ¤t)
|
|
if err != nil || result.Revision != 4 {
|
|
t.Fatalf("restore = %#v %v", result, err)
|
|
}
|
|
raw, _ := svc.UserCanvasProject(actor.ID, "canvas")
|
|
if !strings.Contains(string(raw), "video") || !strings.Contains(string(raw), "original") {
|
|
t.Fatalf("wrong restored content: %s", raw)
|
|
}
|
|
list, _ = svc.CanvasHistory(actor.ID, "canvas")
|
|
if len(list.Snapshots) != 2 || list.Snapshots[0].Revision != 3 || list.Snapshots[0].Reason != "before_restore" {
|
|
t.Fatalf("pre-restore backup missing: %#v", list)
|
|
}
|
|
current = 4
|
|
if _, err := svc.RestoreCanvasHistory(actor.ID, "canvas", list.Snapshots[0].ID, ¤t); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, _ = svc.UserCanvasProject(actor.ID, "canvas")
|
|
if strings.Contains(string(raw), "video") {
|
|
t.Fatal("could not undo restore")
|
|
}
|
|
}
|