mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-01 21:10:13 +00:00
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
229 lines
5.4 KiB
Go
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")
|
|
}
|
|
}
|