2025-12-30 08:00:04 +01:00
|
|
|
package user
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"bamort/database"
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// setupRoleTestEnvironment sets up the test environment for role tests
|
|
|
|
|
func setupRoleTestEnvironment(t *testing.T) {
|
|
|
|
|
setupTestEnvironment(t)
|
|
|
|
|
database.SetupTestDB()
|
|
|
|
|
err := MigrateStructure()
|
|
|
|
|
require.NoError(t, err, "Should migrate user structure")
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestUserRoleDefaults tests that new users get standard role by default
|
|
|
|
|
func TestUserRoleDefaults(t *testing.T) {
|
|
|
|
|
setupRoleTestEnvironment(t)
|
|
|
|
|
|
|
|
|
|
user := &User{
|
|
|
|
|
Username: "roletest_user",
|
|
|
|
|
PasswordHash: "hashedpw",
|
|
|
|
|
Email: "roletest@example.com",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := user.Create()
|
|
|
|
|
require.NoError(t, err, "Should create user")
|
|
|
|
|
assert.Equal(t, RoleStandardUser, user.Role, "New users should have standard role")
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:21:43 +01:00
|
|
|
// TestUserDisplayNameDefaultsToUsername ensures a user's display name falls back to the username
|
|
|
|
|
func TestUserDisplayNameDefaultsToUsername(t *testing.T) {
|
|
|
|
|
setupRoleTestEnvironment(t)
|
|
|
|
|
|
|
|
|
|
user := &User{
|
|
|
|
|
Username: "display_user",
|
|
|
|
|
PasswordHash: "hashedpw",
|
|
|
|
|
Email: "display@example.com",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := user.Create()
|
|
|
|
|
require.NoError(t, err, "Should create user")
|
|
|
|
|
assert.Equal(t, user.Username, user.DisplayName, "DisplayName should default to username")
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-30 08:00:04 +01:00
|
|
|
// TestRoleValidation tests role validation
|
|
|
|
|
func TestRoleValidation(t *testing.T) {
|
|
|
|
|
assert.True(t, ValidateRole(RoleStandardUser), "standard should be valid")
|
|
|
|
|
assert.True(t, ValidateRole(RoleMaintainer), "maintainer should be valid")
|
|
|
|
|
assert.True(t, ValidateRole(RoleAdmin), "admin should be valid")
|
|
|
|
|
assert.False(t, ValidateRole("invalid"), "invalid should not be valid")
|
|
|
|
|
assert.False(t, ValidateRole(""), "empty should not be valid")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestRoleHierarchy tests role hierarchy methods
|
|
|
|
|
func TestRoleHierarchy(t *testing.T) {
|
|
|
|
|
standardUser := &User{Role: RoleStandardUser}
|
|
|
|
|
maintainer := &User{Role: RoleMaintainer}
|
|
|
|
|
admin := &User{Role: RoleAdmin}
|
|
|
|
|
|
|
|
|
|
// Test IsStandardUser
|
|
|
|
|
assert.True(t, standardUser.IsStandardUser(), "standard user should pass IsStandardUser")
|
|
|
|
|
assert.True(t, maintainer.IsStandardUser(), "maintainer should pass IsStandardUser")
|
|
|
|
|
assert.True(t, admin.IsStandardUser(), "admin should pass IsStandardUser")
|
|
|
|
|
|
|
|
|
|
// Test IsMaintainer
|
|
|
|
|
assert.False(t, standardUser.IsMaintainer(), "standard user should not pass IsMaintainer")
|
|
|
|
|
assert.True(t, maintainer.IsMaintainer(), "maintainer should pass IsMaintainer")
|
|
|
|
|
assert.True(t, admin.IsMaintainer(), "admin should pass IsMaintainer")
|
|
|
|
|
|
|
|
|
|
// Test IsAdmin
|
|
|
|
|
assert.False(t, standardUser.IsAdmin(), "standard user should not pass IsAdmin")
|
|
|
|
|
assert.False(t, maintainer.IsAdmin(), "maintainer should not pass IsAdmin")
|
|
|
|
|
assert.True(t, admin.IsAdmin(), "admin should pass IsAdmin")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestListUsers tests listing all users (admin only)
|
|
|
|
|
func TestListUsers(t *testing.T) {
|
|
|
|
|
setupRoleTestEnvironment(t)
|
|
|
|
|
|
|
|
|
|
// Create test users
|
2026-02-03 17:21:43 +01:00
|
|
|
admin := &User{Username: "listadmin", PasswordHash: "hash", Email: "listadmin@test.com", Role: RoleAdmin, DisplayName: "List Admin"}
|
2025-12-30 08:00:04 +01:00
|
|
|
require.NoError(t, admin.Create())
|
|
|
|
|
|
|
|
|
|
standardUser := &User{Username: "listuser", PasswordHash: "hash", Email: "listuser@test.com", Role: RoleStandardUser}
|
|
|
|
|
require.NoError(t, standardUser.Create())
|
|
|
|
|
|
|
|
|
|
// Test admin access
|
|
|
|
|
router := gin.Default()
|
|
|
|
|
router.GET("/users", func(c *gin.Context) {
|
|
|
|
|
c.Set("user", admin)
|
|
|
|
|
}, RequireAdmin(), ListUsers)
|
|
|
|
|
|
|
|
|
|
req, _ := http.NewRequest("GET", "/users", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code, "Admin should access list")
|
|
|
|
|
|
2026-02-03 17:21:43 +01:00
|
|
|
var response []map[string]interface{}
|
|
|
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
var adminEntry map[string]interface{}
|
|
|
|
|
var standardEntry map[string]interface{}
|
|
|
|
|
for _, entry := range response {
|
|
|
|
|
switch entry["username"] {
|
|
|
|
|
case admin.Username:
|
|
|
|
|
adminEntry = entry
|
|
|
|
|
case standardUser.Username:
|
|
|
|
|
standardEntry = entry
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
require.NotNil(t, adminEntry, "admin should be present in response")
|
|
|
|
|
require.NotNil(t, standardEntry, "standard user should be present in response")
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, admin.DisplayName, adminEntry["display_name"], "admin display name should be returned")
|
|
|
|
|
assert.Equal(t, standardUser.Username, standardEntry["display_name"], "standard user should fall back to username when display name is empty")
|
|
|
|
|
|
2025-12-30 08:00:04 +01:00
|
|
|
// Test standard user access (should fail)
|
|
|
|
|
router2 := gin.Default()
|
|
|
|
|
router2.GET("/users", func(c *gin.Context) {
|
|
|
|
|
c.Set("user", standardUser)
|
|
|
|
|
}, RequireAdmin(), ListUsers)
|
|
|
|
|
|
|
|
|
|
req2, _ := http.NewRequest("GET", "/users", nil)
|
|
|
|
|
w2 := httptest.NewRecorder()
|
|
|
|
|
router2.ServeHTTP(w2, req2)
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, http.StatusForbidden, w2.Code, "Standard user should not access list")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestUpdateUserRole tests updating user role (admin only)
|
|
|
|
|
func TestUpdateUserRole(t *testing.T) {
|
|
|
|
|
setupRoleTestEnvironment(t)
|
|
|
|
|
|
|
|
|
|
// Create admin and target user
|
|
|
|
|
admin := &User{Username: "roleadmin", PasswordHash: "hash", Email: "roleadmin@test.com", Role: RoleAdmin}
|
|
|
|
|
require.NoError(t, admin.Create())
|
|
|
|
|
|
|
|
|
|
targetUser := &User{Username: "roletarget", PasswordHash: "hash", Email: "roletarget@test.com", Role: RoleStandardUser}
|
|
|
|
|
require.NoError(t, targetUser.Create())
|
|
|
|
|
|
|
|
|
|
// Test valid role update
|
|
|
|
|
router := gin.Default()
|
|
|
|
|
router.PUT("/users/:id/role", func(c *gin.Context) {
|
|
|
|
|
c.Set("user", admin)
|
|
|
|
|
}, RequireAdmin(), UpdateUserRole)
|
|
|
|
|
|
|
|
|
|
updateData := map[string]string{"role": RoleMaintainer}
|
|
|
|
|
jsonData, _ := json.Marshal(updateData)
|
|
|
|
|
|
|
|
|
|
req, _ := http.NewRequest("PUT", fmt.Sprintf("/users/%d/role", targetUser.UserID), bytes.NewBuffer(jsonData))
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code, "Admin should update role")
|
|
|
|
|
|
|
|
|
|
// Verify role was updated
|
|
|
|
|
var updatedUser User
|
|
|
|
|
require.NoError(t, updatedUser.FirstId(targetUser.UserID))
|
|
|
|
|
assert.Equal(t, RoleMaintainer, updatedUser.Role, "Role should be updated to maintainer")
|
|
|
|
|
|
|
|
|
|
// Test invalid role update
|
|
|
|
|
invalidData := map[string]string{"role": "invalid"}
|
|
|
|
|
jsonData2, _ := json.Marshal(invalidData)
|
|
|
|
|
|
|
|
|
|
req2, _ := http.NewRequest("PUT", fmt.Sprintf("/users/%d/role", targetUser.UserID), bytes.NewBuffer(jsonData2))
|
|
|
|
|
req2.Header.Set("Content-Type", "application/json")
|
|
|
|
|
w2 := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w2, req2)
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w2.Code, "Should reject invalid role")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestDeleteUser tests deleting a user (admin only)
|
|
|
|
|
func TestDeleteUser(t *testing.T) {
|
|
|
|
|
setupRoleTestEnvironment(t)
|
|
|
|
|
|
|
|
|
|
// Create admin and target user
|
|
|
|
|
admin := &User{Username: "deladmin", PasswordHash: "hash", Email: "deladmin@test.com", Role: RoleAdmin}
|
|
|
|
|
require.NoError(t, admin.Create())
|
|
|
|
|
|
|
|
|
|
targetUser := &User{Username: "deltarget", PasswordHash: "hash", Email: "deltarget@test.com", Role: RoleStandardUser}
|
|
|
|
|
require.NoError(t, targetUser.Create())
|
|
|
|
|
|
|
|
|
|
// Test deletion
|
|
|
|
|
router := gin.Default()
|
|
|
|
|
router.DELETE("/users/:id", func(c *gin.Context) {
|
|
|
|
|
c.Set("user", admin)
|
|
|
|
|
}, RequireAdmin(), DeleteUser)
|
|
|
|
|
|
|
|
|
|
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/users/%d", targetUser.UserID), nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code, "Admin should delete user")
|
|
|
|
|
|
|
|
|
|
// Verify user was deleted
|
|
|
|
|
var deletedUser User
|
|
|
|
|
err := deletedUser.FirstId(targetUser.UserID)
|
|
|
|
|
assert.Error(t, err, "User should not exist after deletion")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestMaintainerPermissions tests maintainer-specific permissions
|
|
|
|
|
func TestMaintainerPermissions(t *testing.T) {
|
|
|
|
|
setupRoleTestEnvironment(t)
|
|
|
|
|
|
|
|
|
|
standardUser := &User{Username: "permuser", PasswordHash: "hash", Email: "permuser@test.com", Role: RoleStandardUser}
|
|
|
|
|
require.NoError(t, standardUser.Create())
|
|
|
|
|
|
|
|
|
|
maintainer := &User{Username: "permmaintainer", PasswordHash: "hash", Email: "permmaintainer@test.com", Role: RoleMaintainer}
|
|
|
|
|
require.NoError(t, maintainer.Create())
|
|
|
|
|
|
|
|
|
|
admin := &User{Username: "permadmin", PasswordHash: "hash", Email: "permadmin@test.com", Role: RoleAdmin}
|
|
|
|
|
require.NoError(t, admin.Create())
|
|
|
|
|
|
|
|
|
|
router := gin.Default()
|
|
|
|
|
router.GET("/maintainer-only", func(c *gin.Context) {
|
|
|
|
|
c.Set("user", standardUser)
|
|
|
|
|
}, RequireMaintainer(), func(c *gin.Context) {
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Standard user should fail
|
|
|
|
|
req, _ := http.NewRequest("GET", "/maintainer-only", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code, "Standard user should not access")
|
|
|
|
|
|
|
|
|
|
// Maintainer should succeed
|
|
|
|
|
router2 := gin.Default()
|
|
|
|
|
router2.GET("/maintainer-only", func(c *gin.Context) {
|
|
|
|
|
c.Set("user", maintainer)
|
|
|
|
|
}, RequireMaintainer(), func(c *gin.Context) {
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
req2, _ := http.NewRequest("GET", "/maintainer-only", nil)
|
|
|
|
|
w2 := httptest.NewRecorder()
|
|
|
|
|
router2.ServeHTTP(w2, req2)
|
|
|
|
|
assert.Equal(t, http.StatusOK, w2.Code, "Maintainer should access")
|
|
|
|
|
|
|
|
|
|
// Admin should also succeed
|
|
|
|
|
router3 := gin.Default()
|
|
|
|
|
router3.GET("/maintainer-only", func(c *gin.Context) {
|
|
|
|
|
c.Set("user", admin)
|
|
|
|
|
}, RequireMaintainer(), func(c *gin.Context) {
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
req3, _ := http.NewRequest("GET", "/maintainer-only", nil)
|
|
|
|
|
w3 := httptest.NewRecorder()
|
|
|
|
|
router3.ServeHTTP(w3, req3)
|
|
|
|
|
assert.Equal(t, http.StatusOK, w3.Code, "Admin should access maintainer endpoints")
|
|
|
|
|
}
|