add tests

This commit is contained in:
Fran Jurmanović
2025-07-06 19:19:42 +02:00
parent 26a0d33592
commit b9cb315944
8 changed files with 2918 additions and 0 deletions

View File

@@ -0,0 +1,373 @@
package integration
import (
"omega-server/local/api"
"omega-server/local/model"
"omega-server/local/repository"
"omega-server/local/service"
"omega-server/tests"
"testing"
"github.com/gofiber/fiber/v2"
)
func TestGraphQLIntegration(t *testing.T) {
// Initialize test suite
testSuite := tests.NewTestSuite()
testSuite.Setup(t)
defer testSuite.Teardown(t)
// Skip if no test database
testSuite.SkipIfNoTestDB(t)
// Create Fiber app
app := fiber.New(fiber.Config{
DisableStartupMessage: true,
})
// Initialize services and repositories
membershipRepo := repository.NewMembershipRepository(testSuite.DB)
membershipService := service.NewMembershipService(membershipRepo)
// Provide services to DI container
err := testSuite.DI.Provide(func() *service.MembershipService {
return membershipService
})
if err != nil {
t.Fatalf("Failed to provide membership service: %v", err)
}
// Initialize API routes
api.Init(testSuite.DI, app)
// Create integration test utils
integrationUtils := NewIntegrationTestUtils(app, testSuite.DB, membershipService)
t.Run("GraphQLSchemaEndpoint", func(t *testing.T) {
req := TestRequest{
Method: "GET",
URL: "/v1/graphql",
}
resp := integrationUtils.ExecuteRequest(t, req)
integrationUtils.AssertStatusCode(t, resp, 200)
integrationUtils.AssertResponseBody(t, resp, "type User")
integrationUtils.AssertResponseBody(t, resp, "type Query")
integrationUtils.AssertResponseBody(t, resp, "type Mutation")
})
t.Run("GraphQLLoginMutation", func(t *testing.T) {
// First create a user through the service
user := &model.User{
Email: "graphql@example.com",
FullName: "GraphQL User",
}
if err := user.SetPassword("password123"); err != nil {
t.Fatalf("Failed to set password: %v", err)
}
_, err := membershipService.CreateUser(
testSuite.TestContext(),
user,
[]string{"user"},
)
if err != nil {
t.Fatalf("Failed to create test user: %v", err)
}
// Test GraphQL login mutation
req := TestRequest{
Method: "POST",
URL: "/v1/graphql",
Body: map[string]interface{}{
"query": `
mutation Login($email: String!, $password: String!) {
login(input: {email: $email, password: $password}) {
token
user {
id
email
fullName
}
}
}
`,
"variables": map[string]interface{}{
"email": "graphql@example.com",
"password": "password123",
},
},
}
resp := integrationUtils.ExecuteRequest(t, req)
integrationUtils.AssertStatusCode(t, resp, 200)
// Parse response and check for token
data := integrationUtils.ParseJSONResponse(t, resp)
if errors, exists := data["errors"]; exists {
t.Fatalf("GraphQL returned errors: %v", errors)
}
loginData := integrationUtils.ExtractJSONField(t, resp, "data", "login")
if loginData == nil {
t.Fatal("Expected login data in response")
}
token := integrationUtils.ExtractStringField(t, resp, "data", "login", "token")
if token == "" {
t.Error("Expected non-empty token")
}
userEmail := integrationUtils.ExtractStringField(t, resp, "data", "login", "user", "email")
if userEmail != "graphql@example.com" {
t.Errorf("Expected email 'graphql@example.com', got '%s'", userEmail)
}
})
t.Run("GraphQLCreateUserMutation", func(t *testing.T) {
req := TestRequest{
Method: "POST",
URL: "/v1/graphql",
Body: map[string]interface{}{
"query": `
mutation CreateUser($email: String!, $password: String!, $fullName: String!) {
createUser(input: {email: $email, password: $password, fullName: $fullName}) {
id
email
fullName
createdAt
updatedAt
}
}
`,
"variables": map[string]interface{}{
"email": "newuser@example.com",
"password": "password123",
"fullName": "New User",
},
},
}
resp := integrationUtils.ExecuteRequest(t, req)
integrationUtils.AssertStatusCode(t, resp, 200)
// Parse response and verify user creation
data := integrationUtils.ParseJSONResponse(t, resp)
if errors, exists := data["errors"]; exists {
t.Fatalf("GraphQL returned errors: %v", errors)
}
userEmail := integrationUtils.ExtractStringField(t, resp, "data", "createUser", "email")
if userEmail != "newuser@example.com" {
t.Errorf("Expected email 'newuser@example.com', got '%s'", userEmail)
}
userFullName := integrationUtils.ExtractStringField(t, resp, "data", "createUser", "fullName")
if userFullName != "New User" {
t.Errorf("Expected full name 'New User', got '%s'", userFullName)
}
userID := integrationUtils.ExtractStringField(t, resp, "data", "createUser", "id")
if userID == "" {
t.Error("Expected non-empty user ID")
}
})
t.Run("GraphQLMeQuery", func(t *testing.T) {
req := TestRequest{
Method: "POST",
URL: "/v1/graphql",
Body: map[string]interface{}{
"query": `
query Me {
me {
id
email
fullName
createdAt
updatedAt
}
}
`,
},
}
resp := integrationUtils.ExecuteRequest(t, req)
integrationUtils.AssertStatusCode(t, resp, 200)
// Parse response
data := integrationUtils.ParseJSONResponse(t, resp)
if errors, exists := data["errors"]; exists {
t.Fatalf("GraphQL returned errors: %v", errors)
}
userEmail := integrationUtils.ExtractStringField(t, resp, "data", "me", "email")
if userEmail == "" {
t.Error("Expected non-empty email")
}
userID := integrationUtils.ExtractStringField(t, resp, "data", "me", "id")
if userID == "" {
t.Error("Expected non-empty user ID")
}
})
t.Run("GraphQLInvalidQuery", func(t *testing.T) {
req := TestRequest{
Method: "POST",
URL: "/v1/graphql",
Body: map[string]interface{}{
"query": "invalid query syntax {",
},
}
resp := integrationUtils.ExecuteRequest(t, req)
integrationUtils.AssertStatusCode(t, resp, 400)
// Should have errors in response
data := integrationUtils.ParseJSONResponse(t, resp)
if errors, exists := data["errors"]; !exists {
t.Fatal("Expected errors in response for invalid query")
} else {
errorList, ok := errors.([]interface{})
if !ok || len(errorList) == 0 {
t.Fatal("Expected non-empty error list")
}
}
})
t.Run("GraphQLUnsupportedQuery", func(t *testing.T) {
req := TestRequest{
Method: "POST",
URL: "/v1/graphql",
Body: map[string]interface{}{
"query": `
query UnsupportedQuery {
unsupportedField {
id
name
}
}
`,
},
}
resp := integrationUtils.ExecuteRequest(t, req)
integrationUtils.AssertStatusCode(t, resp, 400)
// Should return "Query not supported" error
data := integrationUtils.ParseJSONResponse(t, resp)
if errors, exists := data["errors"]; exists {
errorList, ok := errors.([]interface{})
if ok && len(errorList) > 0 {
if errorMap, ok := errorList[0].(map[string]interface{}); ok {
if message, ok := errorMap["message"].(string); ok {
if message != "Query not supported" {
t.Errorf("Expected 'Query not supported' error, got '%s'", message)
}
}
}
}
}
})
t.Run("GraphQLCreateProjectMutation", func(t *testing.T) {
req := TestRequest{
Method: "POST",
URL: "/v1/graphql",
Body: map[string]interface{}{
"query": `
mutation CreateProject($name: String!, $description: String, $ownerId: String!) {
createProject(input: {name: $name, description: $description, ownerId: $ownerId}) {
id
name
description
ownerId
createdAt
updatedAt
}
}
`,
"variables": map[string]interface{}{
"name": "Integration Test Project",
"description": "A project created during integration testing",
"ownerId": "test-owner-id",
},
},
}
resp := integrationUtils.ExecuteRequest(t, req)
integrationUtils.AssertStatusCode(t, resp, 200)
// Parse response (should work since it's mocked)
data := integrationUtils.ParseJSONResponse(t, resp)
if errors, exists := data["errors"]; exists {
t.Fatalf("GraphQL returned errors: %v", errors)
}
projectName := integrationUtils.ExtractStringField(t, resp, "data", "createProject", "name")
if projectName != "Integration Test Project" {
t.Errorf("Expected project name 'Integration Test Project', got '%s'", projectName)
}
projectID := integrationUtils.ExtractStringField(t, resp, "data", "createProject", "id")
if projectID == "" {
t.Error("Expected non-empty project ID")
}
})
t.Run("GraphQLCreateTaskMutation", func(t *testing.T) {
req := TestRequest{
Method: "POST",
URL: "/v1/graphql",
Body: map[string]interface{}{
"query": `
mutation CreateTask($title: String!, $description: String, $status: String, $priority: String, $projectId: String!) {
createTask(input: {title: $title, description: $description, status: $status, priority: $priority, projectId: $projectId}) {
id
title
description
status
priority
projectId
createdAt
updatedAt
}
}
`,
"variables": map[string]interface{}{
"title": "Integration Test Task",
"description": "A task created during integration testing",
"status": "todo",
"priority": "medium",
"projectId": "test-project-id",
},
},
}
resp := integrationUtils.ExecuteRequest(t, req)
integrationUtils.AssertStatusCode(t, resp, 200)
// Parse response (should work since it's mocked)
data := integrationUtils.ParseJSONResponse(t, resp)
if errors, exists := data["errors"]; exists {
t.Fatalf("GraphQL returned errors: %v", errors)
}
taskTitle := integrationUtils.ExtractStringField(t, resp, "data", "createTask", "title")
if taskTitle != "Integration Test Task" {
t.Errorf("Expected task title 'Integration Test Task', got '%s'", taskTitle)
}
taskStatus := integrationUtils.ExtractStringField(t, resp, "data", "createTask", "status")
if taskStatus != "todo" {
t.Errorf("Expected task status 'todo', got '%s'", taskStatus)
}
taskID := integrationUtils.ExtractStringField(t, resp, "data", "createTask", "id")
if taskID == "" {
t.Error("Expected non-empty task ID")
}
})
}

View File

@@ -0,0 +1,463 @@
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
}