Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
63 lines
2.4 KiB
Go
63 lines
2.4 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestGroupMembershipHistoryIsAppendOnlyAndNeverOverlaps(t *testing.T) {
|
|
run(t, func(t *testing.T, e *env) {
|
|
ctx := context.Background()
|
|
d := newDevice(t, e, "A")
|
|
i1 := viewIface(t, e, d, 1, "e1", "", nil)
|
|
i2 := viewIface(t, e, d, 2, "e2", "", nil)
|
|
g := &Group{Name: "Acme", Kind: GroupCustomer}
|
|
must(t, e.CreateGroup(ctx, g))
|
|
if err := e.CreateGroup(ctx, &Group{Name: "Acme", Kind: GroupCustomer}); !errors.Is(err, ErrConflict) {
|
|
t.Fatalf("duplicate name = %v", err)
|
|
}
|
|
if err := e.CreateGroup(ctx, &Group{Name: "x", Kind: "bogus"}); !errors.Is(err, ErrInvalid) {
|
|
t.Fatalf("bad kind = %v", err)
|
|
}
|
|
jan := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
feb := time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)
|
|
must(t, e.AddGroupMember(ctx, GroupMember{GroupID: g.ID, InterfaceID: i1.ID, Direction: DirToward, EffectiveFrom: jan}))
|
|
must(t, e.AddGroupMember(ctx, GroupMember{GroupID: g.ID, InterfaceID: i2.ID, Direction: DirAway, EffectiveFrom: feb}))
|
|
err := e.AddGroupMember(ctx, GroupMember{GroupID: g.ID, InterfaceID: i1.ID, Direction: DirAway, EffectiveFrom: feb})
|
|
if !errors.Is(err, ErrConflict) {
|
|
t.Fatalf("overlapping add = %v", err)
|
|
}
|
|
if err := e.AddGroupMember(ctx, GroupMember{GroupID: g.ID, InterfaceID: "nope", Direction: DirAway}); !errors.Is(err, ErrInvalid) {
|
|
t.Fatalf("unknown interface = %v", err)
|
|
}
|
|
must(t, e.EndGroupMember(ctx, g.ID, i1.ID, feb.Add(24*time.Hour)))
|
|
// Re-adding with the other direction after the end keeps the old row.
|
|
must(t, e.AddGroupMember(ctx, GroupMember{GroupID: g.ID, InterfaceID: i1.ID, Direction: DirAway, EffectiveFrom: feb.Add(24 * time.Hour)}))
|
|
|
|
inJan, err := e.GroupMembers(ctx, g.ID, jan, feb)
|
|
must(t, err)
|
|
if len(inJan) != 1 || inJan[0].InterfaceID != i1.ID || inJan[0].Direction != DirToward {
|
|
t.Fatalf("January membership = %+v", inJan)
|
|
}
|
|
all, err := e.GroupMembers(ctx, 0, time.Time{}, time.Time{})
|
|
must(t, err)
|
|
if len(all) != 3 {
|
|
t.Fatalf("history = %+v", all)
|
|
}
|
|
if err := e.EndGroupMember(ctx, g.ID, i2.ID, feb); !errors.Is(err, ErrInvalid) {
|
|
t.Fatalf("end at start = %v", err)
|
|
}
|
|
must(t, e.DeleteGroup(ctx, g.ID))
|
|
if err := e.DeleteGroup(ctx, g.ID); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("second delete = %v", err)
|
|
}
|
|
left, err := e.GroupMembers(ctx, 0, time.Time{}, time.Time{})
|
|
must(t, err)
|
|
if len(left) != 0 {
|
|
t.Fatalf("members survived delete: %+v", left)
|
|
}
|
|
})
|
|
}
|