package db import ( "errors" "testing" ) // ----------------------------------------------------------------------- frames func TestCreateFrame_Basics(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") f, err := s.CreateFrame(p.ID, FrameCreate{Name: "desk", X: 10, Y: 20, Width: 800, Height: 600}) if err != nil { t.Fatalf("create: %v", err) } if f.ProjectID != p.ID || f.Name != "desk" || f.Width != 800 { t.Errorf("unexpected frame: %+v", f) } } func TestCreateFrame_RejectsZeroSize(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") if _, err := s.CreateFrame(p.ID, FrameCreate{Name: "x", Width: 0, Height: 50}); !errors.Is(err, ErrInvalidInput) { t.Errorf("zero width should be ErrInvalidInput; got %v", err) } if _, err := s.CreateFrame(p.ID, FrameCreate{Name: "y", Width: 50, Height: 0}); !errors.Is(err, ErrInvalidInput) { t.Errorf("zero height should be ErrInvalidInput; got %v", err) } } func TestCreateFrame_DuplicateNameInSameProject(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") if _, err := s.CreateFrame(p.ID, FrameCreate{Name: "desk", Width: 100, Height: 50}); err != nil { t.Fatalf("first: %v", err) } if _, err := s.CreateFrame(p.ID, FrameCreate{Name: "desk", Width: 200, Height: 70}); !errors.Is(err, ErrConflict) { t.Errorf("duplicate frame name should ErrConflict; got %v", err) } } func TestCreateFrame_SameNameAcrossProjectsOK(t *testing.T) { s := newTestStore(t) p1, _ := s.CreateProject("LOFT", "", "") p2, _ := s.CreateProject("OFFICE", "", "") if _, err := s.CreateFrame(p1.ID, FrameCreate{Name: "desk", Width: 100, Height: 50}); err != nil { t.Fatalf("p1: %v", err) } if _, err := s.CreateFrame(p2.ID, FrameCreate{Name: "desk", Width: 100, Height: 50}); err != nil { t.Fatalf("p2: %v", err) } } func TestGetFrame_WrongProjectIsNotFound(t *testing.T) { s := newTestStore(t) p1, _ := s.CreateProject("LOFT", "", "") p2, _ := s.CreateProject("OFFICE", "", "") f, _ := s.CreateFrame(p1.ID, FrameCreate{Name: "desk", Width: 100, Height: 50}) if _, err := s.GetFrame(p2.ID, f.ID); !errors.Is(err, ErrNotFound) { t.Errorf("cross-project GetFrame should be ErrNotFound; got %v", err) } } func TestListFrames_OrderedByCreation(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") for _, n := range []string{"rack", "desk", "media"} { if _, err := s.CreateFrame(p.ID, FrameCreate{Name: n, Width: 100, Height: 50}); err != nil { t.Fatalf("create %s: %v", n, err) } } got, _ := s.ListFrames(p.ID) if len(got) != 3 { t.Fatalf("len = %d", len(got)) } if got[0].Name != "rack" || got[2].Name != "media" { t.Errorf("order = %v", []string{got[0].Name, got[1].Name, got[2].Name}) } } func TestUpdateFrame_PartialFields(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") f, _ := s.CreateFrame(p.ID, FrameCreate{Name: "desk", X: 0, Y: 0, Width: 100, Height: 50}) nx := 42.0 updated, err := s.UpdateFrame(p.ID, f.ID, FrameUpdate{X: &nx}) if err != nil { t.Fatalf("update: %v", err) } if updated.X != 42 || updated.Name != "desk" || updated.Width != 100 { t.Errorf("got %+v", updated) } } func TestDeleteFrame_SetsDeviceFrameIDToNull(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") f, _ := s.CreateFrame(p.ID, FrameCreate{Name: "desk", Width: 800, Height: 600}) d, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "Mac", FrameID: &f.ID, X: 10, Y: 20, Width: 100, Height: 35}) if d.FrameID == nil || *d.FrameID != f.ID { t.Fatalf("device frame_id pre-delete = %v, want %d", d.FrameID, f.ID) } if err := s.DeleteFrame(p.ID, f.ID); err != nil { t.Fatalf("delete frame: %v", err) } d2, _ := s.GetDevice(p.ID, d.ID) if d2.FrameID != nil { t.Errorf("device frame_id post-delete = %v, want nil (SET NULL)", d2.FrameID) } } // ---------------------------------------------------------------------- devices func TestCreateDevice_DefaultsColor(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") d, err := s.CreateDevice(p.ID, DeviceCreate{Name: "Mac", X: 10, Y: 20, Width: 100, Height: 35}) if err != nil { t.Fatalf("create: %v", err) } if d.Color != "#1e1e1e" { t.Errorf("default color = %q, want #1e1e1e", d.Color) } if d.FrameID != nil { t.Errorf("frame_id = %v, want nil for unframed device", d.FrameID) } } func TestCreateDevice_DuplicateNameInProject(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") if _, err := s.CreateDevice(p.ID, DeviceCreate{Name: "Mac", X: 0, Y: 0, Width: 100, Height: 35}); err != nil { t.Fatalf("first: %v", err) } if _, err := s.CreateDevice(p.ID, DeviceCreate{Name: "Mac", X: 10, Y: 10, Width: 100, Height: 35}); !errors.Is(err, ErrConflict) { t.Errorf("dup device name should ErrConflict; got %v", err) } } func TestCreateDevice_CrossProjectFrameRejected(t *testing.T) { s := newTestStore(t) p1, _ := s.CreateProject("LOFT", "", "") p2, _ := s.CreateProject("OFFICE", "", "") f2, _ := s.CreateFrame(p2.ID, FrameCreate{Name: "desk", Width: 100, Height: 50}) // Try to put a LOFT device into an OFFICE frame. _, err := s.CreateDevice(p1.ID, DeviceCreate{Name: "Mac", FrameID: &f2.ID, X: 0, Y: 0, Width: 100, Height: 35}) if !errors.Is(err, ErrInvalidInput) { t.Errorf("cross-project frame_id should ErrInvalidInput; got %v", err) } } func TestUpdateDevice_FrameIDTriState(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") f1, _ := s.CreateFrame(p.ID, FrameCreate{Name: "desk", Width: 100, Height: 50}) f2, _ := s.CreateFrame(p.ID, FrameCreate{Name: "rack", Width: 100, Height: 50}) d, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "Mac", FrameID: &f1.ID, X: 0, Y: 0, Width: 100, Height: 35}) // Leave alone (FrameID.Set=false) — even passing a different X. nx := 99.0 u1, _ := s.UpdateDevice(p.ID, d.ID, DeviceUpdate{X: &nx}) if u1.FrameID == nil || *u1.FrameID != f1.ID { t.Errorf("frame_id should be unchanged (f1); got %v", u1.FrameID) } // Move to f2. u2, _ := s.UpdateDevice(p.ID, d.ID, DeviceUpdate{FrameID: FrameRef{Set: true, ID: &f2.ID}}) if u2.FrameID == nil || *u2.FrameID != f2.ID { t.Errorf("frame_id should be f2; got %v", u2.FrameID) } // Clear (move outside any frame). u3, _ := s.UpdateDevice(p.ID, d.ID, DeviceUpdate{FrameID: FrameRef{Set: true, ID: nil}}) if u3.FrameID != nil { t.Errorf("frame_id should be nil after Set:true,ID:nil; got %v", *u3.FrameID) } } func TestUpdateDevice_RejectsCrossProjectFrame(t *testing.T) { s := newTestStore(t) p1, _ := s.CreateProject("LOFT", "", "") p2, _ := s.CreateProject("OFFICE", "", "") d, _ := s.CreateDevice(p1.ID, DeviceCreate{Name: "Mac", X: 0, Y: 0, Width: 100, Height: 35}) f2, _ := s.CreateFrame(p2.ID, FrameCreate{Name: "desk", Width: 100, Height: 50}) _, err := s.UpdateDevice(p1.ID, d.ID, DeviceUpdate{FrameID: FrameRef{Set: true, ID: &f2.ID}}) if !errors.Is(err, ErrInvalidInput) { t.Errorf("cross-project frame_id should ErrInvalidInput; got %v", err) } } func TestListDevices_FilterByFrame(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") f1, _ := s.CreateFrame(p.ID, FrameCreate{Name: "desk", Width: 100, Height: 50}) f2, _ := s.CreateFrame(p.ID, FrameCreate{Name: "rack", Width: 100, Height: 50}) _, _ = s.CreateDevice(p.ID, DeviceCreate{Name: "A", FrameID: &f1.ID, Width: 100, Height: 35}) _, _ = s.CreateDevice(p.ID, DeviceCreate{Name: "B", FrameID: &f2.ID, Width: 100, Height: 35}) _, _ = s.CreateDevice(p.ID, DeviceCreate{Name: "C", Width: 100, Height: 35}) // outside all, _ := s.ListDevices(p.ID, nil) if len(all) != 3 { t.Errorf("all len = %d, want 3", len(all)) } inF1, _ := s.ListDevices(p.ID, &f1.ID) if len(inF1) != 1 || inF1[0].Name != "A" { t.Errorf("inF1 = %+v", inF1) } } func TestSnapshot_PopulatesFramesAndDevices(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") f, _ := s.CreateFrame(p.ID, FrameCreate{Name: "desk", Width: 100, Height: 50}) _, _ = s.CreateDevice(p.ID, DeviceCreate{Name: "Mac", FrameID: &f.ID, Width: 100, Height: 35}) snap, err := s.Snapshot(p.ID) if err != nil { t.Fatalf("snapshot: %v", err) } if len(snap.Frames) != 1 || len(snap.Devices) != 1 { t.Errorf("snapshot frames=%d devices=%d", len(snap.Frames), len(snap.Devices)) } if len(snap.CableTypes) != 5 { t.Errorf("cable_types = %d, want 5", len(snap.CableTypes)) } } func TestDeleteDevice_NotFoundIsNotFound(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") if err := s.DeleteDevice(p.ID, 999); !errors.Is(err, ErrNotFound) { t.Errorf("got %v, want ErrNotFound", err) } }