mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
270 lines
7.3 KiB
Go
270 lines
7.3 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/audit"
|
|
)
|
|
|
|
type captureAuditLogger struct {
|
|
events []audit.Event
|
|
}
|
|
|
|
func (c *captureAuditLogger) Log(event audit.Event) error {
|
|
c.events = append(c.events, event)
|
|
return nil
|
|
}
|
|
|
|
func (c *captureAuditLogger) Query(filter audit.QueryFilter) ([]audit.Event, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (c *captureAuditLogger) Count(filter audit.QueryFilter) (int, error) {
|
|
return len(c.events), nil
|
|
}
|
|
|
|
func (c *captureAuditLogger) GetWebhookURLs() []string {
|
|
return nil
|
|
}
|
|
|
|
func (c *captureAuditLogger) UpdateWebhookURLs(urls []string) error {
|
|
return nil
|
|
}
|
|
|
|
func (c *captureAuditLogger) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func setCaptureAuditLogger(t *testing.T) *captureAuditLogger {
|
|
t.Helper()
|
|
previous := audit.GetLogger()
|
|
capture := &captureAuditLogger{}
|
|
audit.SetLogger(capture)
|
|
t.Cleanup(func() {
|
|
audit.SetLogger(previous)
|
|
})
|
|
return capture
|
|
}
|
|
|
|
func TestShouldAutoImport(t *testing.T) {
|
|
dir := t.TempDir()
|
|
t.Setenv("PULSE_DATA_DIR", dir)
|
|
t.Setenv("PULSE_INIT_CONFIG_DATA", "")
|
|
t.Setenv("PULSE_INIT_CONFIG_FILE", "")
|
|
|
|
if ShouldAutoImport() {
|
|
t.Fatal("expected auto-import false without config")
|
|
}
|
|
|
|
t.Setenv("PULSE_INIT_CONFIG_DATA", "payload")
|
|
if !ShouldAutoImport() {
|
|
t.Fatal("expected auto-import true with data")
|
|
}
|
|
|
|
t.Setenv("PULSE_INIT_CONFIG_DATA", "")
|
|
t.Setenv("PULSE_INIT_CONFIG_FILE", "/tmp/file")
|
|
if !ShouldAutoImport() {
|
|
t.Fatal("expected auto-import true with file")
|
|
}
|
|
|
|
file := filepath.Join(dir, "nodes.enc")
|
|
if err := os.WriteFile(file, []byte("x"), 0600); err != nil {
|
|
t.Fatalf("write nodes: %v", err)
|
|
}
|
|
if ShouldAutoImport() {
|
|
t.Fatal("expected auto-import false when nodes.enc exists")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeImportPayload(t *testing.T) {
|
|
if _, err := NormalizeImportPayload([]byte("")); err == nil {
|
|
t.Fatal("expected error for empty payload")
|
|
}
|
|
|
|
raw := []byte("hello")
|
|
encoded := base64.StdEncoding.EncodeToString(raw)
|
|
out, err := NormalizeImportPayload([]byte(encoded))
|
|
if err != nil {
|
|
t.Fatalf("normalize error: %v", err)
|
|
}
|
|
if out != encoded {
|
|
t.Fatalf("unexpected output: %s", out)
|
|
}
|
|
|
|
double := base64.StdEncoding.EncodeToString([]byte(encoded))
|
|
out, err = NormalizeImportPayload([]byte(double))
|
|
if err != nil {
|
|
t.Fatalf("normalize error: %v", err)
|
|
}
|
|
if out != encoded {
|
|
t.Fatalf("unexpected output: %s", out)
|
|
}
|
|
|
|
out, err = NormalizeImportPayload([]byte("not-base64"))
|
|
if err != nil {
|
|
t.Fatalf("normalize error: %v", err)
|
|
}
|
|
if out == "not-base64" {
|
|
t.Fatal("expected payload to be encoded")
|
|
}
|
|
}
|
|
|
|
func TestLooksLikeBase64(t *testing.T) {
|
|
if LooksLikeBase64("") {
|
|
t.Fatal("expected false for empty")
|
|
}
|
|
if !LooksLikeBase64("aGVsbG8=") {
|
|
t.Fatal("expected true for base64")
|
|
}
|
|
if LooksLikeBase64("nope!!") {
|
|
t.Fatal("expected false for invalid")
|
|
}
|
|
}
|
|
|
|
func TestPerformAutoImportErrors(t *testing.T) {
|
|
t.Setenv("PULSE_INIT_CONFIG_DATA", "data")
|
|
t.Setenv("PULSE_INIT_CONFIG_FILE", "")
|
|
t.Setenv("PULSE_INIT_CONFIG_PASSPHRASE", "")
|
|
if err := PerformAutoImport(); err == nil {
|
|
t.Fatal("expected error without passphrase")
|
|
}
|
|
|
|
t.Setenv("PULSE_INIT_CONFIG_PASSPHRASE", "pass")
|
|
t.Setenv("PULSE_INIT_CONFIG_FILE", "/tmp/missing-file")
|
|
t.Setenv("PULSE_INIT_CONFIG_DATA", "")
|
|
if err := PerformAutoImport(); err == nil {
|
|
t.Fatal("expected error for missing file")
|
|
}
|
|
|
|
t.Setenv("PULSE_INIT_CONFIG_FILE", "")
|
|
t.Setenv("PULSE_INIT_CONFIG_DATA", "")
|
|
if err := PerformAutoImport(); err == nil {
|
|
t.Fatal("expected error for missing data")
|
|
}
|
|
}
|
|
|
|
func TestPerformAutoImport_AuditFailureMissingPassphrase(t *testing.T) {
|
|
capture := setCaptureAuditLogger(t)
|
|
t.Setenv("PULSE_INIT_CONFIG_DATA", "data")
|
|
t.Setenv("PULSE_INIT_CONFIG_FILE", "")
|
|
t.Setenv("PULSE_INIT_CONFIG_PASSPHRASE", "")
|
|
|
|
if err := PerformAutoImport(); err == nil {
|
|
t.Fatal("expected error without passphrase")
|
|
}
|
|
|
|
if len(capture.events) != 1 {
|
|
t.Fatalf("expected 1 audit event, got %d", len(capture.events))
|
|
}
|
|
|
|
event := capture.events[0]
|
|
if event.EventType != "config_auto_import" {
|
|
t.Fatalf("unexpected event type: %s", event.EventType)
|
|
}
|
|
if event.Success {
|
|
t.Fatal("expected failure audit event")
|
|
}
|
|
if event.User != "system" {
|
|
t.Fatalf("unexpected audit user: %q", event.User)
|
|
}
|
|
if event.Path != "/startup/auto-import" {
|
|
t.Fatalf("unexpected audit path: %q", event.Path)
|
|
}
|
|
if !strings.Contains(event.Details, "source=env_data") {
|
|
t.Fatalf("expected source in details, got %q", event.Details)
|
|
}
|
|
if !strings.Contains(event.Details, "reason=missing_passphrase") {
|
|
t.Fatalf("expected failure reason in details, got %q", event.Details)
|
|
}
|
|
}
|
|
|
|
func TestEnsureDefaultOrgOwnerMembership_SeedsMissingDefaultOrg(t *testing.T) {
|
|
mtp := config.NewMultiTenantPersistence(t.TempDir())
|
|
|
|
if err := ensureDefaultOrgOwnerMembership(mtp, "admin"); err != nil {
|
|
t.Fatalf("ensureDefaultOrgOwnerMembership: %v", err)
|
|
}
|
|
|
|
org, err := mtp.LoadOrganization("default")
|
|
if err != nil {
|
|
t.Fatalf("LoadOrganization(default): %v", err)
|
|
}
|
|
if org.ID != "default" {
|
|
t.Fatalf("org.ID = %q, want default", org.ID)
|
|
}
|
|
if org.DisplayName != "default" {
|
|
t.Fatalf("org.DisplayName = %q, want default", org.DisplayName)
|
|
}
|
|
if org.OwnerUserID != "admin" {
|
|
t.Fatalf("org.OwnerUserID = %q, want admin", org.OwnerUserID)
|
|
}
|
|
if org.CreatedAt.IsZero() {
|
|
t.Fatal("expected org.CreatedAt to be set")
|
|
}
|
|
if len(org.Members) != 1 {
|
|
t.Fatalf("org.Members length = %d, want 1", len(org.Members))
|
|
}
|
|
if org.Members[0].UserID != "admin" || org.Members[0].Role != models.OrgRoleOwner {
|
|
t.Fatalf("expected admin owner member, got user=%q role=%q", org.Members[0].UserID, org.Members[0].Role)
|
|
}
|
|
}
|
|
|
|
func TestEnsureDefaultOrgOwnerMembership_PreservesOwnerAndAddsAdminOwnerMembership(t *testing.T) {
|
|
mtp := config.NewMultiTenantPersistence(t.TempDir())
|
|
seed := &models.Organization{
|
|
ID: "default",
|
|
DisplayName: "Default",
|
|
OwnerUserID: "alice",
|
|
Members: []models.OrganizationMember{
|
|
{UserID: "alice", Role: models.OrgRoleOwner},
|
|
},
|
|
}
|
|
if err := mtp.SaveOrganization(seed); err != nil {
|
|
t.Fatalf("SaveOrganization(seed): %v", err)
|
|
}
|
|
|
|
if err := ensureDefaultOrgOwnerMembership(mtp, "admin"); err != nil {
|
|
t.Fatalf("ensureDefaultOrgOwnerMembership: %v", err)
|
|
}
|
|
|
|
org, err := mtp.LoadOrganization("default")
|
|
if err != nil {
|
|
t.Fatalf("LoadOrganization(default): %v", err)
|
|
}
|
|
if org.OwnerUserID != "alice" {
|
|
t.Fatalf("org.OwnerUserID = %q, want alice", org.OwnerUserID)
|
|
}
|
|
|
|
roles := map[string]models.OrganizationRole{}
|
|
for _, member := range org.Members {
|
|
roles[member.UserID] = member.Role
|
|
}
|
|
if roles["alice"] != models.OrgRoleOwner {
|
|
t.Fatalf("alice role = %q, want owner", roles["alice"])
|
|
}
|
|
if roles["admin"] != models.OrgRoleOwner {
|
|
t.Fatalf("admin role = %q, want owner", roles["admin"])
|
|
}
|
|
}
|
|
|
|
func TestEnsureDefaultOrgOwnerMembership_NoOpWithoutAdminUser(t *testing.T) {
|
|
mtp := config.NewMultiTenantPersistence(t.TempDir())
|
|
|
|
if err := ensureDefaultOrgOwnerMembership(mtp, ""); err != nil {
|
|
t.Fatalf("ensureDefaultOrgOwnerMembership: %v", err)
|
|
}
|
|
|
|
persistence, err := mtp.GetPersistence("default")
|
|
if err != nil {
|
|
t.Fatalf("GetPersistence(default): %v", err)
|
|
}
|
|
if _, err := persistence.LoadOrganization(); err == nil {
|
|
t.Fatal("expected no persisted org metadata when admin user is empty")
|
|
}
|
|
}
|