Files
worker/internal/webapp/store_test.go
Gleb Tv 2c7a0236da feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
2026-07-13 17:55:14 +03:00

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)
}