From b9cb315944b056df2e44bd117d2128227287ec2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Sun, 6 Jul 2025 19:19:42 +0200 Subject: [PATCH] add tests --- tests/README.md | 369 ++++++++++++++ tests/graphql/basic_test.go | 276 +++++++++++ tests/graphql/graphql_test_utils.go | 427 ++++++++++++++++ tests/integration/graphql_integration_test.go | 373 ++++++++++++++ tests/integration/integration_test_utils.go | 463 ++++++++++++++++++ tests/testing.go | 362 ++++++++++++++ tests/unit/graphql_handler_test.go | 272 ++++++++++ tests/unit/unit_test_utils.go | 376 ++++++++++++++ 8 files changed, 2918 insertions(+) create mode 100644 tests/README.md create mode 100644 tests/graphql/basic_test.go create mode 100644 tests/graphql/graphql_test_utils.go create mode 100644 tests/integration/graphql_integration_test.go create mode 100644 tests/integration/integration_test_utils.go create mode 100644 tests/testing.go create mode 100644 tests/unit/graphql_handler_test.go create mode 100644 tests/unit/unit_test_utils.go diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..be9d29c --- /dev/null +++ b/tests/README.md @@ -0,0 +1,369 @@ +# Testing Module + +This directory contains the comprehensive testing framework for the Omega server application. The testing module is organized to support unit tests, integration tests, GraphQL tests, and provides utilities for creating test fixtures and managing test data. + +## Directory Structure + +``` +tests/ +├── README.md # This file +├── testing.go # Main testing utilities and test suite +├── fixtures/ # Test data fixtures +│ └── fixtures.go # Predefined test data +├── unit/ # Unit testing utilities +│ └── unit_test_utils.go # Unit test helpers and assertions +├── integration/ # Integration testing utilities +│ └── integration_test_utils.go # HTTP request testing and API integration +└── graphql/ # GraphQL testing utilities + └── graphql_test_utils.go # GraphQL query execution and testing +``` + +## Getting Started + +### Prerequisites + +Before running tests, ensure you have: + +1. **PostgreSQL Test Database**: Set up a dedicated test database +2. **Environment Variables**: Configure test-specific environment variables +3. **Go Testing Tools**: Standard Go testing framework + +### Environment Configuration + +Set the following environment variables for testing: + +```bash +# Test Database Configuration +TEST_DB_HOST=localhost +TEST_DB_PORT=5432 +TEST_DB_USER=postgres +TEST_DB_PASSWORD=password +TEST_DB_NAME=omega_test +TEST_DB_SSL_MODE=disable + +# Optional: Test-specific configurations +LOG_LEVEL=DEBUG +DEFAULT_ADMIN_PASSWORD=testpassword123 +``` + +### Running Tests + +```bash +# Run all tests +go test ./... + +# Run tests with verbose output +go test -v ./... + +# Run specific test package +go test ./tests/unit/... +go test ./tests/integration/... +go test ./tests/graphql/... + +# Run tests with coverage +go test -cover ./... + +# Run tests with detailed coverage report +go test -coverprofile=coverage.out ./... +go tool cover -html=coverage.out +``` + +## Test Categories + +### 1. Unit Tests (`tests/unit/`) + +Unit tests focus on testing individual components in isolation: + +- **Model validation**: Testing model methods and validation logic +- **Service layer**: Testing business logic without external dependencies +- **Utility functions**: Testing helper functions and utilities +- **Repository layer**: Testing data access logic with mocked dependencies + +**Example Unit Test Structure:** +```go +func TestUserValidation(t *testing.T) { + utils := unit.NewUnitTestUtils(nil) + + user := utils.MockUser("test-id", "test@example.com", "Test User") + + err := user.Validate() + utils.AssertNoError(t, err) + utils.AssertStringEqual(t, "test@example.com", user.Email) +} +``` + +### 2. Integration Tests (`tests/integration/`) + +Integration tests verify the interaction between different components: + +- **API endpoints**: Testing complete HTTP request/response cycles +- **Database operations**: Testing real database interactions +- **Authentication flows**: Testing JWT token generation and validation +- **Business workflows**: Testing complete user scenarios + +**Example Integration Test Structure:** +```go +func TestCreateUserAPI(t *testing.T) { + testSuite := tests.NewTestSuite() + testSuite.Setup(t) + defer testSuite.Teardown(t) + + utils := integration.NewIntegrationTestUtils(app, testSuite.DB, membershipService) + + req := integration.TestRequest{ + Method: "POST", + URL: "/v1/users", + Body: map[string]interface{}{ + "email": "new@example.com", + "fullName": "New User", + "password": "password123", + }, + } + + resp := utils.ExecuteRequest(t, req) + utils.AssertStatusCode(t, resp, 201) +} +``` + +### 3. GraphQL Tests (`tests/graphql/`) + +GraphQL tests specifically target the GraphQL API layer: + +- **Query execution**: Testing GraphQL queries and mutations +- **Schema validation**: Ensuring GraphQL schema correctness +- **Error handling**: Testing GraphQL error responses +- **Authentication**: Testing GraphQL with JWT authentication + +**Example GraphQL Test Structure:** +```go +func TestGraphQLLogin(t *testing.T) { + testSuite := tests.NewTestSuite() + testSuite.Setup(t) + defer testSuite.Teardown(t) + + utils := graphql.NewGraphQLTestUtils(testSuite.DB, membershipService) + + response := utils.ExecuteLoginMutation(t, "admin@example.com", "password123") + + utils.AssertNoErrors(t, response) + utils.AssertDataNotNil(t, response) + + token := utils.ExtractString(t, response, "login", "token") + utils.AssertNotEmpty(t, token) +} +``` + +## Test Utilities + +### TestSuite (`testing.go`) + +The main test suite provides: + +- **Database Setup**: Automatic test database configuration +- **Migration Management**: Running migrations for tests +- **Cleanup**: Automatic cleanup after tests +- **Dependency Injection**: DI container setup for tests +- **Helper Methods**: Common test operations + +```go +testSuite := tests.NewTestSuite() +testSuite.Setup(t) +defer testSuite.Teardown(t) + +// Create test data +user := testSuite.CreateTestUser(t, "test@example.com", "Test User") +project := testSuite.CreateTestProject(t, "Test Project", "Description", user.ID, typeID) +``` + +### Fixtures (`fixtures/`) + +Predefined test data for consistent testing: + +- **User Fixtures**: Standard test users with different roles +- **Project Fixtures**: Sample projects with various configurations +- **Task Fixtures**: Tasks in different states and priorities +- **Integration Fixtures**: Sample third-party integrations +- **GraphQL Queries**: Common GraphQL operations + +```go +fixtures := fixtures.NewFixtures() +users := fixtures.Users() +adminUser := users["admin"] +testProject := fixtures.Projects()["omega_project"] +``` + +### Assertions + +Each test utility provides rich assertion methods: + +- **Model Assertions**: Compare model instances +- **Response Assertions**: Validate HTTP responses +- **GraphQL Assertions**: Check GraphQL responses +- **Database Assertions**: Verify database state + +## Best Practices + +### 1. Test Organization + +- **One test file per source file**: Mirror the source code structure +- **Descriptive test names**: Use `TestFunctionName_Scenario_ExpectedBehavior` format +- **Group related tests**: Use subtests with `t.Run()` for related scenarios + +### 2. Test Data Management + +- **Use fixtures**: Leverage predefined fixtures for consistent test data +- **Clean up**: Always clean up test data after tests +- **Isolation**: Ensure tests don't depend on each other +- **Transactions**: Use database transactions for rollback capabilities + +### 3. Mocking and Dependencies + +- **Mock external services**: Don't make real API calls in tests +- **Inject dependencies**: Use dependency injection for testability +- **Test doubles**: Use appropriate test doubles (mocks, stubs, fakes) + +### 4. Error Testing + +- **Test error paths**: Ensure error conditions are properly tested +- **Validate error messages**: Check that error messages are meaningful +- **Edge cases**: Test boundary conditions and edge cases + +### 5. Performance Considerations + +- **Fast tests**: Keep unit tests fast (< 100ms each) +- **Parallel execution**: Use `t.Parallel()` where appropriate +- **Database optimization**: Use transactions and minimal data for speed + +## Test Patterns + +### 1. Table-Driven Tests + +```go +func TestUserValidation(t *testing.T) { + testCases := []struct { + name string + email string + expectedErr string + }{ + {"valid email", "test@example.com", ""}, + {"invalid email", "invalid-email", "invalid email format"}, + {"empty email", "", "email is required"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + user := &model.User{Email: tc.email} + err := user.Validate() + + if tc.expectedErr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} +``` + +### 2. Setup and Teardown + +```go +func TestUserService(t *testing.T) { + testSuite := tests.NewTestSuite() + testSuite.Setup(t) + defer testSuite.Teardown(t) + + t.Run("CreateUser", func(t *testing.T) { + // Test implementation + }) + + t.Run("GetUser", func(t *testing.T) { + // Test implementation + }) +} +``` + +### 3. Database Transactions + +```go +func TestDatabaseOperations(t *testing.T) { + testSuite := tests.NewTestSuite() + testSuite.Setup(t) + defer testSuite.Teardown(t) + + testSuite.RunInTestTransaction(t, func(tx *gorm.DB) { + // Database operations here will be rolled back + user := &model.User{Email: "test@example.com"} + tx.Create(user) + + // Assertions + var count int64 + tx.Model(&model.User{}).Where("email = ?", "test@example.com").Count(&count) + assert.Equal(t, int64(1), count) + }) +} +``` + +## Debugging Tests + +### 1. Verbose Output + +```bash +go test -v ./tests/... +``` + +### 2. Run Specific Tests + +```bash +go test -run TestSpecificFunction ./tests/unit/... +``` + +### 3. Test Coverage + +```bash +go test -coverprofile=coverage.out ./... +go tool cover -func=coverage.out +go tool cover -html=coverage.out -o coverage.html +``` + +### 4. Race Condition Detection + +```bash +go test -race ./tests/... +``` + +## Contributing + +When adding new tests: + +1. **Follow naming conventions**: Use clear, descriptive names +2. **Add documentation**: Include comments for complex test logic +3. **Update fixtures**: Add new fixtures for new features +4. **Maintain coverage**: Ensure new code has adequate test coverage +5. **Test edge cases**: Include tests for error conditions and edge cases + +## Troubleshooting + +### Common Issues + +1. **Database Connection Failed** + - Check PostgreSQL is running + - Verify environment variables + - Ensure test database exists + +2. **Tests Failing Intermittently** + - Check for race conditions + - Ensure proper test isolation + - Review shared state between tests + +3. **Slow Test Execution** + - Profile test performance + - Optimize database operations + - Consider using test transactions + +4. **Import Errors** + - Run `go mod tidy` + - Check module dependencies + - Verify Go version compatibility + +For additional help, refer to the main project documentation or contact the development team. \ No newline at end of file diff --git a/tests/graphql/basic_test.go b/tests/graphql/basic_test.go new file mode 100644 index 0000000..d4c8605 --- /dev/null +++ b/tests/graphql/basic_test.go @@ -0,0 +1,276 @@ +package graphql + +import ( + "omega-server/local/service" + "omega-server/tests" + "testing" + + "gorm.io/gorm" +) + +func TestGraphQLBasic(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 membership service + membershipService := createMembershipService(t, testSuite.DB) + + // Create GraphQL test utils + gqlUtils := NewGraphQLTestUtils(testSuite.DB, membershipService) + + t.Run("GetSchema", func(t *testing.T) { + schema := gqlUtils.GetSchema(t) + if schema == "" { + t.Fatal("Expected non-empty GraphQL schema") + } + + // Check for basic types + if !contains(schema, "type User") { + t.Error("Schema should contain User type") + } + if !contains(schema, "type Project") { + t.Error("Schema should contain Project type") + } + if !contains(schema, "type Task") { + t.Error("Schema should contain Task type") + } + if !contains(schema, "type Query") { + t.Error("Schema should contain Query type") + } + if !contains(schema, "type Mutation") { + t.Error("Schema should contain Mutation type") + } + }) + + t.Run("CreateUser", func(t *testing.T) { + response := gqlUtils.ExecuteCreateUserMutation(t, "test@example.com", "password123", "Test User") + + // Should not have errors for valid user creation + gqlUtils.AssertNoErrors(t, response) + gqlUtils.AssertDataNotNil(t, response) + + // Extract user data + userEmail := gqlUtils.ExtractString(t, response, "createUser", "email") + userFullName := gqlUtils.ExtractString(t, response, "createUser", "fullName") + userId := gqlUtils.ExtractString(t, response, "createUser", "id") + + if userEmail != "test@example.com" { + t.Errorf("Expected email 'test@example.com', got '%s'", userEmail) + } + if userFullName != "Test User" { + t.Errorf("Expected full name 'Test User', got '%s'", userFullName) + } + if userId == "" { + t.Error("Expected non-empty user ID") + } + }) + + t.Run("CreateUserWithInvalidEmail", func(t *testing.T) { + response := gqlUtils.ExecuteCreateUserMutation(t, "invalid-email", "password123", "Test User") + + // Should have errors for invalid email + gqlUtils.AssertHasErrors(t, response) + }) + + t.Run("Login", func(t *testing.T) { + // First create a user + createResponse := gqlUtils.ExecuteCreateUserMutation(t, "login@example.com", "password123", "Login User") + gqlUtils.AssertNoErrors(t, createResponse) + + // Then try to login + loginResponse := gqlUtils.ExecuteLoginMutation(t, "login@example.com", "password123") + + gqlUtils.AssertNoErrors(t, loginResponse) + gqlUtils.AssertDataNotNil(t, loginResponse) + + // Extract token and user data + token := gqlUtils.ExtractString(t, loginResponse, "login", "token") + userEmail := gqlUtils.ExtractString(t, loginResponse, "login", "user", "email") + + if token == "" { + t.Error("Expected non-empty token") + } + if userEmail != "login@example.com" { + t.Errorf("Expected email 'login@example.com', got '%s'", userEmail) + } + }) + + t.Run("LoginWithInvalidCredentials", func(t *testing.T) { + response := gqlUtils.ExecuteLoginMutation(t, "nonexistent@example.com", "wrongpassword") + + // Should have errors for invalid credentials + gqlUtils.AssertHasErrors(t, response) + gqlUtils.AssertErrorMessage(t, response, "Invalid credentials") + }) + + t.Run("MeQuery", func(t *testing.T) { + response := gqlUtils.ExecuteMeQuery(t) + + // Should return mock user data + gqlUtils.AssertNoErrors(t, response) + gqlUtils.AssertDataNotNil(t, response) + + // Extract user data + userEmail := gqlUtils.ExtractString(t, response, "me", "email") + userId := gqlUtils.ExtractString(t, response, "me", "id") + + if userEmail == "" { + t.Error("Expected non-empty email") + } + if userId == "" { + t.Error("Expected non-empty user ID") + } + }) + + t.Run("UsersQuery", func(t *testing.T) { + // Create a few test users first + gqlUtils.ExecuteCreateUserMutation(t, "user1@example.com", "password123", "User One") + gqlUtils.ExecuteCreateUserMutation(t, "user2@example.com", "password123", "User Two") + + response := gqlUtils.ExecuteUsersQuery(t) + + gqlUtils.AssertNoErrors(t, response) + gqlUtils.AssertDataNotNil(t, response) + + // Extract users array + users := gqlUtils.ExtractArray(t, response, "users") + + // Should have at least the users we created (plus possibly the admin user) + if len(users) < 2 { + t.Errorf("Expected at least 2 users, got %d", len(users)) + } + }) + + t.Run("CreateProject", func(t *testing.T) { + // Create a user first to be the owner + userResponse := gqlUtils.ExecuteCreateUserMutation(t, "owner@example.com", "password123", "Project Owner") + gqlUtils.AssertNoErrors(t, userResponse) + ownerID := gqlUtils.ExtractString(t, userResponse, "createUser", "id") + + response := gqlUtils.ExecuteCreateProjectMutation(t, "Test Project", "A test project", ownerID) + + gqlUtils.AssertNoErrors(t, response) + gqlUtils.AssertDataNotNil(t, response) + + // Extract project data + projectName := gqlUtils.ExtractString(t, response, "createProject", "name") + projectOwnerID := gqlUtils.ExtractString(t, response, "createProject", "ownerId") + projectID := gqlUtils.ExtractString(t, response, "createProject", "id") + + if projectName != "Test Project" { + t.Errorf("Expected project name 'Test Project', got '%s'", projectName) + } + if projectOwnerID != ownerID { + t.Errorf("Expected owner ID '%s', got '%s'", ownerID, projectOwnerID) + } + if projectID == "" { + t.Error("Expected non-empty project ID") + } + }) + + t.Run("ProjectsQuery", func(t *testing.T) { + response := gqlUtils.ExecuteProjectsQuery(t) + + // Should return empty array for mock implementation + gqlUtils.AssertNoErrors(t, response) + gqlUtils.AssertDataNotNil(t, response) + + projects := gqlUtils.ExtractArray(t, response, "projects") + // Mock implementation returns empty array + if len(projects) != 0 { + t.Errorf("Expected 0 projects from mock implementation, got %d", len(projects)) + } + }) + + t.Run("CreateTask", func(t *testing.T) { + response := gqlUtils.ExecuteCreateTaskMutation(t, "Test Task", "A test task", "todo", "medium", "project-id") + + gqlUtils.AssertNoErrors(t, response) + gqlUtils.AssertDataNotNil(t, response) + + // Extract task data + taskTitle := gqlUtils.ExtractString(t, response, "createTask", "title") + taskStatus := gqlUtils.ExtractString(t, response, "createTask", "status") + taskPriority := gqlUtils.ExtractString(t, response, "createTask", "priority") + taskID := gqlUtils.ExtractString(t, response, "createTask", "id") + + if taskTitle != "Test Task" { + t.Errorf("Expected task title 'Test Task', got '%s'", taskTitle) + } + if taskStatus != "todo" { + t.Errorf("Expected task status 'todo', got '%s'", taskStatus) + } + if taskPriority != "medium" { + t.Errorf("Expected task priority 'medium', got '%s'", taskPriority) + } + if taskID == "" { + t.Error("Expected non-empty task ID") + } + }) + + t.Run("TasksQuery", func(t *testing.T) { + response := gqlUtils.ExecuteTasksQuery(t, nil) + + // Should return empty array for mock implementation + gqlUtils.AssertNoErrors(t, response) + gqlUtils.AssertDataNotNil(t, response) + + tasks := gqlUtils.ExtractArray(t, response, "tasks") + // Mock implementation returns empty array + if len(tasks) != 0 { + t.Errorf("Expected 0 tasks from mock implementation, got %d", len(tasks)) + } + }) + + t.Run("InvalidQuery", func(t *testing.T) { + response := gqlUtils.ExecuteQuery(t, "invalid query syntax", nil) + + // Should have errors for invalid query + gqlUtils.AssertHasErrors(t, response) + }) + + t.Run("UnsupportedQuery", func(t *testing.T) { + response := gqlUtils.ExecuteQuery(t, "query { unsupportedField }", nil) + + // Should have errors for unsupported query + gqlUtils.AssertHasErrors(t, response) + gqlUtils.AssertErrorMessage(t, response, "Query not supported") + }) +} + +// createMembershipService creates a membership service for testing +func createMembershipService(t *testing.T, db *gorm.DB) *service.MembershipService { + // This would normally involve creating repository and other dependencies + // For now, we'll create a basic service that works with our test setup + // Note: This is a simplified version for testing + + // In a real implementation, you would: + // 1. Create repository with DB + // 2. Set up all dependencies + // 3. Configure the service properly + + // For this test, we'll return nil and handle it in the GraphQL handler + // The handler should gracefully handle the basic operations we're testing + return nil +} + +// 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 +} diff --git a/tests/graphql/graphql_test_utils.go b/tests/graphql/graphql_test_utils.go new file mode 100644 index 0000000..3193a8e --- /dev/null +++ b/tests/graphql/graphql_test_utils.go @@ -0,0 +1,427 @@ +package graphql + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "omega-server/local/graphql/handler" + "omega-server/local/service" + "testing" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +// GraphQLTestUtils provides utilities for testing GraphQL endpoints +type GraphQLTestUtils struct { + App *fiber.App + Handler *handler.GraphQLHandler + DB *gorm.DB +} + +// GraphQLTestRequest represents a GraphQL test request +type GraphQLTestRequest struct { + Query string `json:"query"` + Variables map[string]interface{} `json:"variables,omitempty"` +} + +// GraphQLTestResponse represents a GraphQL test response +type GraphQLTestResponse struct { + Data interface{} `json:"data,omitempty"` + Errors []GraphQLError `json:"errors,omitempty"` +} + +// GraphQLError represents a GraphQL error in test responses +type GraphQLError struct { + Message string `json:"message"` + Path []string `json:"path,omitempty"` +} + +// NewGraphQLTestUtils creates a new GraphQL test utilities instance +func NewGraphQLTestUtils(db *gorm.DB, membershipService *service.MembershipService) *GraphQLTestUtils { + app := fiber.New(fiber.Config{ + DisableStartupMessage: true, + }) + + graphqlHandler := handler.NewGraphQLHandler(membershipService) + app.Post("/graphql", graphqlHandler.Handle) + app.Get("/graphql", func(c *fiber.Ctx) error { + return c.SendString(graphqlHandler.GetSchema()) + }) + + return &GraphQLTestUtils{ + App: app, + Handler: graphqlHandler, + DB: db, + } +} + +// ExecuteQuery executes a GraphQL query and returns the response +func (gtu *GraphQLTestUtils) ExecuteQuery(t *testing.T, query string, variables map[string]interface{}) *GraphQLTestResponse { + request := GraphQLTestRequest{ + Query: query, + Variables: variables, + } + + body, err := json.Marshal(request) + if err != nil { + t.Fatalf("Failed to marshal GraphQL request: %v", err) + } + + req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + + resp, err := gtu.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute GraphQL request: %v", err) + } + defer resp.Body.Close() + + var response GraphQLTestResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + t.Fatalf("Failed to decode GraphQL response: %v", err) + } + + return &response +} + +// ExecuteQueryWithContext executes a GraphQL query with context +func (gtu *GraphQLTestUtils) ExecuteQueryWithContext(t *testing.T, ctx context.Context, query string, variables map[string]interface{}) *GraphQLTestResponse { + request := GraphQLTestRequest{ + Query: query, + Variables: variables, + } + + body, err := json.Marshal(request) + if err != nil { + t.Fatalf("Failed to marshal GraphQL request: %v", err) + } + + req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + req = req.WithContext(ctx) + + resp, err := gtu.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute GraphQL request: %v", err) + } + defer resp.Body.Close() + + var response GraphQLTestResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + t.Fatalf("Failed to decode GraphQL response: %v", err) + } + + return &response +} + +// ExecuteLoginMutation executes a login mutation +func (gtu *GraphQLTestUtils) ExecuteLoginMutation(t *testing.T, email, password string) *GraphQLTestResponse { + query := ` + mutation Login($email: String!, $password: String!) { + login(input: {email: $email, password: $password}) { + token + user { + id + email + fullName + } + } + } + ` + + variables := map[string]interface{}{ + "email": email, + "password": password, + } + + return gtu.ExecuteQuery(t, query, variables) +} + +// ExecuteCreateUserMutation executes a create user mutation +func (gtu *GraphQLTestUtils) ExecuteCreateUserMutation(t *testing.T, email, password, fullName string) *GraphQLTestResponse { + 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": email, + "password": password, + "fullName": fullName, + } + + return gtu.ExecuteQuery(t, query, variables) +} + +// ExecuteMeQuery executes a me query +func (gtu *GraphQLTestUtils) ExecuteMeQuery(t *testing.T) *GraphQLTestResponse { + query := ` + query Me { + me { + id + email + fullName + createdAt + updatedAt + } + } + ` + + return gtu.ExecuteQuery(t, query, nil) +} + +// ExecuteUsersQuery executes a users query +func (gtu *GraphQLTestUtils) ExecuteUsersQuery(t *testing.T) *GraphQLTestResponse { + query := ` + query Users { + users { + id + email + fullName + createdAt + updatedAt + } + } + ` + + return gtu.ExecuteQuery(t, query, nil) +} + +// ExecuteUserQuery executes a user query by ID +func (gtu *GraphQLTestUtils) ExecuteUserQuery(t *testing.T, userID string) *GraphQLTestResponse { + query := ` + query User($id: String!) { + user(id: $id) { + id + email + fullName + createdAt + updatedAt + } + } + ` + + variables := map[string]interface{}{ + "id": userID, + } + + return gtu.ExecuteQuery(t, query, variables) +} + +// ExecuteCreateProjectMutation executes a create project mutation +func (gtu *GraphQLTestUtils) ExecuteCreateProjectMutation(t *testing.T, name, description, ownerID string) *GraphQLTestResponse { + 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": name, + "description": description, + "ownerId": ownerID, + } + + return gtu.ExecuteQuery(t, query, variables) +} + +// ExecuteProjectsQuery executes a projects query +func (gtu *GraphQLTestUtils) ExecuteProjectsQuery(t *testing.T) *GraphQLTestResponse { + query := ` + query Projects { + projects { + id + name + description + ownerId + createdAt + updatedAt + } + } + ` + + return gtu.ExecuteQuery(t, query, nil) +} + +// ExecuteCreateTaskMutation executes a create task mutation +func (gtu *GraphQLTestUtils) ExecuteCreateTaskMutation(t *testing.T, title, description, status, priority, projectID string) *GraphQLTestResponse { + 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": title, + "description": description, + "status": status, + "priority": priority, + "projectId": projectID, + } + + return gtu.ExecuteQuery(t, query, variables) +} + +// ExecuteTasksQuery executes a tasks query +func (gtu *GraphQLTestUtils) ExecuteTasksQuery(t *testing.T, projectID *string) *GraphQLTestResponse { + query := ` + query Tasks($projectId: String) { + tasks(projectId: $projectId) { + id + title + description + status + priority + projectId + createdAt + updatedAt + } + } + ` + + variables := make(map[string]interface{}) + if projectID != nil { + variables["projectId"] = *projectID + } + + return gtu.ExecuteQuery(t, query, variables) +} + +// GetSchema returns the GraphQL schema +func (gtu *GraphQLTestUtils) GetSchema(t *testing.T) string { + return gtu.Handler.GetSchema() +} + +// AssertNoErrors asserts that the GraphQL response has no errors +func (gtu *GraphQLTestUtils) AssertNoErrors(t *testing.T, response *GraphQLTestResponse) { + if len(response.Errors) > 0 { + t.Fatalf("Expected no GraphQL errors, but got: %+v", response.Errors) + } +} + +// AssertHasErrors asserts that the GraphQL response has errors +func (gtu *GraphQLTestUtils) AssertHasErrors(t *testing.T, response *GraphQLTestResponse) { + if len(response.Errors) == 0 { + t.Fatalf("Expected GraphQL errors, but got none") + } +} + +// AssertErrorMessage asserts that the GraphQL response contains a specific error message +func (gtu *GraphQLTestUtils) AssertErrorMessage(t *testing.T, response *GraphQLTestResponse, expectedMessage string) { + if len(response.Errors) == 0 { + t.Fatalf("Expected GraphQL errors, but got none") + } + + for _, err := range response.Errors { + if err.Message == expectedMessage { + return + } + } + + t.Fatalf("Expected error message '%s', but not found in errors: %+v", expectedMessage, response.Errors) +} + +// AssertDataNotNil asserts that the GraphQL response data is not nil +func (gtu *GraphQLTestUtils) AssertDataNotNil(t *testing.T, response *GraphQLTestResponse) { + if response.Data == nil { + t.Fatalf("Expected GraphQL data to not be nil, but it was") + } +} + +// AssertDataNil asserts that the GraphQL response data is nil +func (gtu *GraphQLTestUtils) AssertDataNil(t *testing.T, response *GraphQLTestResponse) { + if response.Data != nil { + t.Fatalf("Expected GraphQL data to be nil, but got: %+v", response.Data) + } +} + +// ExtractField extracts a field from the GraphQL response data +func (gtu *GraphQLTestUtils) ExtractField(t *testing.T, response *GraphQLTestResponse, fieldPath ...string) interface{} { + if response.Data == nil { + t.Fatalf("Cannot extract field from nil data") + } + + data := response.Data + for _, field := range fieldPath { + if dataMap, ok := data.(map[string]interface{}); ok { + if value, exists := dataMap[field]; exists { + data = value + } else { + t.Fatalf("Field '%s' not found in data: %+v", field, dataMap) + } + } else { + t.Fatalf("Cannot extract field '%s' from non-map data: %+v", field, data) + } + } + + return data +} + +// ExtractString extracts a string field from the GraphQL response data +func (gtu *GraphQLTestUtils) ExtractString(t *testing.T, response *GraphQLTestResponse, fieldPath ...string) string { + value := gtu.ExtractField(t, response, fieldPath...) + if str, ok := value.(string); ok { + return str + } + t.Fatalf("Expected string value for field %v, but got: %+v", fieldPath, value) + return "" +} + +// ExtractInt extracts an integer field from the GraphQL response data +func (gtu *GraphQLTestUtils) ExtractInt(t *testing.T, response *GraphQLTestResponse, fieldPath ...string) int { + value := gtu.ExtractField(t, response, 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 +} + +// ExtractBool extracts a boolean field from the GraphQL response data +func (gtu *GraphQLTestUtils) ExtractBool(t *testing.T, response *GraphQLTestResponse, fieldPath ...string) bool { + value := gtu.ExtractField(t, response, fieldPath...) + if boolVal, ok := value.(bool); ok { + return boolVal + } + t.Fatalf("Expected bool value for field %v, but got: %+v", fieldPath, value) + return false +} + +// ExtractArray extracts an array field from the GraphQL response data +func (gtu *GraphQLTestUtils) ExtractArray(t *testing.T, response *GraphQLTestResponse, fieldPath ...string) []interface{} { + value := gtu.ExtractField(t, response, fieldPath...) + if arrVal, ok := value.([]interface{}); ok { + return arrVal + } + t.Fatalf("Expected array value for field %v, but got: %+v", fieldPath, value) + return nil +} diff --git a/tests/integration/graphql_integration_test.go b/tests/integration/graphql_integration_test.go new file mode 100644 index 0000000..0df49d6 --- /dev/null +++ b/tests/integration/graphql_integration_test.go @@ -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") + } + }) +} diff --git a/tests/integration/integration_test_utils.go b/tests/integration/integration_test_utils.go new file mode 100644 index 0000000..9a4dc34 --- /dev/null +++ b/tests/integration/integration_test_utils.go @@ -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 +} diff --git a/tests/testing.go b/tests/testing.go new file mode 100644 index 0000000..657d10c --- /dev/null +++ b/tests/testing.go @@ -0,0 +1,362 @@ +package tests + +import ( + "context" + "fmt" + "omega-server/local/model" + "omega-server/local/utl/logging" + "os" + "testing" + + "go.uber.org/dig" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// TestConfig holds configuration for testing +type TestConfig struct { + DBHost string + DBPort string + DBUser string + DBPassword string + DBName string + DBSSLMode string +} + +// TestSuite provides a base structure for test suites +type TestSuite struct { + DB *gorm.DB + DI *dig.Container + Config *TestConfig +} + +// NewTestSuite creates a new test suite +func NewTestSuite() *TestSuite { + config := &TestConfig{ + DBHost: getEnvOrDefault("TEST_DB_HOST", "localhost"), + DBPort: getEnvOrDefault("TEST_DB_PORT", "5432"), + DBUser: getEnvOrDefault("TEST_DB_USER", "postgres"), + DBPassword: getEnvOrDefault("TEST_DB_PASSWORD", "password"), + DBName: getEnvOrDefault("TEST_DB_NAME", "omega_test"), + DBSSLMode: getEnvOrDefault("TEST_DB_SSL_MODE", "disable"), + } + + return &TestSuite{ + Config: config, + DI: dig.New(), + } +} + +// Setup initializes the test environment +func (ts *TestSuite) Setup(t *testing.T) { + // Initialize logger for tests + logger, err := logging.Initialize() + if err != nil { + t.Fatalf("Failed to initialize logger: %v", err) + } + defer logger.Close() + + // Setup test database + ts.setupTestDatabase(t) + + // Setup dependency injection + ts.setupDI(t) + + // Migrate database + ts.migrateDatabase(t) +} + +// Teardown cleans up the test environment +func (ts *TestSuite) Teardown(t *testing.T) { + if ts.DB != nil { + // Clean up test data + ts.cleanupDatabase(t) + + // Close database connection + sqlDB, err := ts.DB.DB() + if err == nil { + sqlDB.Close() + } + } +} + +// setupTestDatabase initializes the test database connection +func (ts *TestSuite) setupTestDatabase(t *testing.T) { + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=UTC", + ts.Config.DBHost, + ts.Config.DBUser, + ts.Config.DBPassword, + ts.Config.DBName, + ts.Config.DBPort, + ts.Config.DBSSLMode, + ) + + // Use silent logger for tests + gormLogger := logger.Default.LogMode(logger.Silent) + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ + Logger: gormLogger, + }) + if err != nil { + t.Skipf("Failed to connect to test database: %v", err) + } + + ts.DB = db +} + +// setupDI initializes dependency injection for tests +func (ts *TestSuite) setupDI(t *testing.T) { + err := ts.DI.Provide(func() *gorm.DB { + return ts.DB + }) + if err != nil { + t.Fatalf("Failed to provide database to DI: %v", err) + } +} + +// migrateDatabase runs database migrations for tests +func (ts *TestSuite) migrateDatabase(t *testing.T) { + err := ts.DB.AutoMigrate( + &model.User{}, + &model.Role{}, + &model.Permission{}, + &model.Type{}, + &model.Project{}, + &model.Task{}, + &model.Integration{}, + &model.ProjectMember{}, + &model.TaskAssignee{}, + &model.SystemConfig{}, + &model.AuditLog{}, + &model.SecurityEvent{}, + ) + if err != nil { + t.Fatalf("Failed to migrate test database: %v", err) + } +} + +// cleanupDatabase cleans up test data +func (ts *TestSuite) 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 := ts.DB.Exec(fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", table)).Error + if err != nil { + t.Logf("Warning: Failed to truncate table %s: %v", table, err) + } + } +} + +// CreateTestUser creates a test user +func (ts *TestSuite) CreateTestUser(t *testing.T, email, fullName string) *model.User { + user := &model.User{ + Email: email, + FullName: fullName, + } + user.Init() + + err := user.SetPassword("testpassword123") + if err != nil { + t.Fatalf("Failed to set password for test user: %v", err) + } + + err = ts.DB.Create(user).Error + if err != nil { + t.Fatalf("Failed to create test user: %v", err) + } + + return user +} + +// CreateTestRole creates a test role +func (ts *TestSuite) CreateTestRole(t *testing.T, name, description string) *model.Role { + role := &model.Role{ + Name: name, + Description: description, + Active: true, + } + role.Init() + + err := ts.DB.Create(role).Error + if err != nil { + t.Fatalf("Failed to create test role: %v", err) + } + + return role +} + +// CreateTestPermission creates a test permission +func (ts *TestSuite) CreateTestPermission(t *testing.T, name, description, category string) *model.Permission { + permission := &model.Permission{ + Name: name, + Description: description, + Category: category, + Active: true, + } + permission.Init() + + err := ts.DB.Create(permission).Error + if err != nil { + t.Fatalf("Failed to create test permission: %v", err) + } + + return permission +} + +// CreateTestType creates a test project type +func (ts *TestSuite) CreateTestType(t *testing.T, name, description string, userID *string) *model.Type { + projectType := &model.Type{ + Name: name, + Description: description, + UserID: userID, + } + projectType.Init() + + err := ts.DB.Create(projectType).Error + if err != nil { + t.Fatalf("Failed to create test type: %v", err) + } + + return projectType +} + +// CreateTestProject creates a test project +func (ts *TestSuite) 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 := ts.DB.Create(project).Error + if err != nil { + t.Fatalf("Failed to create test project: %v", err) + } + + return project +} + +// CreateTestTask creates a test task +func (ts *TestSuite) 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 := ts.DB.Create(task).Error + if err != nil { + t.Fatalf("Failed to create test task: %v", err) + } + + return task +} + +// WithTransaction runs a function within a database transaction +func (ts *TestSuite) WithTransaction(t *testing.T, fn func(tx *gorm.DB) error) { + tx := ts.DB.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + t.Fatalf("Transaction panicked: %v", r) + } + }() + + if err := fn(tx); err != nil { + tx.Rollback() + t.Fatalf("Transaction failed: %v", err) + } + + if err := tx.Commit().Error; err != nil { + t.Fatalf("Failed to commit transaction: %v", err) + } +} + +// AssertUserExists asserts that a user exists in the database +func (ts *TestSuite) AssertUserExists(t *testing.T, email string) *model.User { + var user model.User + err := ts.DB.Where("email = ?", email).First(&user).Error + if err != nil { + t.Fatalf("Expected user with email %s to exist, but not found: %v", email, err) + } + return &user +} + +// AssertUserNotExists asserts that a user does not exist in the database +func (ts *TestSuite) AssertUserNotExists(t *testing.T, email string) { + var user model.User + err := ts.DB.Where("email = ?", email).First(&user).Error + if err == nil { + t.Fatalf("Expected user with email %s to not exist, but found: %+v", email, user) + } +} + +// AssertProjectExists asserts that a project exists in the database +func (ts *TestSuite) AssertProjectExists(t *testing.T, name string) *model.Project { + var project model.Project + err := ts.DB.Where("name = ?", name).First(&project).Error + if err != nil { + t.Fatalf("Expected project with name %s to exist, but not found: %v", name, err) + } + return &project +} + +// AssertTaskExists asserts that a task exists in the database +func (ts *TestSuite) AssertTaskExists(t *testing.T, title string) *model.Task { + var task model.Task + err := ts.DB.Where("title = ?", title).First(&task).Error + if err != nil { + t.Fatalf("Expected task with title %s to exist, but not found: %v", title, err) + } + return &task +} + +// TestContext creates a test context +func (ts *TestSuite) TestContext() context.Context { + return context.Background() +} + +// getEnvOrDefault returns environment variable value or default +func getEnvOrDefault(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +// SkipIfNoTestDB skips test if test database is not available +func (ts *TestSuite) SkipIfNoTestDB(t *testing.T) { + if ts.DB == nil { + t.Skip("Test database not available, skipping test") + } +} + +// RunInTestTransaction runs a test function in a transaction that gets rolled back +func (ts *TestSuite) RunInTestTransaction(t *testing.T, testFn func(tx *gorm.DB)) { + tx := ts.DB.Begin() + defer tx.Rollback() + + testFn(tx) +} diff --git a/tests/unit/graphql_handler_test.go b/tests/unit/graphql_handler_test.go new file mode 100644 index 0000000..f887ee4 --- /dev/null +++ b/tests/unit/graphql_handler_test.go @@ -0,0 +1,272 @@ +package unit + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "omega-server/local/graphql/handler" + "testing" + + "github.com/gofiber/fiber/v2" +) + +func TestGraphQLHandler(t *testing.T) { + // Create a basic GraphQL handler without dependencies + graphqlHandler := handler.NewGraphQLHandler(nil) + + // Create a Fiber app for testing + app := fiber.New(fiber.Config{ + DisableStartupMessage: true, + }) + + // Set up routes + app.Post("/graphql", graphqlHandler.Handle) + app.Get("/graphql", func(c *fiber.Ctx) error { + return c.SendString(graphqlHandler.GetSchema()) + }) + + t.Run("GetSchema", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/graphql", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + // Read response body + buf := new(bytes.Buffer) + buf.ReadFrom(resp.Body) + schema := buf.String() + + if schema == "" { + t.Fatal("Expected non-empty schema") + } + + // Check for basic GraphQL types + expectedTypes := []string{"type User", "type Project", "type Task", "type Query", "type Mutation"} + for _, expectedType := range expectedTypes { + if !contains(schema, expectedType) { + t.Errorf("Schema should contain '%s'", expectedType) + } + } + }) + + t.Run("InvalidRequestBody", func(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBufferString("invalid json")) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("Expected status 400, got %d", resp.StatusCode) + } + + // Parse response + var response map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + t.Fatalf("Failed to decode response: %v", err) + } + + // Should have errors + if errors, exists := response["errors"]; !exists { + t.Error("Expected errors in response") + } else { + errorList, ok := errors.([]interface{}) + if !ok || len(errorList) == 0 { + t.Error("Expected non-empty error list") + } + } + }) + + t.Run("MeQuery", func(t *testing.T) { + requestBody := map[string]interface{}{ + "query": ` + query Me { + me { + id + email + fullName + } + } + `, + } + + body, _ := json.Marshal(requestBody) + req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + // Parse response + var response map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + t.Fatalf("Failed to decode response: %v", err) + } + + // Should have data + if data, exists := response["data"]; !exists { + t.Error("Expected data in response") + } else { + dataMap, ok := data.(map[string]interface{}) + if !ok { + t.Error("Expected data to be an object") + } else { + if me, exists := dataMap["me"]; !exists { + t.Error("Expected 'me' field in data") + } else { + meMap, ok := me.(map[string]interface{}) + if !ok { + t.Error("Expected 'me' to be an object") + } else { + // Check for required fields + if id, exists := meMap["id"]; !exists || id == "" { + t.Error("Expected non-empty 'id' field") + } + if email, exists := meMap["email"]; !exists || email == "" { + t.Error("Expected non-empty 'email' field") + } + } + } + } + } + }) + + t.Run("UnsupportedQuery", func(t *testing.T) { + requestBody := map[string]interface{}{ + "query": ` + query UnsupportedQuery { + unsupportedField { + id + name + } + } + `, + } + + body, _ := json.Marshal(requestBody) + req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("Expected status 400, got %d", resp.StatusCode) + } + + // Parse response + var response map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + t.Fatalf("Failed to decode response: %v", err) + } + + // Should have errors + if errors, exists := response["errors"]; !exists { + t.Error("Expected errors in response") + } else { + errorList, ok := errors.([]interface{}) + if !ok || len(errorList) == 0 { + t.Error("Expected non-empty error list") + } else { + // Check error message + 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', got '%s'", message) + } + } + } + } + } + }) + + t.Run("CreateProjectMutation", func(t *testing.T) { + requestBody := map[string]interface{}{ + "query": ` + mutation CreateProject($name: String!, $description: String, $ownerId: String!) { + createProject(input: {name: $name, description: $description, ownerId: $ownerId}) { + id + name + description + ownerId + } + } + `, + "variables": map[string]interface{}{ + "name": "Test Project", + "description": "A test project", + "ownerId": "test-owner-id", + }, + } + + body, _ := json.Marshal(requestBody) + req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + // Parse response + var response map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + t.Fatalf("Failed to decode response: %v", err) + } + + // Should have data + if data, exists := response["data"]; !exists { + t.Error("Expected data in response") + } else { + dataMap, ok := data.(map[string]interface{}) + if !ok { + t.Error("Expected data to be an object") + } else { + if createProject, exists := dataMap["createProject"]; !exists { + t.Error("Expected 'createProject' field in data") + } else { + projectMap, ok := createProject.(map[string]interface{}) + if !ok { + t.Error("Expected 'createProject' to be an object") + } else { + // Check for required fields + if id, exists := projectMap["id"]; !exists || id == "" { + t.Error("Expected non-empty 'id' field") + } + if name, exists := projectMap["name"]; !exists || name != "Test Project" { + t.Errorf("Expected name 'Test Project', got '%v'", name) + } + if ownerId, exists := projectMap["ownerId"]; !exists || ownerId != "test-owner-id" { + t.Errorf("Expected ownerId 'test-owner-id', got '%v'", ownerId) + } + } + } + } + } + }) +} diff --git a/tests/unit/unit_test_utils.go b/tests/unit/unit_test_utils.go new file mode 100644 index 0000000..41447ea --- /dev/null +++ b/tests/unit/unit_test_utils.go @@ -0,0 +1,376 @@ +package unit + +import ( + "context" + "omega-server/local/model" + "testing" + + "gorm.io/gorm" +) + +// UnitTestUtils provides utilities for unit testing +type UnitTestUtils struct { + DB *gorm.DB +} + +// NewUnitTestUtils creates a new unit test utilities instance +func NewUnitTestUtils(db *gorm.DB) *UnitTestUtils { + return &UnitTestUtils{ + DB: db, + } +} + +// MockUser creates a mock user for testing +func (utu *UnitTestUtils) MockUser(id, email, fullName string) *model.User { + user := &model.User{ + BaseModel: model.BaseModel{ + ID: id, + }, + Email: email, + FullName: fullName, + } + user.Init() + return user +} + +// MockRole creates a mock role for testing +func (utu *UnitTestUtils) MockRole(id, name, description string) *model.Role { + role := &model.Role{ + BaseModel: model.BaseModel{ + ID: id, + }, + Name: name, + Description: description, + Active: true, + System: false, + } + role.Init() + return role +} + +// MockPermission creates a mock permission for testing +func (utu *UnitTestUtils) MockPermission(id, name, description, category string) *model.Permission { + permission := &model.Permission{ + BaseModel: model.BaseModel{ + ID: id, + }, + Name: name, + Description: description, + Category: category, + Active: true, + System: false, + } + permission.Init() + return permission +} + +// MockType creates a mock project type for testing +func (utu *UnitTestUtils) MockType(id, name, description string, userID *string) *model.Type { + projectType := &model.Type{ + BaseModel: model.BaseModel{ + ID: id, + }, + Name: name, + Description: description, + UserID: userID, + } + projectType.Init() + return projectType +} + +// MockProject creates a mock project for testing +func (utu *UnitTestUtils) MockProject(id, name, description, ownerID, typeID string) *model.Project { + project := &model.Project{ + BaseModel: model.BaseModel{ + ID: id, + }, + Name: name, + Description: description, + OwnerID: ownerID, + TypeID: typeID, + } + project.Init() + return project +} + +// MockTask creates a mock task for testing +func (utu *UnitTestUtils) MockTask(id, title, description, projectID string) *model.Task { + task := &model.Task{ + BaseModel: model.BaseModel{ + ID: id, + }, + Title: title, + Description: description, + Status: model.TaskStatusTodo, + Priority: model.TaskPriorityMedium, + ProjectID: projectID, + } + task.Init() + return task +} + +// MockIntegration creates a mock integration for testing +func (utu *UnitTestUtils) MockIntegration(id, projectID, integrationType string, config map[string]interface{}) *model.Integration { + integration := &model.Integration{ + BaseModel: model.BaseModel{ + ID: id, + }, + ProjectID: projectID, + Type: integrationType, + } + integration.Init() + integration.SetConfig(config) + return integration +} + +// AssertUserEqual asserts that two users are equal +func (utu *UnitTestUtils) AssertUserEqual(t *testing.T, expected, actual *model.User) { + if expected.ID != actual.ID { + t.Errorf("Expected user ID %s, got %s", expected.ID, actual.ID) + } + if expected.Email != actual.Email { + t.Errorf("Expected user email %s, got %s", expected.Email, actual.Email) + } + if expected.FullName != actual.FullName { + t.Errorf("Expected user full name %s, got %s", expected.FullName, actual.FullName) + } +} + +// AssertProjectEqual asserts that two projects are equal +func (utu *UnitTestUtils) AssertProjectEqual(t *testing.T, expected, actual *model.Project) { + if expected.ID != actual.ID { + t.Errorf("Expected project ID %s, got %s", expected.ID, actual.ID) + } + if expected.Name != actual.Name { + t.Errorf("Expected project name %s, got %s", expected.Name, actual.Name) + } + if expected.Description != actual.Description { + t.Errorf("Expected project description %s, got %s", expected.Description, actual.Description) + } + if expected.OwnerID != actual.OwnerID { + t.Errorf("Expected project owner ID %s, got %s", expected.OwnerID, actual.OwnerID) + } + if expected.TypeID != actual.TypeID { + t.Errorf("Expected project type ID %s, got %s", expected.TypeID, actual.TypeID) + } +} + +// AssertTaskEqual asserts that two tasks are equal +func (utu *UnitTestUtils) AssertTaskEqual(t *testing.T, expected, actual *model.Task) { + if expected.ID != actual.ID { + t.Errorf("Expected task ID %s, got %s", expected.ID, actual.ID) + } + if expected.Title != actual.Title { + t.Errorf("Expected task title %s, got %s", expected.Title, actual.Title) + } + if expected.Description != actual.Description { + t.Errorf("Expected task description %s, got %s", expected.Description, actual.Description) + } + if expected.Status != actual.Status { + t.Errorf("Expected task status %s, got %s", expected.Status, actual.Status) + } + if expected.Priority != actual.Priority { + t.Errorf("Expected task priority %s, got %s", expected.Priority, actual.Priority) + } + if expected.ProjectID != actual.ProjectID { + t.Errorf("Expected task project ID %s, got %s", expected.ProjectID, actual.ProjectID) + } +} + +// AssertRoleEqual asserts that two roles are equal +func (utu *UnitTestUtils) AssertRoleEqual(t *testing.T, expected, actual *model.Role) { + if expected.ID != actual.ID { + t.Errorf("Expected role ID %s, got %s", expected.ID, actual.ID) + } + if expected.Name != actual.Name { + t.Errorf("Expected role name %s, got %s", expected.Name, actual.Name) + } + if expected.Description != actual.Description { + t.Errorf("Expected role description %s, got %s", expected.Description, actual.Description) + } + if expected.Active != actual.Active { + t.Errorf("Expected role active %t, got %t", expected.Active, actual.Active) + } + if expected.System != actual.System { + t.Errorf("Expected role system %t, got %t", expected.System, actual.System) + } +} + +// AssertPermissionEqual asserts that two permissions are equal +func (utu *UnitTestUtils) AssertPermissionEqual(t *testing.T, expected, actual *model.Permission) { + if expected.ID != actual.ID { + t.Errorf("Expected permission ID %s, got %s", expected.ID, actual.ID) + } + if expected.Name != actual.Name { + t.Errorf("Expected permission name %s, got %s", expected.Name, actual.Name) + } + if expected.Description != actual.Description { + t.Errorf("Expected permission description %s, got %s", expected.Description, actual.Description) + } + if expected.Category != actual.Category { + t.Errorf("Expected permission category %s, got %s", expected.Category, actual.Category) + } + if expected.Active != actual.Active { + t.Errorf("Expected permission active %t, got %t", expected.Active, actual.Active) + } + if expected.System != actual.System { + t.Errorf("Expected permission system %t, got %t", expected.System, actual.System) + } +} + +// AssertTypeEqual asserts that two types are equal +func (utu *UnitTestUtils) AssertTypeEqual(t *testing.T, expected, actual *model.Type) { + if expected.ID != actual.ID { + t.Errorf("Expected type ID %s, got %s", expected.ID, actual.ID) + } + if expected.Name != actual.Name { + t.Errorf("Expected type name %s, got %s", expected.Name, actual.Name) + } + if expected.Description != actual.Description { + t.Errorf("Expected type description %s, got %s", expected.Description, actual.Description) + } + if (expected.UserID == nil) != (actual.UserID == nil) { + t.Errorf("Expected type user ID nullability mismatch") + } else if expected.UserID != nil && actual.UserID != nil && *expected.UserID != *actual.UserID { + t.Errorf("Expected type user ID %s, got %s", *expected.UserID, *actual.UserID) + } +} + +// AssertIntegrationEqual asserts that two integrations are equal +func (utu *UnitTestUtils) AssertIntegrationEqual(t *testing.T, expected, actual *model.Integration) { + if expected.ID != actual.ID { + t.Errorf("Expected integration ID %s, got %s", expected.ID, actual.ID) + } + if expected.ProjectID != actual.ProjectID { + t.Errorf("Expected integration project ID %s, got %s", expected.ProjectID, actual.ProjectID) + } + if expected.Type != actual.Type { + t.Errorf("Expected integration type %s, got %s", expected.Type, actual.Type) + } +} + +// CreateTestContext creates a test context +func (utu *UnitTestUtils) CreateTestContext() context.Context { + return context.Background() +} + +// AssertNoError asserts that there is no error +func (utu *UnitTestUtils) AssertNoError(t *testing.T, err error) { + if err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } +} + +// AssertError asserts that there is an error +func (utu *UnitTestUtils) AssertError(t *testing.T, err error) { + if err == nil { + t.Fatalf("Expected an error, but got none") + } +} + +// AssertErrorMessage asserts that the error has a specific message +func (utu *UnitTestUtils) AssertErrorMessage(t *testing.T, err error, expectedMessage string) { + if err == nil { + t.Fatalf("Expected an error with message '%s', but got no error", expectedMessage) + } + if err.Error() != expectedMessage { + t.Fatalf("Expected error message '%s', but got '%s'", expectedMessage, err.Error()) + } +} + +// AssertStringEqual asserts that two strings are equal +func (utu *UnitTestUtils) AssertStringEqual(t *testing.T, expected, actual string) { + if expected != actual { + t.Errorf("Expected string '%s', got '%s'", expected, actual) + } +} + +// AssertIntEqual asserts that two integers are equal +func (utu *UnitTestUtils) AssertIntEqual(t *testing.T, expected, actual int) { + if expected != actual { + t.Errorf("Expected int %d, got %d", expected, actual) + } +} + +// AssertBoolEqual asserts that two booleans are equal +func (utu *UnitTestUtils) AssertBoolEqual(t *testing.T, expected, actual bool) { + if expected != actual { + t.Errorf("Expected bool %t, got %t", expected, actual) + } +} + +// AssertNotNil asserts that a value is not nil +func (utu *UnitTestUtils) AssertNotNil(t *testing.T, value interface{}) { + if value == nil { + t.Fatalf("Expected value to not be nil, but it was") + } +} + +// AssertNil asserts that a value is nil +func (utu *UnitTestUtils) AssertNil(t *testing.T, value interface{}) { + if value != nil { + t.Fatalf("Expected value to be nil, but got: %+v", value) + } +} + +// AssertSliceLength asserts that a slice has a specific length +func (utu *UnitTestUtils) AssertSliceLength(t *testing.T, slice interface{}, expectedLength int) { + switch s := slice.(type) { + case []interface{}: + if len(s) != expectedLength { + t.Errorf("Expected slice length %d, got %d", expectedLength, len(s)) + } + case []*model.User: + if len(s) != expectedLength { + t.Errorf("Expected slice length %d, got %d", expectedLength, len(s)) + } + case []*model.Project: + if len(s) != expectedLength { + t.Errorf("Expected slice length %d, got %d", expectedLength, len(s)) + } + case []*model.Task: + if len(s) != expectedLength { + t.Errorf("Expected slice length %d, got %d", expectedLength, len(s)) + } + case []*model.Role: + if len(s) != expectedLength { + t.Errorf("Expected slice length %d, got %d", expectedLength, len(s)) + } + case []*model.Permission: + if len(s) != expectedLength { + t.Errorf("Expected slice length %d, got %d", expectedLength, len(s)) + } + default: + t.Fatalf("Unsupported slice type for length assertion: %T", slice) + } +} + +// AssertContains asserts that a string contains a substring +func (utu *UnitTestUtils) AssertContains(t *testing.T, str, substr string) { + if !contains(str, substr) { + t.Errorf("Expected string '%s' to contain '%s'", str, substr) + } +} + +// AssertNotContains asserts that a string does not contain a substring +func (utu *UnitTestUtils) AssertNotContains(t *testing.T, str, substr string) { + if contains(str, substr) { + t.Errorf("Expected string '%s' to not contain '%s'", str, substr) + } +} + +// 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 +}