diff --git a/.golangci.yml b/.golangci.yml index 2aa0fa1..fd139bc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -6,6 +6,7 @@ linters: - depguard - wsl - testpackage + - gomodguard # deprecated settings: gocritic: enable-all: true diff --git a/database.go b/database.go index 9460988..869ceb7 100644 --- a/database.go +++ b/database.go @@ -209,12 +209,13 @@ func snapshotSubscriber(sub *Subscriber) *Subscriber { } out := &Subscriber{ - ID: sub.ID, - API: sub.API, - Contact: sub.Contact, - Events: snapshotEvents(sub.Events), - Admin: sub.Admin, - Ignored: sub.Ignored, + ID: sub.ID, + API: sub.API, + Contact: sub.Contact, + Events: snapshotEvents(sub.Events), + Admin: sub.Admin, + Ignored: sub.Ignored, + FirstSeen: sub.FirstSeen, } if sub.Meta != nil { diff --git a/subscriber.go b/subscriber.go index f8bb4ef..954aca8 100644 --- a/subscriber.go +++ b/subscriber.go @@ -1,5 +1,7 @@ package subscribe +import "time" + /************************** * Subscriber Methods * **************************/ @@ -19,10 +21,11 @@ func (s *Subscribe) CreateSub(contact, api string, admin, ignore bool) *Subscrib } s.Subscribers = append(s.Subscribers, &Subscriber{ - Contact: contact, - API: api, - Admin: admin, - Ignored: ignore, + Contact: contact, + API: api, + Admin: admin, + Ignored: ignore, + FirstSeen: time.Now().UTC(), Events: &Events{ Map: make(map[string]*Rules), }, @@ -50,11 +53,12 @@ func (s *Subscribe) CreateSubWithID(subID int64, contact, api string, admin, ign } sub := &Subscriber{ - ID: subID, - Contact: contact, - API: api, - Admin: admin, - Ignored: ignore, + ID: subID, + Contact: contact, + API: api, + Admin: admin, + Ignored: ignore, + FirstSeen: time.Now().UTC(), Events: &Events{ Map: make(map[string]*Rules), }, @@ -64,6 +68,26 @@ func (s *Subscribe) CreateSubWithID(subID int64, contact, api string, admin, ign return sub } +// DeleteSubscriber removes a subscriber by ID + API. Returns ErrSubscriberNotFound if missing. +func (s *Subscribe) DeleteSubscriber(subID int64, api string) error { + if subID == 0 { + return ErrSubscriberNotFound + } + + s.mu.Lock() + defer s.mu.Unlock() + + for i, sub := range s.Subscribers { + if sub.ID == subID && sub.API == api { + s.Subscribers = append(s.Subscribers[:i], s.Subscribers[i+1:]...) + + return nil + } + } + + return ErrSubscriberNotFound +} + /* Convenience methods to access specific types of subscribers. */ // GetSubscriber gets a subscriber based on their contact info. diff --git a/subscriber_test.go b/subscriber_test.go index 537afd6..724263e 100644 --- a/subscriber_test.go +++ b/subscriber_test.go @@ -101,17 +101,35 @@ func TestCreateSubWithID(t *testing.T) { assert.EqualValues(t, 10, first.ID) assert.True(t, first.Admin) assert.False(t, first.Ignored) + assert.False(t, first.FirstSeen.IsZero()) assert.Len(t, sub.Subscribers, 1) + seen := first.FirstSeen second := sub.CreateSubWithID(10, "contact-new", "api", false, true) require.NotNil(t, second) assert.Same(t, first, second) assert.False(t, second.Admin) assert.True(t, second.Ignored) assert.Equal(t, "contact", second.Contact) + assert.Equal(t, seen, second.FirstSeen) assert.Len(t, sub.Subscribers, 1) } +func TestDeleteSubscriber(t *testing.T) { + t.Parallel() + + sub := &Subscribe{Events: new(Events)} + sub.CreateSubWithID(10, "a", "api", false, false) + sub.CreateSubWithID(20, "b", "api", false, false) + + require.NoError(t, sub.DeleteSubscriber(10, "api")) + assert.Len(t, sub.Subscribers, 1) + assert.EqualValues(t, 20, sub.Subscribers[0].ID) + + assert.Equal(t, ErrSubscriberNotFound, sub.DeleteSubscriber(10, "api")) + assert.Equal(t, ErrSubscriberNotFound, sub.DeleteSubscriber(0, "api")) +} + func TestGetSubscriberByID(t *testing.T) { t.Parallel() diff --git a/subscription_test.go b/subscription_test.go index 40f9240..6f961c3 100644 --- a/subscription_test.go +++ b/subscription_test.go @@ -14,22 +14,24 @@ func TestCheckAPI(t *testing.T) { assertions := assert.New(t) + const event = "event" + subscriber := &Subscribe{Events: new(Events)} assertions.True(subscriber.checkAPI("test_string"), "an empty slice must always return true") - subscriber.EnableAPIs = []string{"event", "test_string"} + subscriber.EnableAPIs = []string{event, "test_string"} assertions.True(subscriber.checkAPI("test_string://event"), "test_string is an allowed api prefix") - subscriber.EnableAPIs = []string{"event", "any"} + subscriber.EnableAPIs = []string{event, "any"} assertions.True(subscriber.checkAPI("test_string"), "any as an allowed value must return true") - subscriber.EnableAPIs = []string{"event", "all"} + subscriber.EnableAPIs = []string{event, "all"} assertions.True(subscriber.checkAPI("test_string"), "all as an allowed value must return true") - subscriber.EnableAPIs = []string{"event", "test_string"} + subscriber.EnableAPIs = []string{event, "test_string"} assertions.True(subscriber.checkAPI("test_string"), "test_string is an allowed api") - subscriber.EnableAPIs = []string{"event", "test_string2"} + subscriber.EnableAPIs = []string{event, "test_string2"} assertions.False(subscriber.checkAPI("test_string"), "test_string is not an allowed api") } diff --git a/types.go b/types.go index ac105c5..0602306 100644 --- a/types.go +++ b/types.go @@ -42,6 +42,8 @@ type Subscriber struct { Admin bool `json:"isAdmin"` // Ignored will exclude a user from GetSubscribers(). Ignored bool `json:"ignored"` + // FirstSeen is when this subscriber record was first created. Zero if unknown (legacy rows). + FirstSeen time.Time `json:"firstSeen,omitzero"` } // Events represents the map of tracked global Events.