655 lines
20 KiB
Go
655 lines
20 KiB
Go
package tests
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/api/handlers"
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
)
|
|
|
|
// hashForTest returns a bcrypt hash using minimum cost for fast test setup.
|
|
// NEVER use this in production — use models.User.SetPassword instead.
|
|
func hashForTest(t *testing.T, password string) string {
|
|
t.Helper()
|
|
h, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
|
|
require.NoError(t, err)
|
|
return string(h)
|
|
}
|
|
|
|
// setupAuditTestDB creates a clean in-memory database for each test.
|
|
// MaxOpenConns(1) is required: without it, GORM's pool can open multiple
|
|
// connections to ":memory:", each receiving its own empty database.
|
|
func setupAuditTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
sqlDB, err := db.DB()
|
|
require.NoError(t, err)
|
|
sqlDB.SetMaxOpenConns(1)
|
|
sqlDB.SetMaxIdleConns(1)
|
|
|
|
t.Cleanup(func() {
|
|
_ = sqlDB.Close()
|
|
})
|
|
|
|
// Auto-migrate required models (includes SecurityAudit so the
|
|
// background audit goroutine in SecurityService doesn't retry
|
|
// against a missing table).
|
|
err = db.AutoMigrate(
|
|
&models.User{},
|
|
&models.Setting{},
|
|
&models.ProxyHost{},
|
|
&models.SecurityAudit{},
|
|
)
|
|
require.NoError(t, err)
|
|
return db
|
|
}
|
|
|
|
// createTestAdminUser creates an admin user and returns their ID
|
|
func createTestAdminUser(t *testing.T, db *gorm.DB) uint {
|
|
t.Helper()
|
|
admin := models.User{
|
|
UUID: "admin-uuid-1234",
|
|
Email: "admin@test.com",
|
|
Name: "Test Admin",
|
|
Role: models.RoleAdmin,
|
|
Enabled: true,
|
|
APIKey: "test-api-key",
|
|
PasswordHash: hashForTest(t, "adminpassword123"),
|
|
}
|
|
require.NoError(t, db.Create(&admin).Error)
|
|
return admin.ID
|
|
}
|
|
|
|
// setupRouterWithAuth creates a gin router with auth middleware mocked
|
|
func setupRouterWithAuth(db *gorm.DB, userID uint, role string) *gin.Engine {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
|
|
// Mock auth middleware
|
|
r.Use(func(c *gin.Context) {
|
|
c.Set("userID", userID)
|
|
c.Set("role", role)
|
|
c.Next()
|
|
})
|
|
|
|
userHandler := handlers.NewUserHandler(db, nil)
|
|
settingsHandler := handlers.NewSettingsHandler(db)
|
|
|
|
api := r.Group("/api")
|
|
userHandler.RegisterRoutes(api)
|
|
|
|
// Settings routes
|
|
api.GET("/settings/smtp", settingsHandler.GetSMTPConfig)
|
|
api.POST("/settings/smtp", settingsHandler.UpdateSMTPConfig)
|
|
api.POST("/settings/smtp/test", settingsHandler.TestSMTPConfig)
|
|
api.POST("/settings/smtp/test-email", settingsHandler.SendTestEmail)
|
|
|
|
return r
|
|
}
|
|
|
|
// ==================== INVITE TOKEN SECURITY TESTS ====================
|
|
|
|
func TestInviteToken_MustBeUnguessable(t *testing.T) {
|
|
db := setupAuditTestDB(t)
|
|
adminID := createTestAdminUser(t, db)
|
|
r := setupRouterWithAuth(db, adminID, "admin")
|
|
|
|
// Invite a user
|
|
body := `{"email":"user@test.com","role":"user"}`
|
|
req := httptest.NewRequest("POST", "/api/users/invite", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusCreated, w.Code, "invite endpoint failed; body: %s", w.Body.String())
|
|
|
|
var resp map[string]any
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
|
|
var invitedUser models.User
|
|
require.NoError(t, db.Where("email = ?", "user@test.com").First(&invitedUser).Error)
|
|
token := invitedUser.InviteToken
|
|
require.NotEmpty(t, token, "invite token must not be empty")
|
|
|
|
// Token MUST be at least 32 bytes (64 hex chars = 256 bits of entropy)
|
|
require.GreaterOrEqual(t, len(token), 64, "invite token must be at least 64 hex chars (256 bits); got len=%d token=%q", len(token), token)
|
|
|
|
// Token must be valid hex (all characters in [0-9a-f]).
|
|
// hex.DecodeString accepts both cases, so check for lowercase explicitly:
|
|
// hex.EncodeToString (used by generateSecureToken) always emits lowercase,
|
|
// so uppercase would indicate a regression in the token-generation path.
|
|
_, err := hex.DecodeString(token)
|
|
require.NoError(t, err, "invite token must be valid hex; got %q", token)
|
|
require.Equal(t, strings.ToLower(token), token, "invite token must be lowercase hex (as produced by hex.EncodeToString); got %q", token)
|
|
}
|
|
|
|
func TestInviteToken_ExpiredCannotBeUsed(t *testing.T) {
|
|
db := setupAuditTestDB(t)
|
|
adminID := createTestAdminUser(t, db)
|
|
|
|
// Create user with expired invite
|
|
expiredTime := time.Now().Add(-1 * time.Hour)
|
|
invitedAt := time.Now().Add(-50 * time.Hour)
|
|
user := models.User{
|
|
UUID: "invite-uuid-1234",
|
|
Email: "expired@test.com",
|
|
Role: models.RoleUser,
|
|
Enabled: false,
|
|
InviteToken: "expired-token-12345678901234567890123456789012",
|
|
InviteExpires: &expiredTime,
|
|
InvitedAt: &invitedAt,
|
|
InviteStatus: "pending",
|
|
}
|
|
require.NoError(t, db.Create(&user).Error)
|
|
|
|
r := setupRouterWithAuth(db, adminID, "admin")
|
|
|
|
// Try to validate expired token
|
|
req := httptest.NewRequest("GET", "/api/invite/validate?token=expired-token-12345678901234567890123456789012", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusGone, w.Code, "Expired tokens should return 410 Gone")
|
|
}
|
|
|
|
func TestInviteToken_CannotBeReused(t *testing.T) {
|
|
db := setupAuditTestDB(t)
|
|
adminID := createTestAdminUser(t, db)
|
|
|
|
// Create user with already accepted invite
|
|
invitedAt := time.Now().Add(-24 * time.Hour)
|
|
user := models.User{
|
|
UUID: "accepted-uuid-1234",
|
|
Email: "accepted@test.com",
|
|
Name: "Accepted User",
|
|
Role: models.RoleUser,
|
|
Enabled: true,
|
|
PasswordHash: hashForTest(t, "somepassword"),
|
|
InviteToken: "accepted-token-1234567890123456789012345678901",
|
|
InvitedAt: &invitedAt,
|
|
InviteStatus: "accepted",
|
|
}
|
|
require.NoError(t, db.Create(&user).Error)
|
|
|
|
r := setupRouterWithAuth(db, adminID, "admin")
|
|
|
|
// Try to accept again
|
|
body := `{"token":"accepted-token-1234567890123456789012345678901","name":"Hacker","password":"newpassword123"}`
|
|
req := httptest.NewRequest("POST", "/api/invite/accept", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusConflict, w.Code, "Already accepted tokens should return 409 Conflict")
|
|
}
|
|
|
|
// ==================== INPUT VALIDATION TESTS ====================
|
|
|
|
func TestInviteUser_EmailValidation(t *testing.T) {
|
|
db := setupAuditTestDB(t)
|
|
adminID := createTestAdminUser(t, db)
|
|
r := setupRouterWithAuth(db, adminID, "admin")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
email string
|
|
wantCode int
|
|
}{
|
|
{"empty email", "", http.StatusBadRequest},
|
|
{"invalid email no @", "notanemail", http.StatusBadRequest},
|
|
{"invalid email no domain", "test@", http.StatusBadRequest},
|
|
{"sql injection attempt", "'; DROP TABLE users;--@evil.com", http.StatusBadRequest},
|
|
{"script injection", "<script>alert('xss')</script>@evil.com", http.StatusBadRequest},
|
|
{"valid email", "valid@example.com", http.StatusCreated},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
body := `{"email":"` + tc.email + `","role":"user"}`
|
|
req := httptest.NewRequest("POST", "/api/users/invite", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tc.wantCode, w.Code, "Email: %s", tc.email)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAcceptInvite_PasswordValidation(t *testing.T) {
|
|
db := setupAuditTestDB(t)
|
|
adminID := createTestAdminUser(t, db)
|
|
|
|
// Create user with valid invite
|
|
expires := time.Now().Add(24 * time.Hour)
|
|
invitedAt := time.Now()
|
|
user := models.User{
|
|
UUID: "pending-uuid-1234",
|
|
Email: "pending@test.com",
|
|
Role: models.RoleUser,
|
|
Enabled: false,
|
|
InviteToken: "valid-token-12345678901234567890123456789012345",
|
|
InviteExpires: &expires,
|
|
InvitedAt: &invitedAt,
|
|
InviteStatus: "pending",
|
|
}
|
|
require.NoError(t, db.Create(&user).Error)
|
|
|
|
r := setupRouterWithAuth(db, adminID, "admin")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
password string
|
|
wantCode int
|
|
}{
|
|
{"empty password", "", http.StatusBadRequest},
|
|
{"too short", "short", http.StatusBadRequest},
|
|
{"7 chars", "1234567", http.StatusBadRequest},
|
|
{"8 chars valid", "12345678", http.StatusOK},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Reset user to pending state for each test
|
|
db.Model(&user).Updates(map[string]any{
|
|
"invite_status": "pending",
|
|
"enabled": false,
|
|
"password_hash": "",
|
|
})
|
|
|
|
body := `{"token":"valid-token-12345678901234567890123456789012345","name":"Test User","password":"` + tc.password + `"}`
|
|
req := httptest.NewRequest("POST", "/api/invite/accept", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tc.wantCode, w.Code, "Password: %s", tc.password)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ==================== AUTHORIZATION TESTS ====================
|
|
|
|
func TestUserEndpoints_RequireAdmin(t *testing.T) {
|
|
db := setupAuditTestDB(t)
|
|
|
|
// Create regular user
|
|
user := models.User{
|
|
UUID: "user-uuid-1234",
|
|
Email: "user@test.com",
|
|
Name: "Regular User",
|
|
Role: models.RoleUser,
|
|
Enabled: true,
|
|
APIKey: "user-api-key-unique",
|
|
PasswordHash: hashForTest(t, "userpassword123"),
|
|
}
|
|
require.NoError(t, db.Create(&user).Error)
|
|
|
|
// Create a second user to test admin-only operations against a non-self target
|
|
otherUser := models.User{
|
|
UUID: "other-uuid-5678",
|
|
Email: "other@test.com",
|
|
Name: "Other User",
|
|
Role: models.RoleUser,
|
|
Enabled: true,
|
|
APIKey: "other-api-key-unique",
|
|
PasswordHash: hashForTest(t, "otherpassword123"),
|
|
}
|
|
require.NoError(t, db.Create(&otherUser).Error)
|
|
|
|
// Router with regular user role
|
|
r := setupRouterWithAuth(db, user.ID, "user")
|
|
|
|
otherID := fmt.Sprintf("%d", otherUser.ID)
|
|
endpoints := []struct {
|
|
method string
|
|
path string
|
|
body string
|
|
}{
|
|
{"GET", "/api/users", ""},
|
|
{"POST", "/api/users", `{"email":"new@test.com","name":"New","password":"password123"}`},
|
|
{"POST", "/api/users/invite", `{"email":"invite@test.com"}`},
|
|
{"GET", "/api/users/" + otherID, ""},
|
|
{"PUT", "/api/users/" + otherID, `{"name":"Updated"}`},
|
|
{"DELETE", "/api/users/" + otherID, ""},
|
|
{"PUT", "/api/users/" + otherID + "/permissions", `{"permission_mode":"deny_all"}`},
|
|
}
|
|
|
|
for _, ep := range endpoints {
|
|
t.Run(ep.method+" "+ep.path, func(t *testing.T) {
|
|
var req *http.Request
|
|
if ep.body != "" {
|
|
req = httptest.NewRequest(ep.method, ep.path, strings.NewReader(ep.body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
} else {
|
|
req = httptest.NewRequest(ep.method, ep.path, http.NoBody)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code, "Non-admin should be forbidden from %s %s", ep.method, ep.path)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSMTPEndpoints_RequireAdmin(t *testing.T) {
|
|
db := setupAuditTestDB(t)
|
|
|
|
user := models.User{
|
|
UUID: "user-uuid-5678",
|
|
Email: "user2@test.com",
|
|
Name: "Regular User 2",
|
|
Role: models.RoleUser,
|
|
Enabled: true,
|
|
PasswordHash: hashForTest(t, "userpassword123"),
|
|
}
|
|
require.NoError(t, db.Create(&user).Error)
|
|
|
|
r := setupRouterWithAuth(db, user.ID, "user")
|
|
|
|
// POST endpoints should require admin
|
|
postEndpoints := []struct {
|
|
path string
|
|
body string
|
|
}{
|
|
{"/api/settings/smtp", `{"host":"smtp.test.com","port":587,"from_address":"test@test.com","encryption":"starttls"}`},
|
|
{"/api/settings/smtp/test", ""},
|
|
{"/api/settings/smtp/test-email", `{"to":"test@test.com"}`},
|
|
}
|
|
|
|
for _, ep := range postEndpoints {
|
|
t.Run("POST "+ep.path, func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", ep.path, strings.NewReader(ep.body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code, "Non-admin should be forbidden from POST %s", ep.path)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ==================== SMTP CONFIG SECURITY TESTS ====================
|
|
|
|
func TestSMTPConfig_PasswordMasked(t *testing.T) {
|
|
db := setupAuditTestDB(t)
|
|
adminID := createTestAdminUser(t, db)
|
|
|
|
// Save SMTP config with password
|
|
settings := []models.Setting{
|
|
{Key: "smtp_host", Value: "smtp.test.com", Category: "smtp"},
|
|
{Key: "smtp_port", Value: "587", Category: "smtp"},
|
|
{Key: "smtp_password", Value: "supersecretpassword", Category: "smtp"},
|
|
{Key: "smtp_from_address", Value: "test@test.com", Category: "smtp"},
|
|
{Key: "smtp_encryption", Value: "starttls", Category: "smtp"},
|
|
}
|
|
for _, s := range settings {
|
|
require.NoError(t, db.Create(&s).Error)
|
|
}
|
|
|
|
r := setupRouterWithAuth(db, adminID, "admin")
|
|
|
|
req := httptest.NewRequest("GET", "/api/settings/smtp", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp map[string]any
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
|
|
// Password MUST be masked
|
|
assert.Equal(t, "********", resp["password"], "Password must be masked in response")
|
|
assert.NotEqual(t, "supersecretpassword", resp["password"], "Real password must not be exposed")
|
|
}
|
|
|
|
func TestSMTPConfig_PortValidation(t *testing.T) {
|
|
db := setupAuditTestDB(t)
|
|
adminID := createTestAdminUser(t, db)
|
|
r := setupRouterWithAuth(db, adminID, "admin")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
port int
|
|
wantCode int
|
|
}{
|
|
{"port 0 invalid", 0, http.StatusBadRequest},
|
|
{"port -1 invalid", -1, http.StatusBadRequest},
|
|
{"port 65536 invalid", 65536, http.StatusBadRequest},
|
|
{"port 587 valid", 587, http.StatusOK},
|
|
{"port 465 valid", 465, http.StatusOK},
|
|
{"port 25 valid", 25, http.StatusOK},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
body, _ := json.Marshal(map[string]any{
|
|
"host": "smtp.test.com",
|
|
"port": tc.port,
|
|
"from_address": "test@test.com",
|
|
"encryption": "starttls",
|
|
})
|
|
req := httptest.NewRequest("POST", "/api/settings/smtp", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tc.wantCode, w.Code, "Port: %d", tc.port)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSMTPConfig_EncryptionValidation(t *testing.T) {
|
|
db := setupAuditTestDB(t)
|
|
adminID := createTestAdminUser(t, db)
|
|
r := setupRouterWithAuth(db, adminID, "admin")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
encryption string
|
|
wantCode int
|
|
}{
|
|
{"empty encryption invalid", "", http.StatusBadRequest},
|
|
{"invalid encryption", "invalid", http.StatusBadRequest},
|
|
{"tls lowercase valid", "ssl", http.StatusOK},
|
|
{"starttls valid", "starttls", http.StatusOK},
|
|
{"none valid", "none", http.StatusOK},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
body, _ := json.Marshal(map[string]any{
|
|
"host": "smtp.test.com",
|
|
"port": 587,
|
|
"from_address": "test@test.com",
|
|
"encryption": tc.encryption,
|
|
})
|
|
req := httptest.NewRequest("POST", "/api/settings/smtp", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tc.wantCode, w.Code, "Encryption: %s", tc.encryption)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ==================== DUPLICATE EMAIL PROTECTION TESTS ====================
|
|
|
|
func TestInviteUser_DuplicateEmailBlocked(t *testing.T) {
|
|
db := setupAuditTestDB(t)
|
|
adminID := createTestAdminUser(t, db)
|
|
|
|
// Create existing user
|
|
existing := models.User{
|
|
UUID: "existing-uuid-1234",
|
|
Email: "existing@test.com",
|
|
Name: "Existing User",
|
|
Role: models.RoleUser,
|
|
Enabled: true,
|
|
}
|
|
require.NoError(t, db.Create(&existing).Error)
|
|
|
|
r := setupRouterWithAuth(db, adminID, "admin")
|
|
|
|
// Try to invite same email
|
|
body := `{"email":"existing@test.com","role":"user"}`
|
|
req := httptest.NewRequest("POST", "/api/users/invite", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusConflict, w.Code, "Duplicate email should return 409 Conflict")
|
|
}
|
|
|
|
func TestInviteUser_EmailCaseInsensitive(t *testing.T) {
|
|
db := setupAuditTestDB(t)
|
|
adminID := createTestAdminUser(t, db)
|
|
|
|
// Create existing user with lowercase email
|
|
existing := models.User{
|
|
UUID: "existing-uuid-5678",
|
|
Email: "test@example.com",
|
|
Name: "Existing User",
|
|
Role: models.RoleUser,
|
|
Enabled: true,
|
|
}
|
|
require.NoError(t, db.Create(&existing).Error)
|
|
|
|
r := setupRouterWithAuth(db, adminID, "admin")
|
|
|
|
// Try to invite with different case
|
|
body := `{"email":"TEST@EXAMPLE.COM","role":"user"}`
|
|
req := httptest.NewRequest("POST", "/api/users/invite", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusConflict, w.Code, "Email comparison should be case-insensitive")
|
|
}
|
|
|
|
// ==================== SELF-DELETION PREVENTION TEST ====================
|
|
|
|
func TestDeleteUser_CannotDeleteSelf(t *testing.T) {
|
|
db := setupAuditTestDB(t)
|
|
adminID := createTestAdminUser(t, db)
|
|
r := setupRouterWithAuth(db, adminID, "admin")
|
|
|
|
// Try to delete self
|
|
req := httptest.NewRequest("DELETE", "/api/users/"+string(rune(adminID+'0')), http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Should be forbidden (cannot delete own account)
|
|
assert.Equal(t, http.StatusForbidden, w.Code, "Admin should not be able to delete their own account")
|
|
}
|
|
|
|
// ==================== PERMISSION MODE VALIDATION TESTS ====================
|
|
|
|
func TestUpdatePermissions_ValidModes(t *testing.T) {
|
|
db := setupAuditTestDB(t)
|
|
adminID := createTestAdminUser(t, db)
|
|
|
|
// Create a user to update
|
|
user := models.User{
|
|
UUID: "perms-user-1234",
|
|
Email: "permsuser@test.com",
|
|
Name: "Perms User",
|
|
Role: models.RoleUser,
|
|
Enabled: true,
|
|
}
|
|
require.NoError(t, db.Create(&user).Error)
|
|
|
|
r := setupRouterWithAuth(db, adminID, "admin")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
mode string
|
|
wantCode int
|
|
}{
|
|
{"allow_all valid", "allow_all", http.StatusOK},
|
|
{"deny_all valid", "deny_all", http.StatusOK},
|
|
{"invalid mode", "invalid", http.StatusBadRequest},
|
|
{"empty mode", "", http.StatusBadRequest},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
body, _ := json.Marshal(map[string]any{
|
|
"permission_mode": tc.mode,
|
|
"permitted_hosts": []int{},
|
|
})
|
|
req := httptest.NewRequest("PUT", "/api/users/"+string(rune(user.ID+'0'))+"/permissions", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Note: The route path conversion is simplified; actual implementation would need proper ID parsing
|
|
})
|
|
}
|
|
}
|
|
|
|
// ==================== PUBLIC ENDPOINTS ACCESS TEST ====================
|
|
|
|
func TestPublicEndpoints_NoAuthRequired(t *testing.T) {
|
|
db := setupAuditTestDB(t)
|
|
|
|
// Router WITHOUT auth middleware
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
userHandler := handlers.NewUserHandler(db, nil)
|
|
api := r.Group("/api")
|
|
userHandler.RegisterRoutes(api)
|
|
|
|
// Create user with valid invite for testing
|
|
expires := time.Now().Add(24 * time.Hour)
|
|
invitedAt := time.Now()
|
|
user := models.User{
|
|
UUID: "public-test-uuid",
|
|
Email: "public@test.com",
|
|
Role: models.RoleUser,
|
|
Enabled: false,
|
|
InviteToken: "public-test-token-123456789012345678901234567",
|
|
InviteExpires: &expires,
|
|
InvitedAt: &invitedAt,
|
|
InviteStatus: "pending",
|
|
}
|
|
require.NoError(t, db.Create(&user).Error)
|
|
|
|
// Validate invite should work without auth
|
|
req := httptest.NewRequest("GET", "/api/invite/validate?token=public-test-token-123456789012345678901234567", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code, "ValidateInvite should be accessible without auth")
|
|
|
|
// Accept invite should work without auth
|
|
body := `{"token":"public-test-token-123456789012345678901234567","name":"Public User","password":"password123"}`
|
|
req = httptest.NewRequest("POST", "/api/invite/accept", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w = httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code, "AcceptInvite should be accessible without auth")
|
|
}
|