180 lines
4.8 KiB
Go
180 lines
4.8 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/oklog/ulid/v2"
|
|
)
|
|
|
|
func newTestStoreWithHost(t *testing.T) (*Store, string) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
st, err := Open(context.Background(), filepath.Join(dir, "rm.db"))
|
|
if err != nil {
|
|
t.Fatalf("open: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = st.Close() })
|
|
hostID := ulid.Make().String()
|
|
if err := st.CreateHost(context.Background(), Host{
|
|
ID: hostID, Name: "h", OS: "linux", Arch: "amd64",
|
|
EnrolledAt: time.Now().UTC(),
|
|
}, "deadbeef", ""); err != nil {
|
|
t.Fatalf("create host: %v", err)
|
|
}
|
|
return st, hostID
|
|
}
|
|
|
|
func TestRaiseOrTouchInsertsThenTouches(t *testing.T) {
|
|
t.Parallel()
|
|
st, hostID := newTestStoreWithHost(t)
|
|
ctx := context.Background()
|
|
|
|
t0 := time.Now().UTC()
|
|
id1, didRaise, err := st.RaiseOrTouch(ctx, hostID, "backup_failed", "warning",
|
|
"Backup failed: 401", t0)
|
|
if err != nil {
|
|
t.Fatalf("first raise: %v", err)
|
|
}
|
|
if !didRaise {
|
|
t.Fatalf("first call must didRaise=true")
|
|
}
|
|
if id1 == "" {
|
|
t.Fatalf("expected non-empty id")
|
|
}
|
|
|
|
// Second call within the same open window should touch, not insert.
|
|
t1 := t0.Add(60 * time.Second)
|
|
id2, didRaise2, err := st.RaiseOrTouch(ctx, hostID, "backup_failed", "warning",
|
|
"Backup failed: 401 (still)", t1)
|
|
if err != nil {
|
|
t.Fatalf("touch: %v", err)
|
|
}
|
|
if didRaise2 {
|
|
t.Fatalf("second call must didRaise=false")
|
|
}
|
|
if id2 != id1 {
|
|
t.Fatalf("touch returned a different id: got %q want %q", id2, id1)
|
|
}
|
|
|
|
// last_seen_at and message must be updated.
|
|
got, err := st.GetAlert(ctx, id1)
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
if got.LastSeenAt == nil || !got.LastSeenAt.Equal(t1) {
|
|
t.Errorf("last_seen_at: got %v want %v", got.LastSeenAt, t1)
|
|
}
|
|
if got.Message != "Backup failed: 401 (still)" {
|
|
t.Errorf("message not refreshed: %q", got.Message)
|
|
}
|
|
}
|
|
|
|
func TestResolveAndReRaiseStartsFreshAlert(t *testing.T) {
|
|
t.Parallel()
|
|
st, hostID := newTestStoreWithHost(t)
|
|
ctx := context.Background()
|
|
|
|
t0 := time.Now().UTC()
|
|
id1, _, err := st.RaiseOrTouch(ctx, hostID, "backup_failed", "warning", "first", t0)
|
|
if err != nil {
|
|
t.Fatalf("raise: %v", err)
|
|
}
|
|
if err := st.Resolve(ctx, id1, t0.Add(time.Minute)); err != nil {
|
|
t.Fatalf("resolve: %v", err)
|
|
}
|
|
|
|
id2, didRaise, err := st.RaiseOrTouch(ctx, hostID, "backup_failed", "warning", "second", t0.Add(2*time.Minute))
|
|
if err != nil {
|
|
t.Fatalf("re-raise: %v", err)
|
|
}
|
|
if !didRaise {
|
|
t.Fatalf("post-resolve raise must didRaise=true")
|
|
}
|
|
if id2 == id1 {
|
|
t.Fatalf("re-raise reused the resolved id; want a fresh row")
|
|
}
|
|
}
|
|
|
|
func TestAcknowledgeKeepsAlertOpen(t *testing.T) {
|
|
t.Parallel()
|
|
st, hostID := newTestStoreWithHost(t)
|
|
ctx := context.Background()
|
|
|
|
// Create a real user so the acknowledged_by FK is satisfied.
|
|
userID := ulid.Make().String()
|
|
if err := st.CreateUser(ctx, User{
|
|
ID: userID, Username: "ackuser", PasswordHash: "x",
|
|
Role: RoleOperator, CreatedAt: time.Now().UTC(),
|
|
}); err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
|
|
id, _, err := st.RaiseOrTouch(ctx, hostID, "backup_failed", "warning", "m", time.Now().UTC())
|
|
if err != nil {
|
|
t.Fatalf("raise: %v", err)
|
|
}
|
|
if err := st.Acknowledge(ctx, id, userID, time.Now().UTC()); err != nil {
|
|
t.Fatalf("ack: %v", err)
|
|
}
|
|
got, err := st.GetAlert(ctx, id)
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
if got.AcknowledgedAt == nil {
|
|
t.Errorf("acknowledged_at not set")
|
|
}
|
|
if got.AcknowledgedBy == nil || *got.AcknowledgedBy != userID {
|
|
t.Errorf("acknowledged_by: got %v want %q", got.AcknowledgedBy, userID)
|
|
}
|
|
if got.ResolvedAt != nil {
|
|
t.Errorf("ack must not set resolved_at; got %v", got.ResolvedAt)
|
|
}
|
|
}
|
|
|
|
func TestAutoResolveClearsOpenAlerts(t *testing.T) {
|
|
t.Parallel()
|
|
st, hostID := newTestStoreWithHost(t)
|
|
ctx := context.Background()
|
|
|
|
t0 := time.Now().UTC()
|
|
id, _, _ := st.RaiseOrTouch(ctx, hostID, "backup_failed", "warning", "m", t0)
|
|
if err := st.AutoResolve(ctx, hostID, "backup_failed", t0.Add(time.Minute)); err != nil {
|
|
t.Fatalf("auto-resolve: %v", err)
|
|
}
|
|
got, _ := st.GetAlert(ctx, id)
|
|
if got.ResolvedAt == nil {
|
|
t.Errorf("expected resolved_at set")
|
|
}
|
|
}
|
|
|
|
func TestListAlertsFilters(t *testing.T) {
|
|
t.Parallel()
|
|
st, hostID := newTestStoreWithHost(t)
|
|
ctx := context.Background()
|
|
t0 := time.Now().UTC()
|
|
|
|
// One open warning + one resolved info.
|
|
_, _, _ = st.RaiseOrTouch(ctx, hostID, "backup_failed", "warning", "open", t0)
|
|
id2, _, _ := st.RaiseOrTouch(ctx, hostID, "stale_schedule", "info", "done", t0)
|
|
_ = st.Resolve(ctx, id2, t0.Add(time.Minute))
|
|
|
|
open, err := st.ListAlerts(ctx, AlertFilter{Status: "open"})
|
|
if err != nil {
|
|
t.Fatalf("list open: %v", err)
|
|
}
|
|
if len(open) != 1 || open[0].Severity != "warning" {
|
|
t.Errorf("open filter: got %+v", open)
|
|
}
|
|
|
|
all, err := st.ListAlerts(ctx, AlertFilter{Status: "all"})
|
|
if err != nil {
|
|
t.Fatalf("list all: %v", err)
|
|
}
|
|
if len(all) != 2 {
|
|
t.Errorf("all filter: got %d, want 2", len(all))
|
|
}
|
|
}
|