234 строки
5.9 KiB
Go
234 строки
5.9 KiB
Go
package webapp
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func newTestStore(t *testing.T) *Store {
|
|
t.Helper()
|
|
// Use a per-test in-memory DSN so the schema is fresh and there
|
|
// are no goroutine-leak concerns from shared cache.
|
|
store, err := OpenStore(":memory:")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = store.Close() })
|
|
return store
|
|
}
|
|
|
|
func TestStoreCreateAndGetUser(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
_, err := s.GetUser(ctx)
|
|
require.ErrorIs(t, err, sql.ErrNoRows, "fresh store must have no user")
|
|
|
|
hash, err := HashPassword("hello world")
|
|
require.NoError(t, err)
|
|
u, err := s.CreateUser(ctx, hash, false)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, u)
|
|
assert.NotZero(t, u.ID)
|
|
assert.False(t, u.RequiresChange, "fresh user must not require change")
|
|
|
|
got, err := s.GetUser(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, u.ID, got.ID)
|
|
assert.Equal(t, hash, got.BcryptHash)
|
|
}
|
|
|
|
func TestStoreUpdatePasswordMarksRequiresChange(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
hash, err := HashPassword("first")
|
|
require.NoError(t, err)
|
|
u, err := s.CreateUser(ctx, hash, false)
|
|
require.NoError(t, err)
|
|
|
|
newHash, err := HashPassword("second")
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.UpdatePassword(ctx, u.ID, newHash))
|
|
|
|
got, err := s.GetUser(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, newHash, got.BcryptHash)
|
|
assert.False(t, got.RequiresChange)
|
|
|
|
require.NoError(t, s.MarkRequiresChange(ctx, u.ID))
|
|
got, err = s.GetUser(ctx)
|
|
require.NoError(t, err)
|
|
assert.True(t, got.RequiresChange)
|
|
}
|
|
|
|
func TestStoreSessionLifecycle(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
hash, err := HashPassword("p")
|
|
require.NoError(t, err)
|
|
u, err := s.CreateUser(ctx, hash, false)
|
|
require.NoError(t, err)
|
|
|
|
sess := Session{
|
|
ID: "sess-1",
|
|
UserID: u.ID,
|
|
CSRFToken: "csrf-1",
|
|
IP: "127.0.0.1",
|
|
UA: "ua",
|
|
ExpiresAt: time.Now().Add(time.Minute).UTC(),
|
|
}
|
|
require.NoError(t, s.CreateSession(ctx, &sess))
|
|
|
|
got, err := s.GetSession(ctx, "sess-1")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, sess.CSRFToken, got.CSRFToken)
|
|
assert.Equal(t, u.ID, got.UserID)
|
|
|
|
require.NoError(t, s.TouchSession(ctx, "sess-1", 5*time.Minute, 8*time.Hour))
|
|
require.NoError(t, s.DeleteSession(ctx, "sess-1"))
|
|
|
|
_, err = s.GetSession(ctx, "sess-1")
|
|
assert.ErrorIs(t, err, sql.ErrNoRows)
|
|
}
|
|
|
|
func TestStoreGetSessionExpired(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
hash, err := HashPassword("p")
|
|
require.NoError(t, err)
|
|
u, err := s.CreateUser(ctx, hash, false)
|
|
require.NoError(t, err)
|
|
|
|
sess := Session{
|
|
ID: "sess-2",
|
|
UserID: u.ID,
|
|
CSRFToken: "csrf-2",
|
|
ExpiresAt: time.Now().Add(-time.Second).UTC(),
|
|
}
|
|
require.NoError(t, s.CreateSession(ctx, &sess))
|
|
|
|
_, err = s.GetSession(ctx, "sess-2")
|
|
assert.ErrorIs(t, err, sql.ErrNoRows, "expired session must surface as no-rows")
|
|
}
|
|
|
|
func TestStoreCreateSessionValidation(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
cases := []struct {
|
|
name string
|
|
sess Session
|
|
}{
|
|
{
|
|
"empty id",
|
|
Session{CSRFToken: "c", UserID: 1, ExpiresAt: time.Now().Add(time.Minute)},
|
|
},
|
|
{
|
|
"empty csrf",
|
|
Session{ID: "x", UserID: 1, ExpiresAt: time.Now().Add(time.Minute)},
|
|
},
|
|
{
|
|
"empty expiry",
|
|
Session{ID: "x", UserID: 1, CSRFToken: "c"},
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := s.CreateSession(ctx, &tc.sess)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStoreAuditWriteAndRecent(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
for i := 0; i < 5; i++ {
|
|
require.NoError(t, s.WriteAudit(ctx, &AuditEntry{
|
|
Actor: "operator",
|
|
Role: "admin",
|
|
AuthMode: "local",
|
|
Action: "login",
|
|
Target: "self",
|
|
BeforeHash: "",
|
|
AfterHash: "",
|
|
}))
|
|
}
|
|
got, err := s.RecentAudit(ctx, 3)
|
|
require.NoError(t, err)
|
|
assert.Len(t, got, 3, "RecentAudit must respect limit")
|
|
|
|
// Empty action must be rejected.
|
|
err = s.WriteAudit(ctx, &AuditEntry{Action: ""})
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestStoreAuditPrune(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
now := time.Now().UTC()
|
|
// 10 rows older than 1h and 5 rows from "now".
|
|
for i := 0; i < 10; i++ {
|
|
require.NoError(t, s.WriteAudit(ctx, &AuditEntry{
|
|
Action: "old",
|
|
TS: now.Add(-2 * time.Hour),
|
|
}))
|
|
}
|
|
for i := 0; i < 5; i++ {
|
|
require.NoError(t, s.WriteAudit(ctx, &AuditEntry{
|
|
Action: "fresh",
|
|
TS: now,
|
|
}))
|
|
}
|
|
|
|
n, err := s.PruneAudit(ctx, time.Hour)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(10), n, "expected to prune exactly the 10 old rows")
|
|
|
|
got, err := s.RecentAudit(ctx, 100)
|
|
require.NoError(t, err)
|
|
require.Len(t, got, 5)
|
|
for _, e := range got {
|
|
assert.Equal(t, "fresh", e.Action, "only fresh rows must remain")
|
|
}
|
|
}
|
|
|
|
func TestStoreReplaceAndListApps(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
first := []App{
|
|
{Name: "rsmon-worker", Source: "process", PID: 100, Ports: "7401/tcp", StartTS: 1, LastSeen: time.Now().UTC(), JSONBlob: `{"foo":"bar"}`},
|
|
{Name: "postgres", Source: "process", PID: 200, Ports: "5432/tcp", StartTS: 2, LastSeen: time.Now().UTC(), JSONBlob: ""},
|
|
}
|
|
require.NoError(t, s.ReplaceApps(ctx, first))
|
|
|
|
list, err := s.ListApps(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, list, 2)
|
|
assert.Equal(t, "rsmon-worker", list[0].Name)
|
|
assert.True(t, strings.HasPrefix(list[0].JSONBlob, "{"), "JSON blob must round-trip")
|
|
|
|
second := []App{
|
|
{Name: "redis", Source: "process", PID: 300, Ports: "6379/tcp", StartTS: 3, LastSeen: time.Now().UTC()},
|
|
}
|
|
require.NoError(t, s.ReplaceApps(ctx, second))
|
|
|
|
list, err = s.ListApps(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, list, 1)
|
|
assert.Equal(t, "redis", list[0].Name)
|
|
}
|
|
|
|
func TestStoreOpenInvalidPath(t *testing.T) {
|
|
_, err := OpenStore("")
|
|
assert.True(t, errors.Is(err, err) || err != nil)
|
|
}
|