chore: git cache cleanup

This commit is contained in:
GitHub Actions
2026-03-04 18:34:39 +00:00
parent 7a8b0343e4
commit c32cce2a88
2001 changed files with 0 additions and 683185 deletions

View File

@@ -1,626 +0,0 @@
package tests
import (
"bytes"
"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"
"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"
)
// setupAuditTestDB creates a clean in-memory database for each test
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)
// Auto-migrate required models
err = db.AutoMigrate(
&models.User{},
&models.Setting{},
&models.ProxyHost{},
)
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",
}
require.NoError(t, admin.SetPassword("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)
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)
// Token MUST be at least 32 chars (64 hex = 32 bytes = 256 bits)
assert.GreaterOrEqual(t, len(token), 64, "Invite token must be at least 64 hex chars (256 bits)")
// Token must be hex
for _, c := range token {
assert.True(t, (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'), "Token must be hex encoded")
}
}
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,
InviteToken: "accepted-token-1234567890123456789012345678901",
InvitedAt: &invitedAt,
InviteStatus: "accepted",
}
require.NoError(t, user.SetPassword("somepassword"))
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",
}
require.NoError(t, user.SetPassword("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",
}
require.NoError(t, otherUser.SetPassword("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,
}
require.NoError(t, user.SetPassword("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")
}