Pulse/internal/api/audit_handlers_test.go
rcourtman 668cdf3393 feat(license): add audit_logging, advanced_sso, advanced_reporting to Pro tier
Major changes:
- Add audit_logging, advanced_sso, advanced_reporting features to Pro tier
- Persist session username for RBAC authorization after restart
- Add hot-dev auto-detection for pulse-pro binary (enables SQLite audit logging)

Frontend improvements:
- Replace isEnterprise() with hasFeature() for granular feature gating
- Update AuditLogPanel, OIDCPanel, RolesPanel, UserAssignmentsPanel, AISettings
- Update AuditWebhookPanel to use hasFeature('audit_logging')

Backend changes:
- Session store now persists and restores username field
- Update CreateSession/CreateOIDCSession to accept username parameter
- GetSessionUsername falls back to persisted username after restart

Testing:
- Update license_test.go to reflect Pro tier feature changes
- Update session tests for new username parameter
2026-01-10 12:55:02 +00:00

229 lines
5.4 KiB
Go

package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/rcourtman/pulse-go-rewrite/pkg/audit"
)
type verifyResponse struct {
Available bool `json:"available"`
Verified bool `json:"verified"`
Message string `json:"message"`
}
type testAuditLogger struct {
events []audit.Event
verifyResult bool
queryErr error
}
func (l *testAuditLogger) Log(event audit.Event) error {
l.events = append(l.events, event)
return nil
}
func (l *testAuditLogger) Query(filter audit.QueryFilter) ([]audit.Event, error) {
if l.queryErr != nil {
return nil, l.queryErr
}
if filter.ID != "" {
for _, event := range l.events {
if event.ID == filter.ID {
return []audit.Event{event}, nil
}
}
return []audit.Event{}, nil
}
return l.events, nil
}
func (l *testAuditLogger) Count(filter audit.QueryFilter) (int, error) {
events, err := l.Query(filter)
if err != nil {
return 0, err
}
return len(events), nil
}
func (l *testAuditLogger) Close() error {
return nil
}
func (l *testAuditLogger) VerifySignature(event audit.Event) bool {
return l.verifyResult
}
func (l *testAuditLogger) GetWebhookURLs() []string {
return []string{}
}
func (l *testAuditLogger) UpdateWebhookURLs(urls []string) error {
return nil
}
type testAuditLoggerNoVerify struct {
events []audit.Event
}
func (l *testAuditLoggerNoVerify) Log(event audit.Event) error {
l.events = append(l.events, event)
return nil
}
func (l *testAuditLoggerNoVerify) Query(filter audit.QueryFilter) ([]audit.Event, error) {
return l.events, nil
}
func (l *testAuditLoggerNoVerify) Count(filter audit.QueryFilter) (int, error) {
return len(l.events), nil
}
func (l *testAuditLoggerNoVerify) Close() error {
return nil
}
func (l *testAuditLoggerNoVerify) GetWebhookURLs() []string {
return []string{}
}
func (l *testAuditLoggerNoVerify) UpdateWebhookURLs(urls []string) error {
return nil
}
func setAuditLogger(t *testing.T, logger audit.Logger) {
prev := audit.GetLogger()
audit.SetLogger(logger)
t.Cleanup(func() {
audit.SetLogger(prev)
})
}
func TestHandleVerifyAuditEvent_InvalidPath(t *testing.T) {
handler := NewAuditHandlers()
req := httptest.NewRequest(http.MethodGet, "/api/audit/verify", nil)
rec := httptest.NewRecorder()
handler.HandleVerifyAuditEvent(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
}
}
func TestHandleVerifyAuditEvent_NotPersistent(t *testing.T) {
setAuditLogger(t, audit.NewConsoleLogger())
handler := NewAuditHandlers()
req := httptest.NewRequest(http.MethodGet, "/api/audit/abc/verify", nil)
req.SetPathValue("id", "abc")
rec := httptest.NewRecorder()
handler.HandleVerifyAuditEvent(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
var resp verifyResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Available {
t.Fatalf("expected available to be false")
}
if resp.Message == "" {
t.Fatalf("expected message to be set")
}
}
func TestHandleVerifyAuditEvent_NoVerifier(t *testing.T) {
setAuditLogger(t, &testAuditLoggerNoVerify{})
handler := NewAuditHandlers()
req := httptest.NewRequest(http.MethodGet, "/api/audit/abc/verify", nil)
req.SetPathValue("id", "abc")
rec := httptest.NewRecorder()
handler.HandleVerifyAuditEvent(rec, req)
if rec.Code != http.StatusNotImplemented {
t.Fatalf("expected status %d, got %d", http.StatusNotImplemented, rec.Code)
}
}
func TestHandleVerifyAuditEvent_NotFound(t *testing.T) {
setAuditLogger(t, &testAuditLogger{})
handler := NewAuditHandlers()
req := httptest.NewRequest(http.MethodGet, "/api/audit/abc/verify", nil)
req.SetPathValue("id", "abc")
rec := httptest.NewRecorder()
handler.HandleVerifyAuditEvent(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("expected status %d, got %d", http.StatusNotFound, rec.Code)
}
}
func TestHandleVerifyAuditEvent_Verified(t *testing.T) {
setAuditLogger(t, &testAuditLogger{
events: []audit.Event{{ID: "abc"}},
verifyResult: true,
})
handler := NewAuditHandlers()
req := httptest.NewRequest(http.MethodGet, "/api/audit/abc/verify", nil)
req.SetPathValue("id", "abc")
rec := httptest.NewRecorder()
handler.HandleVerifyAuditEvent(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
var resp verifyResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if !resp.Available {
t.Fatalf("expected available to be true")
}
if !resp.Verified {
t.Fatalf("expected verified to be true")
}
}
func TestHandleVerifyAuditEvent_Failed(t *testing.T) {
setAuditLogger(t, &testAuditLogger{
events: []audit.Event{{ID: "abc"}},
verifyResult: false,
})
handler := NewAuditHandlers()
req := httptest.NewRequest(http.MethodGet, "/api/audit/abc/verify", nil)
req.SetPathValue("id", "abc")
rec := httptest.NewRecorder()
handler.HandleVerifyAuditEvent(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
var resp verifyResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if !resp.Available {
t.Fatalf("expected available to be true")
}
if resp.Verified {
t.Fatalf("expected verified to be false")
}
}