package admin import ( "context" "strings" "time" ) // -------------------------------------------------------------------------- // Fake Store // -------------------------------------------------------------------------- type fakeStore struct { admins map[string]*Admin nodes []NodeRow events map[int64][]NodeEvent audits []AuditEntry lastLogin map[int64]time.Time } func newFakeStore() *fakeStore { return &fakeStore{ admins: map[string]*Admin{}, events: map[int64][]NodeEvent{}, lastLogin: map[int64]time.Time{}, } } func (f *fakeStore) GetAdminByUsername(_ context.Context, username string) (*Admin, error) { a, ok := f.admins[username] if !ok { return nil, ErrAdminNotFound } return a, nil } func (f *fakeStore) CreateAdmin(_ context.Context, username, pwHash string, enc []byte) (int64, error) { id := int64(len(f.admins) + 1) f.admins[username] = &Admin{ID: id, Username: username, PwHash: pwHash, TOTPSecretEnc: enc, Status: "active"} return id, nil } func (f *fakeStore) UpdateLastLogin(_ context.Context, id int64, at time.Time) error { f.lastLogin[id] = at return nil } func (f *fakeStore) ListNodes(_ context.Context, eventsPerNode int) ([]NodeRow, error) { out := make([]NodeRow, len(f.nodes)) copy(out, f.nodes) for i := range out { ev := f.events[out[i].ID] if eventsPerNode > 0 && len(ev) > eventsPerNode { ev = ev[:eventsPerNode] } out[i].RecentEvents = ev } return out, nil } func (f *fakeStore) GetNode(_ context.Context, id int64) (*NodeRow, error) { for i := range f.nodes { if f.nodes[i].ID == id { n := f.nodes[i] return &n, nil } } return nil, nil } func (f *fakeStore) WriteAudit(_ context.Context, actor, action, target, metaJSON string) error { f.audits = append(f.audits, AuditEntry{ ID: int64(len(f.audits) + 1), Actor: actor, Action: action, Target: target, Meta: metaJSON, At: time.Now().UTC(), }) return nil } func (f *fakeStore) QueryAudit(_ context.Context, flt AuditFilter) ([]AuditEntry, int, error) { var matched []AuditEntry for _, e := range f.audits { if flt.Actor != "" && e.Actor != flt.Actor { continue } if flt.Action != "" && e.Action != flt.Action { continue } if flt.Target != "" && !strings.Contains(e.Target, flt.Target) { continue } matched = append(matched, e) } total := len(matched) off := flt.Offset if off > len(matched) { off = len(matched) } matched = matched[off:] if flt.Limit > 0 && len(matched) > flt.Limit { matched = matched[:flt.Limit] } return matched, total, nil } func (f *fakeStore) QueryNodeEvents(_ context.Context, nodeID int64, limit int) ([]NodeEvent, error) { ev := f.events[nodeID] if limit > 0 && len(ev) > limit { ev = ev[:limit] } return ev, nil } // auditFor returns the audit entries whose action matches. func (f *fakeStore) auditFor(action string) []AuditEntry { var out []AuditEntry for _, e := range f.audits { if e.Action == action { out = append(out, e) } } return out } // -------------------------------------------------------------------------- // Fake CodesService // -------------------------------------------------------------------------- type fakeCodes struct { batches []BatchSummary created []CodeBatchParams nextCodes []string nextID int64 voided []int64 voidReturn int64 } func (c *fakeCodes) CreateBatch(_ context.Context, p CodeBatchParams) (*GeneratedBatch, error) { c.created = append(c.created, p) c.nextID++ codes := c.nextCodes if codes == nil { codes = make([]string, p.Count) for i := range codes { codes[i] = "PLAINCODE" + itoa(i) } } return &GeneratedBatch{ BatchID: c.nextID, Plan: p.Plan, DurationDays: p.DurationDays, Channel: p.Channel, Codes: codes, GeneratedAt: time.Now().UTC(), }, nil } func (c *fakeCodes) ListBatches(_ context.Context, limit, offset int) ([]BatchSummary, int, error) { return c.batches, len(c.batches), nil } func (c *fakeCodes) VoidBatch(_ context.Context, batchID int64) (int64, error) { c.voided = append(c.voided, batchID) return c.voidReturn, nil } // -------------------------------------------------------------------------- // Recording Lifecycle / Provision services // -------------------------------------------------------------------------- type lifeCall struct { NodeID int64 Target string Actor string } type recordingLifecycle struct { ready bool err error calls []lifeCall } func (r *recordingLifecycle) TransitionStatus(_ context.Context, nodeID int64, target, actor string) error { r.calls = append(r.calls, lifeCall{nodeID, target, actor}) return r.err } func (r *recordingLifecycle) Ready() bool { return r.ready } type provCall struct { NodeID int64 Actor string } type recordingProvision struct { ready bool err error calls []provCall } func (r *recordingProvision) Replace(_ context.Context, nodeID int64, actor string) error { r.calls = append(r.calls, provCall{nodeID, actor}) return r.err } func (r *recordingProvision) Ready() bool { return r.ready } func itoa(i int) string { if i == 0 { return "0" } var b [20]byte pos := len(b) for i > 0 { pos-- b[pos] = byte('0' + i%10) i /= 10 } return string(b[pos:]) }