From 133c94bfb9e7645df5c1abd60dbead93ef7a02f3 Mon Sep 17 00:00:00 2001 From: Tanishq Dubey Date: Mon, 2 Feb 2026 21:27:10 -0500 Subject: [PATCH] Add podman-compose/docker-compose integration tests --- Makefile | 58 +++++ README.md | 21 ++ tests/integration/compose_integration_test.go | 234 ++++++++++++++++++ tests/integration/compose_test.sh | 159 ++++++++++++ 4 files changed, 472 insertions(+) create mode 100644 Makefile create mode 100644 tests/integration/compose_integration_test.go create mode 100755 tests/integration/compose_test.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e328ffa --- /dev/null +++ b/Makefile @@ -0,0 +1,58 @@ +.PHONY: build test test-unit test-integration test-compose run clean docker-build docker-push help + +# Variables +BINARY_NAME=dyn-server +DOCKER_IMAGE=git.dws.rip/DWS/dyn +VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") + +help: ## Show this help message + @echo "Available targets:" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' + +build: ## Build the binary + go build -o $(BINARY_NAME) cmd/server/main.go + +test: ## Run all tests + go test ./... -v + +test-unit: ## Run unit tests only + go test ./internal/... -v + +test-integration: ## Run integration tests (with mock server) + go test ./tests/integration -v -run 'TestIntegration_[^C]' + +test-compose: ## Run containerized integration tests with podman/docker-compose + @echo "Running compose integration tests..." + bash tests/integration/compose_test.sh + +test-compose-go: ## Run Go-based compose integration tests + RUN_COMPOSE_TESTS=true go test ./tests/integration -v -run TestComposeIntegration -timeout 120s + +run: build ## Build and run the server + ./$(BINARY_NAME) + +dev: ## Run in development mode + go run cmd/server/main.go + +docker-build: ## Build Docker image + docker build -t $(DOCKER_IMAGE):$(VERSION) -t $(DOCKER_IMAGE):latest . + +docker-push: docker-build ## Push Docker image + docker push $(DOCKER_IMAGE):$(VERSION) + docker push $(DOCKER_IMAGE):latest + +clean: ## Clean build artifacts + rm -f $(BINARY_NAME) + go clean + +deps: ## Download dependencies + go mod download + go mod tidy + +lint: ## Run linter (requires golangci-lint) + golangci-lint run ./... + +fmt: ## Format Go code + go fmt ./... + +.DEFAULT_GOAL := help diff --git a/README.md b/README.md index 1264d1c..6e2178a 100644 --- a/README.md +++ b/README.md @@ -288,6 +288,27 @@ go test ./tests/integration -v -run TestIntegration_FullWorkflow # Run with timeout go test ./tests/integration -v -timeout 30s + +# Run containerized integration tests with podman/docker-compose +make test-compose + +# Or using the shell script directly +bash tests/integration/compose_test.sh + +# Run Go-based compose tests (requires RUN_COMPOSE_TESTS=true) +RUN_COMPOSE_TESTS=true go test ./tests/integration -v -run TestComposeIntegration +``` + +### Using Make +```bash +make help # Show all available targets +make build # Build the binary +make test # Run all tests +make test-unit # Run unit tests only +make test-integration # Run integration tests +make test-compose # Run containerized tests +make run # Build and run the server +make dev # Run in development mode ``` ## License diff --git a/tests/integration/compose_integration_test.go b/tests/integration/compose_integration_test.go new file mode 100644 index 0000000..7e39f8e --- /dev/null +++ b/tests/integration/compose_integration_test.go @@ -0,0 +1,234 @@ +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)) +} diff --git a/tests/integration/compose_test.sh b/tests/integration/compose_test.sh new file mode 100755 index 0000000..5dcae54 --- /dev/null +++ b/tests/integration/compose_test.sh @@ -0,0 +1,159 @@ +#!/bin/bash +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +COMPOSE_FILE="docker-compose.yml" +TEST_TIMEOUT=60 +CONTAINER_NAME="dyn-ddns-test" + +echo -e "${YELLOW}=== DDNS Container Integration Test ===${NC}" + +# Detect compose command +if command -v podman-compose &> /dev/null; then + COMPOSE_CMD="podman-compose" + echo -e "${GREEN}Using podman-compose${NC}" +elif command -v docker-compose &> /dev/null; then + COMPOSE_CMD="docker-compose" + echo -e "${GREEN}Using docker-compose${NC}" +else + echo -e "${RED}Error: Neither podman-compose nor docker-compose found${NC}" + exit 1 +fi + +# Cleanup function +cleanup() { + echo -e "${YELLOW}Cleaning up...${NC}" + $COMPOSE_CMD -f $COMPOSE_FILE -p $CONTAINER_NAME down -v 2>/dev/null || true + rm -f .env.test +} + +# Set trap for cleanup on exit +trap cleanup EXIT + +# Create test environment file +cat > .env.test << EOF +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 +EOF + +echo -e "${YELLOW}Starting services...${NC}" +$COMPOSE_CMD -f $COMPOSE_FILE -p $CONTAINER_NAME --env-file .env.test up -d --build + +# Wait for services to be ready +echo -e "${YELLOW}Waiting for services to start (timeout: ${TEST_TIMEOUT}s)...${NC}" +for i in $(seq 1 $TEST_TIMEOUT); do + if curl -s http://localhost:8080/ > /dev/null 2>&1; then + echo -e "${GREEN}Services are ready!${NC}" + break + fi + if [ $i -eq $TEST_TIMEOUT ]; then + echo -e "${RED}Timeout waiting for services${NC}" + exit 1 + fi + sleep 1 +done + +echo -e "${YELLOW}Running integration tests...${NC}" + +# Test 1: Health check +echo -n "Test 1: Health check... " +if curl -s http://localhost:8080/ | grep -q "DWS Dynamic DNS"; then + echo -e "${GREEN}PASS${NC}" +else + echo -e "${RED}FAIL${NC}" + exit 1 +fi + +# Test 2: Check subdomain availability +echo -n "Test 2: Check subdomain availability... " +RESPONSE=$(curl -s "http://localhost:8080/api/check?subdomain=testspace") +if echo "$RESPONSE" | grep -q '"available":true'; then + echo -e "${GREEN}PASS${NC}" +else + echo -e "${RED}FAIL${NC} - Response: $RESPONSE" + exit 1 +fi + +# Test 3: Claim space +echo -n "Test 3: Claim space... " +RESPONSE=$(curl -s -X POST http://localhost:8080/api/claim \ + -H "Content-Type: application/json" \ + -d '{"subdomain":"mytest"}') +if echo "$RESPONSE" | grep -q '"token"'; then + TOKEN=$(echo "$RESPONSE" | grep -o '"token":"[^"]*"' | cut -d'"' -f4) + echo -e "${GREEN}PASS${NC} (token: ${TOKEN:0:20}...)" +else + echo -e "${RED}FAIL${NC} - Response: $RESPONSE" + exit 1 +fi + +# Test 4: Check claimed subdomain is not available +echo -n "Test 4: Check claimed subdomain unavailable... " +RESPONSE=$(curl -s "http://localhost:8080/api/check?subdomain=mytest") +if echo "$RESPONSE" | grep -q '"available":false'; then + echo -e "${GREEN}PASS${NC}" +else + echo -e "${RED}FAIL${NC} - Response: $RESPONSE" + exit 1 +fi + +# Test 5: DynDNS update (mocked - will fail DNS but test auth) +echo -n "Test 5: DynDNS update endpoint... " +RESPONSE=$(curl -s -u "none:$TOKEN" \ + "http://localhost:8080/api/nic/update?hostname=mytest.space.test.rip&myip=192.168.1.100") +# We expect 911 since there's no real DNS server, but auth should work +if [ $? -eq 0 ]; then + echo -e "${GREEN}PASS${NC} (endpoint reachable)" +else + echo -e "${RED}FAIL${NC}" + exit 1 +fi + +# Test 6: Profanity filter +echo -n "Test 6: Profanity filter... " +RESPONSE=$(curl -s -X POST http://localhost:8080/api/claim \ + -H "Content-Type: application/json" \ + -d '{"subdomain":"fuck"}') +if echo "$RESPONSE" | grep -q 'inappropriate'; then + echo -e "${GREEN}PASS${NC}" +else + echo -e "${RED}FAIL${NC} - Response: $RESPONSE" + exit 1 +fi + +# Test 7: Custom filter (DWS terms) +echo -n "Test 7: Custom filter (DWS terms)... " +RESPONSE=$(curl -s -X POST http://localhost:8080/api/claim \ + -H "Content-Type: application/json" \ + -d '{"subdomain":"dws"}') +if echo "$RESPONSE" | grep -q 'reserved'; then + echo -e "${GREEN}PASS${NC}" +else + echo -e "${RED}FAIL${NC} - Response: $RESPONSE" + exit 1 +fi + +# Test 8: Invalid subdomain format +echo -n "Test 8: Invalid subdomain format... " +RESPONSE=$(curl -s -X POST http://localhost:8080/api/claim \ + -H "Content-Type: application/json" \ + -d '{"subdomain":"ab"}') +if echo "$RESPONSE" | grep -q 'Invalid'; then + echo -e "${GREEN}PASS${NC}" +else + echo -e "${RED}FAIL${NC} - Response: $RESPONSE" + exit 1 +fi + +echo -e "${GREEN}=== All tests passed! ===${NC}"