Public source validation / validate (push) Failing after 3m8s
138 lines
5.8 KiB
Go
138 lines
5.8 KiB
Go
package alertopsapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/itworx/pulse/internal/alert"
|
|
"github.com/itworx/pulse/internal/audit"
|
|
"github.com/itworx/pulse/internal/auth"
|
|
)
|
|
|
|
type memoryStore struct {
|
|
mu sync.Mutex
|
|
items map[string]alert.Alert
|
|
occurrences map[string]alert.Occurrence
|
|
}
|
|
|
|
func newMemoryStore() *memoryStore {
|
|
return &memoryStore{items: map[string]alert.Alert{}, occurrences: map[string]alert.Occurrence{}}
|
|
}
|
|
func (s *memoryStore) ListAlerts(_ context.Context, _ int, state string) ([]alert.Alert, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
result := make([]alert.Alert, 0)
|
|
for _, item := range s.items {
|
|
if state == "" || string(item.State) == state {
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
func (s *memoryStore) GetAlert(_ context.Context, id string, _ int) (alert.Alert, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
item, ok := s.items[id]
|
|
if !ok {
|
|
return alert.Alert{}, alert.ErrInstanceNotFound
|
|
}
|
|
return item, nil
|
|
}
|
|
func (s *memoryStore) AcknowledgeRevision(_ context.Context, id, actor, key string, at time.Time, expected int64) (alert.Instance, alert.Occurrence, bool, error) {
|
|
return s.mutate(id, actor, key, at, expected, true)
|
|
}
|
|
func (s *memoryStore) Unacknowledge(_ context.Context, id, actor, key string, at time.Time, expected int64) (alert.Instance, alert.Occurrence, bool, error) {
|
|
return s.mutate(id, actor, key, at, expected, false)
|
|
}
|
|
func (s *memoryStore) mutate(id, actor, key string, at time.Time, expected int64, acknowledge bool) (alert.Instance, alert.Occurrence, bool, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
item, ok := s.items[id]
|
|
if !ok {
|
|
return alert.Instance{}, alert.Occurrence{}, false, alert.ErrInstanceNotFound
|
|
}
|
|
if occurrence, ok := s.occurrences[key]; ok {
|
|
return item.Instance, occurrence, true, nil
|
|
}
|
|
if item.Revision != expected {
|
|
return alert.Instance{}, alert.Occurrence{}, false, alert.ErrRevisionConflict
|
|
}
|
|
if acknowledge {
|
|
if item.State != alert.StateFiring && item.State != alert.StatePending {
|
|
return alert.Instance{}, alert.Occurrence{}, false, alert.ErrStateConflict
|
|
}
|
|
item.State, item.RetainedState, item.AcknowledgedBy, item.AcknowledgedAt, item.Reason = alert.StateAcknowledged, alert.StateAcknowledged, actor, &at, "acknowledged"
|
|
} else {
|
|
if item.State != alert.StateAcknowledged {
|
|
return alert.Instance{}, alert.Occurrence{}, false, alert.ErrStateConflict
|
|
}
|
|
item.State, item.RetainedState, item.AcknowledgedBy, item.AcknowledgedAt, item.Reason = alert.StateFiring, alert.StateFiring, "", nil, "unacknowledged"
|
|
}
|
|
item.Revision++
|
|
occurrence := alert.Occurrence{ID: key, InstanceID: id, EvaluationKey: key, EventType: "acknowledge", From: alert.StateFiring, To: item.State, ObservedAt: at, Reason: item.Reason}
|
|
if !acknowledge {
|
|
occurrence.EventType = "unacknowledge"
|
|
occurrence.From = alert.StateAcknowledged
|
|
}
|
|
s.items[id] = item
|
|
s.occurrences[key] = occurrence
|
|
return item.Instance, occurrence, false, nil
|
|
}
|
|
|
|
func TestHandlerRoleMatrixIdempotenceAndAudit(t *testing.T) {
|
|
store := newMemoryStore()
|
|
store.items["instance-1"] = alert.Alert{Instance: alert.Instance{ID: "instance-1", State: alert.StateFiring, RetainedState: alert.StateFiring, Revision: 1, LastValue: 90, SourceHealth: map[string]any{}}, RuleName: "CPU", Severity: alert.SeverityCritical}
|
|
auditStore := &audit.MemoryStore{}
|
|
handler := Handler{Store: store, Audit: auditStore}
|
|
viewer := requestWithPrincipal(http.MethodPost, "/api/v1/alerts/instance-1/acknowledge?revision=1", map[string]string{"evaluationKey": "ack-1"}, auth.RoleViewer)
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, viewer)
|
|
if response.Code != http.StatusForbidden {
|
|
t.Fatalf("viewer status = %d", response.Code)
|
|
}
|
|
operator := requestWithPrincipal(http.MethodPost, "/api/v1/alerts/instance-1/acknowledge?revision=1", map[string]string{"evaluationKey": "ack-1"}, auth.RoleOperator)
|
|
response = httptest.NewRecorder()
|
|
handler.ServeHTTP(response, operator)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("ack status = %d body=%s", response.Code, response.Body.String())
|
|
}
|
|
retry := requestWithPrincipal(http.MethodPost, "/api/v1/alerts/instance-1/acknowledge?revision=1", map[string]string{"evaluationKey": "ack-1"}, auth.RoleOperator)
|
|
response = httptest.NewRecorder()
|
|
handler.ServeHTTP(response, retry)
|
|
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"duplicate":true`) {
|
|
t.Fatalf("duplicate ack = %d %s", response.Code, response.Body.String())
|
|
}
|
|
unack := requestWithPrincipal(http.MethodPost, "/api/v1/alerts/instance-1/unacknowledge?revision=2", map[string]string{"evaluationKey": "unack-1"}, auth.RoleOperator)
|
|
response = httptest.NewRecorder()
|
|
handler.ServeHTTP(response, unack)
|
|
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"state":"firing"`) {
|
|
t.Fatalf("unack = %d %s", response.Code, response.Body.String())
|
|
}
|
|
list := requestWithPrincipal(http.MethodGet, "/api/v1/alerts?state=firing", nil, auth.RoleViewer)
|
|
response = httptest.NewRecorder()
|
|
handler.ServeHTTP(response, list)
|
|
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"ruleName":"CPU"`) {
|
|
t.Fatalf("list = %d %s", response.Code, response.Body.String())
|
|
}
|
|
if len(auditStore.Events) != 3 || auditStore.Events[1].Result != "idempotent" {
|
|
t.Fatalf("audit = %#v", auditStore.Events)
|
|
}
|
|
}
|
|
|
|
func requestWithPrincipal(method, path string, body any, role auth.Role) *http.Request {
|
|
encoded := ""
|
|
if body != nil {
|
|
value, _ := json.Marshal(body)
|
|
encoded = string(value)
|
|
}
|
|
request := httptest.NewRequest(method, path, strings.NewReader(encoded)).WithContext(auth.WithPrincipal(context.Background(), auth.Principal{Subject: "operator-1", Role: role}))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
return request
|
|
}
|