464 lines
13 KiB
Go
464 lines
13 KiB
Go
package integration
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"omega-server/local/model"
|
|
"omega-server/local/service"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// IntegrationTestUtils provides utilities for integration testing
|
|
type IntegrationTestUtils struct {
|
|
App *fiber.App
|
|
DB *gorm.DB
|
|
MembershipService *service.MembershipService
|
|
}
|
|
|
|
// TestRequest represents an HTTP test request
|
|
type TestRequest struct {
|
|
Method string
|
|
URL string
|
|
Body interface{}
|
|
Headers map[string]string
|
|
}
|
|
|
|
// TestResponse represents an HTTP test response
|
|
type TestResponse struct {
|
|
StatusCode int
|
|
Body string
|
|
Headers map[string]string
|
|
}
|
|
|
|
// NewIntegrationTestUtils creates a new integration test utilities instance
|
|
func NewIntegrationTestUtils(app *fiber.App, db *gorm.DB, membershipService *service.MembershipService) *IntegrationTestUtils {
|
|
return &IntegrationTestUtils{
|
|
App: app,
|
|
DB: db,
|
|
MembershipService: membershipService,
|
|
}
|
|
}
|
|
|
|
// ExecuteRequest executes an HTTP request and returns the response
|
|
func (itu *IntegrationTestUtils) ExecuteRequest(t *testing.T, req TestRequest) *TestResponse {
|
|
var body []byte
|
|
var err error
|
|
|
|
if req.Body != nil {
|
|
body, err = json.Marshal(req.Body)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal request body: %v", err)
|
|
}
|
|
}
|
|
|
|
httpReq := httptest.NewRequest(req.Method, req.URL, bytes.NewBuffer(body))
|
|
|
|
// Set default headers
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
|
|
// Set custom headers
|
|
for key, value := range req.Headers {
|
|
httpReq.Header.Set(key, value)
|
|
}
|
|
|
|
resp, err := itu.App.Test(httpReq)
|
|
if err != nil {
|
|
t.Fatalf("Failed to execute request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read response body
|
|
buf := new(bytes.Buffer)
|
|
buf.ReadFrom(resp.Body)
|
|
responseBody := buf.String()
|
|
|
|
// Extract response headers
|
|
responseHeaders := make(map[string]string)
|
|
for key, values := range resp.Header {
|
|
if len(values) > 0 {
|
|
responseHeaders[key] = values[0]
|
|
}
|
|
}
|
|
|
|
return &TestResponse{
|
|
StatusCode: resp.StatusCode,
|
|
Body: responseBody,
|
|
Headers: responseHeaders,
|
|
}
|
|
}
|
|
|
|
// ExecuteRequestWithAuth executes an HTTP request with authentication
|
|
func (itu *IntegrationTestUtils) ExecuteRequestWithAuth(t *testing.T, req TestRequest, token string) *TestResponse {
|
|
if req.Headers == nil {
|
|
req.Headers = make(map[string]string)
|
|
}
|
|
req.Headers["Authorization"] = "Bearer " + token
|
|
|
|
return itu.ExecuteRequest(t, req)
|
|
}
|
|
|
|
// LoginUser logs in a user and returns the auth token
|
|
func (itu *IntegrationTestUtils) LoginUser(t *testing.T, email, password string) string {
|
|
loginReq := TestRequest{
|
|
Method: "POST",
|
|
URL: "/v1/auth/login",
|
|
Body: map[string]interface{}{
|
|
"email": email,
|
|
"password": password,
|
|
},
|
|
}
|
|
|
|
resp := itu.ExecuteRequest(t, loginReq)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("Failed to login user: status %d, body: %s", resp.StatusCode, resp.Body)
|
|
}
|
|
|
|
var loginResp map[string]interface{}
|
|
if err := json.Unmarshal([]byte(resp.Body), &loginResp); err != nil {
|
|
t.Fatalf("Failed to unmarshal login response: %v", err)
|
|
}
|
|
|
|
token, ok := loginResp["token"].(string)
|
|
if !ok {
|
|
t.Fatalf("Failed to extract token from login response: %+v", loginResp)
|
|
}
|
|
|
|
return token
|
|
}
|
|
|
|
// CreateTestUserWithAuth creates a test user and returns auth token
|
|
func (itu *IntegrationTestUtils) CreateTestUserWithAuth(t *testing.T, email, fullName, password string) (string, *model.User) {
|
|
// Create domain model
|
|
user := &model.User{
|
|
Email: email,
|
|
FullName: fullName,
|
|
}
|
|
|
|
if err := user.SetPassword(password); err != nil {
|
|
t.Fatalf("Failed to set password: %v", err)
|
|
}
|
|
|
|
// Create user
|
|
createdUser, err := itu.MembershipService.CreateUser(context.Background(), user, []string{"user"})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test user: %v", err)
|
|
}
|
|
|
|
// Login to get token
|
|
token := itu.LoginUser(t, email, password)
|
|
|
|
return token, createdUser
|
|
}
|
|
|
|
// CreateTestAdmin creates a test admin user and returns auth token
|
|
func (itu *IntegrationTestUtils) CreateTestAdmin(t *testing.T, email, fullName, password string) (string, *model.User) {
|
|
// Create domain model
|
|
user := &model.User{
|
|
Email: email,
|
|
FullName: fullName,
|
|
}
|
|
|
|
if err := user.SetPassword(password); err != nil {
|
|
t.Fatalf("Failed to set password: %v", err)
|
|
}
|
|
|
|
// Create admin user
|
|
createdUser, err := itu.MembershipService.CreateUser(context.Background(), user, []string{"admin"})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test admin: %v", err)
|
|
}
|
|
|
|
// Login to get token
|
|
token := itu.LoginUser(t, email, password)
|
|
|
|
return token, createdUser
|
|
}
|
|
|
|
// AssertStatusCode asserts that the response has the expected status code
|
|
func (itu *IntegrationTestUtils) AssertStatusCode(t *testing.T, resp *TestResponse, expectedStatusCode int) {
|
|
if resp.StatusCode != expectedStatusCode {
|
|
t.Errorf("Expected status code %d, got %d. Response body: %s", expectedStatusCode, resp.StatusCode, resp.Body)
|
|
}
|
|
}
|
|
|
|
// AssertResponseBody asserts that the response body contains expected content
|
|
func (itu *IntegrationTestUtils) AssertResponseBody(t *testing.T, resp *TestResponse, expectedContent string) {
|
|
if !contains(resp.Body, expectedContent) {
|
|
t.Errorf("Expected response body to contain '%s', but got: %s", expectedContent, resp.Body)
|
|
}
|
|
}
|
|
|
|
// AssertResponseNotContains asserts that the response body does not contain specific content
|
|
func (itu *IntegrationTestUtils) AssertResponseNotContains(t *testing.T, resp *TestResponse, content string) {
|
|
if contains(resp.Body, content) {
|
|
t.Errorf("Expected response body to not contain '%s', but it did: %s", content, resp.Body)
|
|
}
|
|
}
|
|
|
|
// AssertResponseJSON asserts that the response body is valid JSON and matches expected structure
|
|
func (itu *IntegrationTestUtils) AssertResponseJSON(t *testing.T, resp *TestResponse, expectedJSON interface{}) {
|
|
var actualJSON interface{}
|
|
if err := json.Unmarshal([]byte(resp.Body), &actualJSON); err != nil {
|
|
t.Fatalf("Failed to unmarshal response body as JSON: %v. Body: %s", err, resp.Body)
|
|
}
|
|
|
|
expectedBytes, err := json.Marshal(expectedJSON)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal expected JSON: %v", err)
|
|
}
|
|
|
|
actualBytes, err := json.Marshal(actualJSON)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal actual JSON: %v", err)
|
|
}
|
|
|
|
if string(expectedBytes) != string(actualBytes) {
|
|
t.Errorf("Expected JSON: %s, got: %s", string(expectedBytes), string(actualBytes))
|
|
}
|
|
}
|
|
|
|
// AssertHeader asserts that the response has a specific header with expected value
|
|
func (itu *IntegrationTestUtils) AssertHeader(t *testing.T, resp *TestResponse, headerName, expectedValue string) {
|
|
actualValue, exists := resp.Headers[headerName]
|
|
if !exists {
|
|
t.Errorf("Expected header '%s' to exist, but it doesn't", headerName)
|
|
return
|
|
}
|
|
|
|
if actualValue != expectedValue {
|
|
t.Errorf("Expected header '%s' to have value '%s', got '%s'", headerName, expectedValue, actualValue)
|
|
}
|
|
}
|
|
|
|
// ParseJSONResponse parses the response body as JSON
|
|
func (itu *IntegrationTestUtils) ParseJSONResponse(t *testing.T, resp *TestResponse) map[string]interface{} {
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal([]byte(resp.Body), &result); err != nil {
|
|
t.Fatalf("Failed to parse response body as JSON: %v. Body: %s", err, resp.Body)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ExtractJSONField extracts a field from JSON response
|
|
func (itu *IntegrationTestUtils) ExtractJSONField(t *testing.T, resp *TestResponse, fieldPath ...string) interface{} {
|
|
data := itu.ParseJSONResponse(t, resp)
|
|
|
|
var result interface{} = data
|
|
for _, field := range fieldPath {
|
|
if dataMap, ok := result.(map[string]interface{}); ok {
|
|
if value, exists := dataMap[field]; exists {
|
|
result = value
|
|
} else {
|
|
t.Fatalf("Field '%s' not found in JSON response: %+v", field, dataMap)
|
|
}
|
|
} else {
|
|
t.Fatalf("Cannot extract field '%s' from non-map data: %+v", field, result)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// ExtractStringField extracts a string field from JSON response
|
|
func (itu *IntegrationTestUtils) ExtractStringField(t *testing.T, resp *TestResponse, fieldPath ...string) string {
|
|
value := itu.ExtractJSONField(t, resp, fieldPath...)
|
|
if str, ok := value.(string); ok {
|
|
return str
|
|
}
|
|
t.Fatalf("Expected string value for field %v, but got: %+v", fieldPath, value)
|
|
return ""
|
|
}
|
|
|
|
// ExtractIntField extracts an integer field from JSON response
|
|
func (itu *IntegrationTestUtils) ExtractIntField(t *testing.T, resp *TestResponse, fieldPath ...string) int {
|
|
value := itu.ExtractJSONField(t, resp, fieldPath...)
|
|
if floatVal, ok := value.(float64); ok {
|
|
return int(floatVal)
|
|
}
|
|
if intVal, ok := value.(int); ok {
|
|
return intVal
|
|
}
|
|
t.Fatalf("Expected int value for field %v, but got: %+v", fieldPath, value)
|
|
return 0
|
|
}
|
|
|
|
// ExtractBoolField extracts a boolean field from JSON response
|
|
func (itu *IntegrationTestUtils) ExtractBoolField(t *testing.T, resp *TestResponse, fieldPath ...string) bool {
|
|
value := itu.ExtractJSONField(t, resp, fieldPath...)
|
|
if boolVal, ok := value.(bool); ok {
|
|
return boolVal
|
|
}
|
|
t.Fatalf("Expected bool value for field %v, but got: %+v", fieldPath, value)
|
|
return false
|
|
}
|
|
|
|
// WaitForCondition waits for a condition to be true with timeout
|
|
func (itu *IntegrationTestUtils) WaitForCondition(t *testing.T, condition func() bool, timeout time.Duration, message string) {
|
|
start := time.Now()
|
|
for time.Since(start) < timeout {
|
|
if condition() {
|
|
return
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
t.Fatalf("Condition not met within timeout: %s", message)
|
|
}
|
|
|
|
// CleanupDatabase cleans up test data from database
|
|
func (itu *IntegrationTestUtils) CleanupDatabase(t *testing.T) {
|
|
// Clean up in reverse order of dependencies
|
|
tables := []string{
|
|
"task_assignees",
|
|
"project_members",
|
|
"integrations",
|
|
"tasks",
|
|
"projects",
|
|
"types",
|
|
"user_roles",
|
|
"role_permissions",
|
|
"users",
|
|
"roles",
|
|
"permissions",
|
|
"system_configs",
|
|
"audit_logs",
|
|
"security_events",
|
|
}
|
|
|
|
for _, table := range tables {
|
|
err := itu.DB.Exec(fmt.Sprintf("DELETE FROM %s", table)).Error
|
|
if err != nil {
|
|
t.Logf("Warning: Failed to clean table %s: %v", table, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SeedTestData seeds the database with test data
|
|
func (itu *IntegrationTestUtils) SeedTestData(t *testing.T) {
|
|
// Create test roles
|
|
itu.CreateTestRole(t, "admin", "Administrator role")
|
|
itu.CreateTestRole(t, "user", "Regular user role")
|
|
|
|
// Create test permissions
|
|
itu.CreateTestPermission(t, "user:read", "Read user data", "user")
|
|
itu.CreateTestPermission(t, "user:write", "Write user data", "user")
|
|
itu.CreateTestPermission(t, "project:read", "Read project data", "project")
|
|
itu.CreateTestPermission(t, "project:write", "Write project data", "project")
|
|
|
|
// Create test project types
|
|
itu.CreateTestType(t, "Web Development", "Standard web development project", nil)
|
|
itu.CreateTestType(t, "Mobile App", "Mobile application development", nil)
|
|
}
|
|
|
|
// CreateTestRole creates a test role in the database
|
|
func (itu *IntegrationTestUtils) CreateTestRole(t *testing.T, name, description string) *model.Role {
|
|
role := &model.Role{
|
|
Name: name,
|
|
Description: description,
|
|
Active: true,
|
|
}
|
|
role.Init()
|
|
|
|
err := itu.DB.Create(role).Error
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test role: %v", err)
|
|
}
|
|
|
|
return role
|
|
}
|
|
|
|
// CreateTestPermission creates a test permission in the database
|
|
func (itu *IntegrationTestUtils) CreateTestPermission(t *testing.T, name, description, category string) *model.Permission {
|
|
permission := &model.Permission{
|
|
Name: name,
|
|
Description: description,
|
|
Category: category,
|
|
Active: true,
|
|
}
|
|
permission.Init()
|
|
|
|
err := itu.DB.Create(permission).Error
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test permission: %v", err)
|
|
}
|
|
|
|
return permission
|
|
}
|
|
|
|
// CreateTestType creates a test project type in the database
|
|
func (itu *IntegrationTestUtils) CreateTestType(t *testing.T, name, description string, userID *string) *model.Type {
|
|
projectType := &model.Type{
|
|
Name: name,
|
|
Description: description,
|
|
UserID: userID,
|
|
}
|
|
projectType.Init()
|
|
|
|
err := itu.DB.Create(projectType).Error
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test type: %v", err)
|
|
}
|
|
|
|
return projectType
|
|
}
|
|
|
|
// CreateTestProject creates a test project in the database
|
|
func (itu *IntegrationTestUtils) CreateTestProject(t *testing.T, name, description, ownerID, typeID string) *model.Project {
|
|
project := &model.Project{
|
|
Name: name,
|
|
Description: description,
|
|
OwnerID: ownerID,
|
|
TypeID: typeID,
|
|
}
|
|
project.Init()
|
|
|
|
err := itu.DB.Create(project).Error
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test project: %v", err)
|
|
}
|
|
|
|
return project
|
|
}
|
|
|
|
// CreateTestTask creates a test task in the database
|
|
func (itu *IntegrationTestUtils) CreateTestTask(t *testing.T, title, description, projectID string) *model.Task {
|
|
task := &model.Task{
|
|
Title: title,
|
|
Description: description,
|
|
Status: model.TaskStatusTodo,
|
|
Priority: model.TaskPriorityMedium,
|
|
ProjectID: projectID,
|
|
}
|
|
task.Init()
|
|
|
|
err := itu.DB.Create(task).Error
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test task: %v", err)
|
|
}
|
|
|
|
return task
|
|
}
|
|
|
|
// contains checks if a string contains a substring
|
|
func contains(str, substr string) bool {
|
|
return len(str) >= len(substr) && (str == substr || len(substr) == 0 ||
|
|
(len(substr) > 0 && findSubstring(str, substr)))
|
|
}
|
|
|
|
// findSubstring finds if substr exists in str
|
|
func findSubstring(str, substr string) bool {
|
|
for i := 0; i <= len(str)-len(substr); i++ {
|
|
if str[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|