feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
233
internal/webapp/store_test.go
Обычный файл
233
internal/webapp/store_test.go
Обычный файл
@@ -0,0 +1,233 @@
|
||||
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)
|
||||
}
|
||||
Ссылка в новой задаче
Block a user