package integration import ( "bytes" "encoding/base64" "encoding/json" "fmt" "net/http" "os" "os/exec" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // ComposeIntegrationTest runs integration tests against a containerized instance func TestComposeIntegration(t *testing.T) { if os.Getenv("RUN_COMPOSE_TESTS") != "true" { t.Skip("Skipping compose integration tests. Set RUN_COMPOSE_TESTS=true to run") } composeCmd := detectComposeCommand(t) projectName := "dyn-test-" + fmt.Sprintf("%d", time.Now().Unix()) // Cleanup function cleanup := func() { t.Log("Cleaning up containers...") cmd := exec.Command(composeCmd, "-f", "../../docker-compose.yml", "-p", projectName, "down", "-v") cmd.Run() os.Remove(".env.test") } defer cleanup() // Create test environment envContent := `SERVER_PORT=8080 DATABASE_PATH=/data/dyn.db TECHNITIUM_URL=http://mock-dns:8080 TECHNITIUM_TOKEN=test-token BASE_DOMAIN=test.rip SPACE_SUBDOMAIN=space RATE_LIMIT_PER_IP=100 RATE_LIMIT_PER_TOKEN=100 ` err := os.WriteFile(".env.test", []byte(envContent), 0644) require.NoError(t, err) // Start services t.Log("Starting services with", composeCmd) cmd := exec.Command(composeCmd, "-f", "../../docker-compose.yml", "-p", projectName, "--env-file", ".env.test", "up", "-d", "--build") output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("Failed to start services: %v\nOutput: %s", err, output) } // Wait for service to be ready baseURL := waitForService(t, "http://localhost:8080", 60) t.Logf("Service ready at %s", baseURL) // Run tests t.Run("HealthCheck", func(t *testing.T) { testHealthCheck(t, baseURL) }) t.Run("CheckSubdomain", func(t *testing.T) { testCheckSubdomain(t, baseURL) }) t.Run("ClaimSpace", func(t *testing.T) { testClaimSpace(t, baseURL) }) t.Run("ProfanityFilter", func(t *testing.T) { testProfanityFilterCompose(t, baseURL) }) t.Run("CustomFilter", func(t *testing.T) { testCustomFilterCompose(t, baseURL) }) t.Run("DynDNSEndpoint", func(t *testing.T) { testDynDNSEndpoint(t, baseURL) }) } func detectComposeCommand(t *testing.T) string { // Try podman-compose first if _, err := exec.LookPath("podman-compose"); err == nil { t.Log("Using podman-compose") return "podman-compose" } // Try docker-compose if _, err := exec.LookPath("docker-compose"); err == nil { t.Log("Using docker-compose") return "docker-compose" } t.Skip("Neither podman-compose nor docker-compose found") return "" } func waitForService(t *testing.T, url string, timeout int) string { client := &http.Client{Timeout: 2 * time.Second} for i := 0; i < timeout; i++ { resp, err := client.Get(url) if err == nil && resp.StatusCode == http.StatusOK { resp.Body.Close() return url } if resp != nil { resp.Body.Close() } time.Sleep(1 * time.Second) } t.Fatalf("Service did not become ready within %d seconds", timeout) return "" } func testHealthCheck(t *testing.T, baseURL string) { resp, err := http.Get(baseURL + "/") require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) body := new(bytes.Buffer) body.ReadFrom(resp.Body) assert.Contains(t, body.String(), "DWS Dynamic DNS") } func testCheckSubdomain(t *testing.T, baseURL string) { // Test available subdomain resp, err := http.Get(baseURL + "/api/check?subdomain=newspace") require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) var result map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) assert.True(t, result["available"].(bool)) } func testClaimSpace(t *testing.T, baseURL string) string { payload := map[string]string{"subdomain": "testclaim"} body, _ := json.Marshal(payload) resp, err := http.Post(baseURL+"/api/claim", "application/json", bytes.NewBuffer(body)) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, http.StatusCreated, resp.StatusCode) var result map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) token := result["token"].(string) assert.NotEmpty(t, token) assert.Equal(t, "testclaim", result["subdomain"]) return token } func testProfanityFilterCompose(t *testing.T, baseURL string) { profaneWords := []string{"fuck", "shit", "bitch"} for _, word := range profaneWords { t.Run(word, func(t *testing.T) { payload := map[string]string{"subdomain": word} body, _ := json.Marshal(payload) resp, err := http.Post(baseURL+"/api/claim", "application/json", bytes.NewBuffer(body)) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, http.StatusBadRequest, resp.StatusCode) var result map[string]string json.NewDecoder(resp.Body).Decode(&result) assert.Contains(t, result["error"], "inappropriate") }) } } func testCustomFilterCompose(t *testing.T, baseURL string) { reservedWords := []string{"dws", "dubey", "tanishq"} for _, word := range reservedWords { t.Run(word, func(t *testing.T) { payload := map[string]string{"subdomain": word} body, _ := json.Marshal(payload) resp, err := http.Post(baseURL+"/api/claim", "application/json", bytes.NewBuffer(body)) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, http.StatusBadRequest, resp.StatusCode) var result map[string]string json.NewDecoder(resp.Body).Decode(&result) assert.Contains(t, result["error"], "reserved") }) } } func testDynDNSEndpoint(t *testing.T, baseURL string) { // First claim a space token := testClaimSpace(t, baseURL) // Try DynDNS update client := &http.Client{Timeout: 5 * time.Second} req, err := http.NewRequest("GET", baseURL+"/api/nic/update?hostname=testclaim.space.test.rip&myip=192.168.1.100", nil) require.NoError(t, err) req.Header.Set("Authorization", "Basic "+basicAuth("none", token)) resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() // We expect 200 or 503 (since there's no real DNS server) assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusServiceUnavailable) } func basicAuth(username, password string) string { auth := username + ":" + password return base64.StdEncoding.EncodeToString([]byte(auth)) }