diff --git a/.github/workflows/e2e-suite.yaml b/.github/workflows/e2e-suite.yaml new file mode 100644 index 00000000..3651b043 --- /dev/null +++ b/.github/workflows/e2e-suite.yaml @@ -0,0 +1,197 @@ +name: E2E Test Nixopus API + +on: + push: + branches: + - "master" + - "main" + - "feat/develop" + paths: + - "api/**/*.go" + - "api/**/*.yaml" + - "api/**/*.yml" + - "api/**/*.sql" + pull_request: + branches: + - "master" + - "main" + - "feat/develop" + paths: + - "api/**/*.go" + - "api/**/*.yaml" + - "api/**/*.yml" + - "api/**/*.sql" + workflow_dispatch: + inputs: + branch: + description: "Custom branch to run the E2E tests on" + required: true + default: "main" + +jobs: + e2e-suite: + runs-on: ubuntu-latest + services: + test-db: + image: postgres:14-alpine + env: + POSTGRES_USER: nixopus + POSTGRES_PASSWORD: nixopus + POSTGRES_DB: nixopus_test + ports: + - 5433:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + redis: + image: redis:7-alpine + ports: + - 6379:6379 + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: "1.23.4" + check-latest: true + cache: true + cache-dependency-path: api/go.sum + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Wait for PostgreSQL + run: | + for i in {1..10}; do + if pg_isready -h localhost -p 5433 -U nixopus -d nixopus_test; then + echo "PostgreSQL is ready" + exit 0 + fi + echo "Waiting for PostgreSQL... $i" + sleep 2 + done + echo "PostgreSQL failed to start" + exit 1 + + - name: Wait for Redis + run: | + for i in {1..10}; do + if nc -z localhost 6379; then + echo "Redis is ready" + exit 0 + fi + echo "Waiting for Redis... $i" + sleep 2 + done + echo "Redis failed to start" + exit 1 + + - name: Create .env file + run: | + cd api + cat > .env << EOF + DB_NAME=nixopus_test + USERNAME=nixopus + PASSWORD=nixopus + HOST_NAME=localhost + DB_PORT=5433 + SSL_MODE=disable + PORT=8080 + REDIS_URL=redis://localhost:6379 + DOCKER_HOST=unix:///var/run/docker.sock + JWT_SECRET=test-jwt-secret-key-for-ci + REFRESH_TOKEN_SECRET=test-refresh-secret-key-for-ci + ENV=development + ALLOWED_ORIGIN=http://localhost:3000 + EOF + + - name: Start API server with Go + env: + DB_HOST: localhost + DB_PORT: 5433 + DB_USER: nixopus + DB_PASSWORD: nixopus + DB_NAME: nixopus_test + PORT: 8080 + run: | + cd api + echo "Contents of .env file:" + cat .env + echo "Starting server with go run..." + go run main.go > server.log 2>&1 & + echo $! > server.pid + echo "Server started with PID: $(cat server.pid)" + sleep 5 + echo "Server log output:" + cat server.log || echo "No server.log file found" + + - name: Wait for API server + run: | + cd api + for i in {1..50}; do + echo "Attempt $i: Testing connection to localhost:8080..." + + # Check if server process is still running + if [ -f server.pid ] && ! kill -0 $(cat server.pid) 2>/dev/null; then + echo "Server process died, checking logs:" + cat server.log || echo "No server.log file" + exit 1 + fi + + # Test health endpoint + if curl -f http://localhost:8080/api/v1/health; then + echo "Health check passed, testing database connectivity..." + + # Test a simple auth endpoint to verify full readiness + response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/v1/auth/is-admin-registered) + if [ "$response" = "200" ]; then + echo "API server is fully ready (health + auth endpoints responding)" + + # Give additional time for any async initialization + sleep 3 + exit 0 + else + echo "Health passed but auth endpoint returned $response, server not fully ready" + fi + fi + + echo "Waiting for API server... $i" + sleep 2 + + # Show server log every 10 attempts + if [ $((i % 10)) -eq 0 ]; then + echo "Server log at attempt $i:" + tail -20 server.log || echo "No server.log file" + fi + done + echo "API server failed to start" + echo "Final server log:" + cat server.log || echo "No server.log file" + exit 1 + + - name: Run unit tests + intg tests + env: + DB_HOST: localhost + DB_PORT: 5433 + DB_USER: nixopus + DB_PASSWORD: nixopus + DB_NAME: nixopus_test + TEST_TIMEOUT: 30s + TEST_RETRY_COUNT: 3 + run: | + cd api && make test-all + + - name: Stop API server + if: always() + run: | + cd api + # Kill server process if it exists + if [ -f server.pid ]; then + kill $(cat server.pid) || true + rm server.pid + fi + lsof -ti:8080 | xargs kill -9 || true diff --git a/.github/workflows/greetings.yaml b/.github/workflows/greetings.yaml index d19553ce..4eab16dd 100644 --- a/.github/workflows/greetings.yaml +++ b/.github/workflows/greetings.yaml @@ -4,13 +4,23 @@ on: [pull_request_target, issues] jobs: greeting: + if: | + github.event_name == 'issues' || ( + github.event_name == 'pull_request_target' && + ( + github.event.pull_request.base.ref == 'main' || + github.event.pull_request.base.ref == 'master' || + contains(github.event.pull_request.base.ref, 'feat/') + + ) + ) runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - - uses: actions/first-interaction@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: "Thank you for creating your first issue! We appreciate your contribution and will review it soon. Please ensure you've provided all necessary details and followed the issue template." - pr-message: "Thank you for your first pull request! Before we review, please ensure your code follows our quality standards: run tests, check formatting, and verify linting. We'll review your changes as soon as possible." \ No newline at end of file + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: "Thank you for creating your first issue! We appreciate your contribution and will review it soon. Please ensure you've provided all necessary details and followed the issue template." + pr-message: "Thank you for your first pull request! Before we review, please ensure your code follows our quality standards: run tests, check formatting, and verify linting. We'll review your changes as soon as possible." diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index 3dddb521..00000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,63 +0,0 @@ -name: Test Nixopus API - -on: - push: - branches: - - 'master' - - 'feat/develop' - paths: - - 'api/**' - -jobs: - test: - runs-on: ubuntu-latest - services: - test-db: - image: postgres:14-alpine - env: - POSTGRES_USER: nixopus - POSTGRES_PASSWORD: nixopus - POSTGRES_DB: nixopus_test - ports: - - 5433:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-go@v5 - with: - go-version: '1.22' - check-latest: true - cache: true - cache-dependency-path: api/go.sum - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Wait for PostgreSQL - run: | - for i in {1..10}; do - if pg_isready -h localhost -p 5433 -U nixopus -d nixopus_test; then - echo "PostgreSQL is ready" - exit 0 - fi - echo "Waiting for PostgreSQL... $i" - sleep 2 - done - echo "PostgreSQL failed to start" - exit 1 - - - name: Build and run tests - env: - DB_HOST: localhost - DB_PORT: 5433 - DB_USER: nixopus - DB_PASSWORD: nixopus - DB_NAME: nixopus_test - run: | - cd api && make test \ No newline at end of file diff --git a/api/Makefile b/api/Makefile index ddeb1fe3..07e26be4 100644 --- a/api/Makefile +++ b/api/Makefile @@ -11,6 +11,9 @@ test: test-all: @go test -p 1 ./... -v -count=1 +test-all-ci: + @go test -p 1 ./... -v -count=1 -timeout=15m -failfast + test-routes: @go test -p 1 ./internal/tests/routes/... -v -count=1 diff --git a/api/api/versions.json b/api/api/versions.json index c9a8d8a6..e0d3d1c7 100644 --- a/api/api/versions.json +++ b/api/api/versions.json @@ -3,7 +3,7 @@ { "version": "v1", "status": "active", - "release_date": "2025-07-10T02:54:43.011943+05:30", + "release_date": "2025-07-12T07:32:25.429981+05:30", "end_of_life": "0001-01-01T00:00:00Z", "changes": [ "Initial API version" diff --git a/api/internal/middleware/auth.go b/api/internal/middleware/auth.go index 8b0a23f6..a051cb5d 100644 --- a/api/internal/middleware/auth.go +++ b/api/internal/middleware/auth.go @@ -3,7 +3,9 @@ package middleware import ( "context" "fmt" + "log" "net/http" + "os" "strings" "time" @@ -25,6 +27,10 @@ func AuthMiddleware(next http.Handler, app *storage.App, cache *cache.Cache) htt token := r.Header.Get("Authorization") if token == "" { + //TODO: rmove log for debugging in CI + if os.Getenv("ENV") == "development" { + log.Printf("AUTH_DEBUG: No authorization token provided for %s %s", r.Method, r.URL.Path) + } utils.SendErrorResponse(w, "No authorization token provided", http.StatusUnauthorized) return } @@ -41,12 +47,20 @@ func AuthMiddleware(next http.Handler, app *storage.App, cache *cache.Cache) htt user, err := verifyToken(token, app.Store.DB, ctx, cache) if err != nil { + // TODO: remove logs for debugging in CI + if os.Getenv("ENV") == "development" { + log.Printf("AUTH_DEBUG: Token verification failed for %s %s: %v", r.Method, r.URL.Path, err) + } utils.SendErrorResponse(w, "Invalid authorization token", http.StatusUnauthorized) return } claims, err := getTokenClaims(token) if err != nil { + // TODO: remove after testing + if os.Getenv("ENV") == "development" { + log.Printf("AUTH_DEBUG: Token claims extraction failed for %s %s: %v", r.Method, r.URL.Path, err) + } utils.SendErrorResponse(w, "Invalid token claims", http.StatusUnauthorized) return } @@ -55,6 +69,10 @@ func AuthMiddleware(next http.Handler, app *storage.App, cache *cache.Cache) htt twoFactorVerified, _ := claims["2fa_verified"].(bool) if twoFactorEnabled && !twoFactorVerified && !is2FAVerificationEndpoint(r.URL.Path) { + // TODO: Log for debugging in CI , remvoe after testing + if os.Getenv("ENV") == "development" { + log.Printf("AUTH_DEBUG: 2FA required for %s %s", r.Method, r.URL.Path) + } utils.SendErrorResponse(w, "Two-factor authentication required", http.StatusForbidden) return } @@ -65,6 +83,10 @@ func AuthMiddleware(next http.Handler, app *storage.App, cache *cache.Cache) htt if !isAuthEndpoint(r.URL.Path) { organizationID := r.Header.Get("X-Organization-Id") if organizationID == "" { + // TODO: Log for debugging in CI , remove after testing + if os.Getenv("ENV") == "development" { + log.Printf("AUTH_DEBUG: No organization ID provided for %s %s", r.Method, r.URL.Path) + } utils.SendErrorResponse(w, "No organization ID provided", http.StatusBadRequest) return } @@ -83,11 +105,19 @@ func AuthMiddleware(next http.Handler, app *storage.App, cache *cache.Cache) htt } belongsToOrg, err = userStorage.UserBelongsToOrganization(user.ID.String(), organizationID) if err != nil { + // TODO: remove log for debugging in CI + if os.Getenv("ENV") == "development" { + log.Printf("AUTH_DEBUG: Error verifying org membership for %s %s: user=%s, org=%s, error=%v", r.Method, r.URL.Path, user.ID.String(), organizationID, err) + } utils.SendErrorResponse(w, "Error verifying organization membership", http.StatusInternalServerError) return } if !belongsToOrg { + // Log for debugging in CI + if os.Getenv("ENV") == "development" { + log.Printf("AUTH_DEBUG: User does not belong to organization for %s %s: user=%s, org=%s", r.Method, r.URL.Path, user.ID.String(), organizationID) + } utils.SendErrorResponse(w, "User does not belong to the specified organization", http.StatusForbidden) return } diff --git a/api/internal/tests/container/get_container_logs_test.go b/api/internal/tests/container/get_container_logs_test.go index 067889bc..7c54b446 100644 --- a/api/internal/tests/container/get_container_logs_test.go +++ b/api/internal/tests/container/get_container_logs_test.go @@ -11,7 +11,7 @@ import ( func TestGetContainerLogs(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } diff --git a/api/internal/tests/container/get_container_test.go b/api/internal/tests/container/get_container_test.go index 0d11cc62..ffb8a400 100644 --- a/api/internal/tests/container/get_container_test.go +++ b/api/internal/tests/container/get_container_test.go @@ -11,7 +11,7 @@ import ( func TestGetContainer(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } diff --git a/api/internal/tests/container/list_containers_test.go b/api/internal/tests/container/list_containers_test.go index 093dbe32..f90063c9 100644 --- a/api/internal/tests/container/list_containers_test.go +++ b/api/internal/tests/container/list_containers_test.go @@ -11,7 +11,7 @@ import ( func TestListContainers(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } @@ -94,7 +94,7 @@ func TestListContainers(t *testing.T) { func TestListContainersWithSpecificContainer(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } @@ -117,7 +117,7 @@ func TestListContainersWithSpecificContainer(t *testing.T) { func TestListContainersErrorHandling(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } diff --git a/api/internal/tests/domain/domain_flow_test.go b/api/internal/tests/domain/domain_flow_test.go index c459ccb3..4aa9efe8 100644 --- a/api/internal/tests/domain/domain_flow_test.go +++ b/api/internal/tests/domain/domain_flow_test.go @@ -11,7 +11,7 @@ import ( func TestCreateDomain(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } @@ -130,7 +130,7 @@ func TestCreateDomain(t *testing.T) { func TestGetDomains(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } @@ -228,7 +228,7 @@ func TestGetDomains(t *testing.T) { func TestUpdateDomain(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } @@ -381,7 +381,7 @@ func TestUpdateDomain(t *testing.T) { func TestDeleteDomain(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } @@ -531,7 +531,7 @@ func TestDeleteDomain(t *testing.T) { func TestGenerateRandomSubDomain(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } @@ -623,7 +623,7 @@ func TestGenerateRandomSubDomain(t *testing.T) { func TestDomainsCRUDFlow(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } @@ -724,7 +724,7 @@ func TestDomainsCRUDFlow(t *testing.T) { func TestDomainPermissions(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } @@ -802,7 +802,7 @@ func TestDomainPermissions(t *testing.T) { func TestDomainErrorHandling(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } diff --git a/api/internal/tests/feature-flags/feature_flags_test.go b/api/internal/tests/feature-flags/feature_flags_test.go index 77462e4e..0934117f 100644 --- a/api/internal/tests/feature-flags/feature_flags_test.go +++ b/api/internal/tests/feature-flags/feature_flags_test.go @@ -11,7 +11,7 @@ import ( func TestGetFeatureFlags(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } @@ -104,7 +104,7 @@ func TestGetFeatureFlags(t *testing.T) { func TestUpdateFeatureFlag(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } @@ -312,7 +312,7 @@ func TestUpdateFeatureFlag(t *testing.T) { func TestIsFeatureEnabled(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } @@ -469,7 +469,7 @@ func TestIsFeatureEnabled(t *testing.T) { func TestFeatureFlagsCRUDFlow(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } @@ -560,7 +560,7 @@ func TestFeatureFlagsCRUDFlow(t *testing.T) { func TestFeatureFlagPermissions(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } @@ -612,7 +612,7 @@ func TestFeatureFlagPermissions(t *testing.T) { func TestFeatureFlagErrorHandling(t *testing.T) { setup := testutils.NewTestSetup() - user, org, err := setup.GetTestAuthResponse() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() if err != nil { t.Fatalf("failed to get test auth response: %v", err) } diff --git a/api/internal/tests/helper.go b/api/internal/tests/helper.go index b91115ad..b5e03184 100644 --- a/api/internal/tests/helper.go +++ b/api/internal/tests/helper.go @@ -2,6 +2,8 @@ package tests var baseURL = "http://localhost:8080/api/v1" + + func GetHealthURL() string { return baseURL + "/health" } diff --git a/api/internal/testutils/feature_flag_test.go b/api/internal/testutils/feature_flag_test.go new file mode 100644 index 00000000..dd8b8781 --- /dev/null +++ b/api/internal/testutils/feature_flag_test.go @@ -0,0 +1,35 @@ +package testutils + +import ( + "fmt" + "os" + "testing" +) + +func TestFeatureFlagSetup(t *testing.T) { + // Test the old way (without all features enabled) + t.Run("Standard setup", func(t *testing.T) { + setup := NewTestSetup() + user, org, err := setup.GetTestAuthResponse() + if err != nil { + t.Fatalf("failed to get test auth response: %v", err) + } + + fmt.Printf("Standard setup - User: %s, Org: %s\n", user.User.Email, org.Name) + }) + + // Test the new way (with all features enabled) + t.Run("All features enabled setup", func(t *testing.T) { + setup := NewTestSetup() + user, org, err := setup.GetTestAuthResponseWithAllFeatures() + if err != nil { + t.Fatalf("failed to get test auth response with all features: %v", err) + } + + fmt.Printf("All features enabled setup - User: %s, Org: %s\n", user.User.Email, org.Name) + }) +} + +func TestMain(m *testing.M) { + os.Exit(m.Run()) +} diff --git a/api/internal/testutils/setup.go b/api/internal/testutils/setup.go index d8a99b43..b85efbe1 100644 --- a/api/internal/testutils/setup.go +++ b/api/internal/testutils/setup.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" @@ -14,6 +15,8 @@ import ( authService "github.com/raghavyuva/nixopus-api/internal/features/auth/service" user_storage "github.com/raghavyuva/nixopus-api/internal/features/auth/storage" authTypes "github.com/raghavyuva/nixopus-api/internal/features/auth/types" + feature_flags_service "github.com/raghavyuva/nixopus-api/internal/features/feature-flags/service" + feature_flags_storage "github.com/raghavyuva/nixopus-api/internal/features/feature-flags/storage" "github.com/raghavyuva/nixopus-api/internal/features/logger" organization_service "github.com/raghavyuva/nixopus-api/internal/features/organization/service" organization_storage "github.com/raghavyuva/nixopus-api/internal/features/organization/storage" @@ -119,6 +122,8 @@ func findMigrationsPath() string { "../../../migrations", "../../../../migrations", "migrations", + "../../migrations", + "../migrations", } for _, path := range paths { @@ -171,9 +176,19 @@ func NewTestSetup() *TestSetup { panic("ctx is nil - context not initialized") } - // Clean database before each test - if err := cleanDatabase(); err != nil { - panic(fmt.Sprintf("failed to clean database: %v", err)) + // Note: QUick validation to clean database before each test with retry logic for CI + maxRetries := 3 + var err error + for i := 0; i < maxRetries; i++ { + err = cleanDatabase() + if err == nil { + break + } + fmt.Printf("Database cleanup attempt %d failed: %v, retrying...\n", i+1, err) + time.Sleep(time.Duration(i+1) * time.Second) + } + if err != nil { + panic(fmt.Sprintf("failed to clean database after %d attempts: %v", maxRetries, err)) } l := logger.NewLogger() @@ -184,14 +199,25 @@ func NewTestSetup() *TestSetup { permStorage := &permissions_storage.PermissionStorage{DB: testDB, Ctx: ctx} roleStorage := &role_storage.RoleStorage{DB: testDB, Ctx: ctx} orgStorage := &organization_storage.OrganizationStore{DB: testDB, Ctx: ctx} - cache, err := cache.NewCache(getEnvOrDefault("REDIS_URL", "redis://localhost:6379")) + + // Initialize cache with retry logic + var cacheInstance *cache.Cache + for i := 0; i < maxRetries; i++ { + cacheInstance, err = cache.NewCache(getEnvOrDefault("REDIS_URL", "redis://localhost:6379")) + if err == nil { + break + } + fmt.Printf("Cache connection attempt %d failed: %v, retrying...\n", i+1, err) + time.Sleep(time.Duration(i+1) * time.Second) + } if err != nil { - panic(fmt.Sprintf("failed to create cache: %v", err)) + panic(fmt.Sprintf("failed to create cache after %d attempts: %v", maxRetries, err)) } + // Create services permService := permissions_service.NewPermissionService(store, ctx, l, permStorage) roleService := role_service.NewRoleService(store, ctx, l, roleStorage) - orgService := organization_service.NewOrganizationService(store, ctx, l, orgStorage, cache) + orgService := organization_service.NewOrganizationService(store, ctx, l, orgStorage, cacheInstance) authService := authService.NewAuthService(userStorage, l, permService, roleService, orgService, ctx) return &TestSetup{ @@ -222,7 +248,9 @@ func (s *TestSetup) CreateTestUserAndOrg() (*types.User, *types.Organization, er } func (s *TestSetup) GetTestAuthResponse() (*authTypes.AuthResponse, *types.Organization, error) { - authResponse, org, err := s.RegistrationHelper("test@example.com", "Password123@", "testuser", "test-org", "Test organization", "admin") + // Make organization name unique to avoid conflicts + orgName := fmt.Sprintf("testuser's Team-%s", uuid.New().String()[:8]) + authResponse, org, err := s.RegistrationHelper("test@example.com", "Password123@", "testuser", orgName, "Test organization", "admin") if err != nil { return nil, nil, fmt.Errorf("failed to create test user: %w", err) } @@ -247,8 +275,8 @@ func (s *TestSetup) RegistrationHelper(email, password, username, orgName, orgDe // Create test organization org := &types.Organization{ ID: uuid.New(), - Name: "test-org", - Description: "Test organization", + Name: orgName, + Description: orgDescription, } if err := s.OrgStorage.CreateOrganization(*org); err != nil { @@ -272,5 +300,76 @@ func (s *TestSetup) RegistrationHelper(email, password, username, orgName, orgDe return nil, nil, fmt.Errorf("failed to add user to organization: %w", err) } + // Verify the user was actually added to the organization + belongs, err := s.UserStorage.UserBelongsToOrganization(authResponse.User.ID.String(), org.ID.String()) + if err != nil { + return nil, nil, fmt.Errorf("failed to verify user organization membership: %w", err) + } + if !belongs { + return nil, nil, fmt.Errorf("user was not successfully added to organization - membership verification failed") + } + + s.Logger.Log(logger.Info, "User organization membership verified", fmt.Sprintf("user_id=%s, org_id=%s", authResponse.User.ID.String(), org.ID.String())) + return &authResponse, org, nil } + +// EnableAllFeaturesForOrg to enable all features for a test organization +func (s *TestSetup) EnableAllFeaturesForOrg(orgID uuid.UUID) error { + featureStorage := &feature_flags_storage.FeatureFlagStorage{DB: s.DB, Ctx: s.Ctx} + featureService := feature_flags_service.NewFeatureFlagService(featureStorage, s.Logger, s.Ctx) + + features := []string{ + "terminal", + "container", + "domain", + "file_manager", + "notifications", + "monitoring", + "github_connector", + "audit", + "self_hosted", + "deploy", + } + + // Add retry logic for feature enabling in CI + maxRetries := 3 + for _, feature := range features { + var err error + for i := 0; i < maxRetries; i++ { + req := types.UpdateFeatureFlagRequest{ + FeatureName: feature, + IsEnabled: true, + } + err = featureService.UpdateFeatureFlag(orgID, req) + if err == nil { + break + } + s.Logger.Log(logger.Info, "Feature flag enable retry", fmt.Sprintf("feature=%s, attempt=%d, error=%v", feature, i+1, err)) + time.Sleep(time.Duration(i+1) * 100 * time.Millisecond) + } + if err != nil { + return fmt.Errorf("failed to enable feature %s after %d attempts: %w", feature, maxRetries, err) + } + } + + // Verify features were enabled with a small delay for consistency + time.Sleep(100 * time.Millisecond) + + return nil +} + +// GetTestAuthResponseWithAllFeatures to create a test user and organization with all features enabled +func (s *TestSetup) GetTestAuthResponseWithAllFeatures() (*authTypes.AuthResponse, *types.Organization, error) { + authResponse, org, err := s.GetTestAuthResponse() + if err != nil { + return nil, nil, err + } + + // Enable all features for the test organization + if err := s.EnableAllFeaturesForOrg(org.ID); err != nil { + return nil, nil, fmt.Errorf("failed to enable features for test org: %w", err) + } + + return authResponse, org, nil +} diff --git a/api/main.go b/api/main.go index bce07c6c..934e57f6 100644 --- a/api/main.go +++ b/api/main.go @@ -33,3 +33,4 @@ func main() { log.Printf("Server starting on port %s", config.AppConfig.Port) log.Fatal(http.ListenAndServe(":"+config.AppConfig.Port, nil)) } +