diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 94b9f16..fb1be15 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -12,6 +12,7 @@ jobs: runs-on: ubuntu-22.04 permissions: contents: write + pull-requests: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} steps: @@ -24,6 +25,80 @@ jobs: with: node-version: '20' + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Run server tests with coverage + id: server_tests + run: | + echo "Running server tests..." + if make server-tests > server-tests-output.txt 2>&1; then + echo "status=success" >> $GITHUB_OUTPUT + echo "Tests passed ✅" >> $GITHUB_STEP_SUMMARY + else + echo "status=failure" >> $GITHUB_OUTPUT + echo "Tests failed ❌" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Extract coverage percentage + COVERAGE=$(grep "total:" server-tests-output.txt | awk '{print $3}') + echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT + + # Display full output + cat server-tests-output.txt + + # Display coverage summary + echo "## Coverage Report" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + grep -A 100 "go tool cover" server-tests-output.txt | tail -n +2 >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: | + server/coverage.out + server-tests-output.txt + retention-days: 30 + + - name: Comment on PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const output = fs.readFileSync('server-tests-output.txt', 'utf8'); + + // Extract test results + const coverageMatch = output.match(/total:.*?\((.*?)\)\s+(\d+\.\d+%)/); + const coverage = coverageMatch ? coverageMatch[2] : 'N/A'; + const status = '${{ steps.server_tests.outputs.status }}'; + const statusEmoji = status === 'success' ? '✅' : '❌'; + + // Get coverage details + const coverageDetails = output.split('go tool cover -func=coverage.out')[1] || ''; + + const statusText = status === 'success' ? 'PASSED' : 'FAILED'; + + const body = "## Server Tests Report " + statusEmoji + "\n\n" + + "**Status:** " + statusText + "\n" + + "**Coverage:** " + coverage + "\n\n" + + "### Coverage Details\n```\n" + coverageDetails.trim() + "\n```\n\n" + + "
\nFull Test Output\n\n```\n" + output + "\n```\n
"; + + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + }); + - name: Install dependencies and build run: | npm ci diff --git a/.gitignore b/.gitignore index a547bf3..aa872c7 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,9 @@ dist-ssr *.njsproj *.sln *.sw? + +# Go coverage files +*.out + +# Test output files +server-tests-output.txt diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0e557c7 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +.PHONY: server-tests +server-tests: + @echo "Running server tests with coverage..." + cd server && go test -v -coverprofile=coverage.out -covermode=atomic ./... + cd server && go tool cover -func=coverage.out diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..76bcdda --- /dev/null +++ b/server/go.mod @@ -0,0 +1,3 @@ +module github.com/simihablo/simihablo.github.io/server + +go 1.24.11 diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..f49c4b3 --- /dev/null +++ b/server/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" +) + +// HealthResponse represents the health check response +type HealthResponse struct { + Status string `json:"status"` + Message string `json:"message"` +} + +// HealthHandler handles health check requests +func HealthHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + response := HealthResponse{ + Status: "ok", + Message: "Server is running", + } + json.NewEncoder(w).Encode(response) +} + +// GreetingHandler handles greeting requests +func GreetingHandler(w http.ResponseWriter, r *http.Request) { + name := r.URL.Query().Get("name") + if name == "" { + name = "World" + } + + w.Header().Set("Content-Type", "application/json") + response := map[string]string{ + "greeting": fmt.Sprintf("Hola, %s!", name), + } + json.NewEncoder(w).Encode(response) +} + +func main() { + http.HandleFunc("/health", HealthHandler) + http.HandleFunc("/greet", GreetingHandler) + + fmt.Println("Server starting on :8080") + if err := http.ListenAndServe(":8080", nil); err != nil { + fmt.Printf("Server failed to start: %v\n", err) + } +} diff --git a/server/main_test.go b/server/main_test.go new file mode 100644 index 0000000..3f49e46 --- /dev/null +++ b/server/main_test.go @@ -0,0 +1,93 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHealthHandler(t *testing.T) { + req, err := http.NewRequest("GET", "/health", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(HealthHandler) + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + var response HealthResponse + if err := json.NewDecoder(rr.Body).Decode(&response); err != nil { + t.Fatalf("Failed to decode response: %v", err) + } + + if response.Status != "ok" { + t.Errorf("handler returned unexpected status: got %v want %v", + response.Status, "ok") + } + + if response.Message != "Server is running" { + t.Errorf("handler returned unexpected message: got %v want %v", + response.Message, "Server is running") + } +} + +func TestGreetingHandler(t *testing.T) { + tests := []struct { + name string + queryParam string + expectedGreet string + }{ + { + name: "with name parameter", + queryParam: "?name=Juan", + expectedGreet: "Hola, Juan!", + }, + { + name: "without name parameter", + queryParam: "", + expectedGreet: "Hola, World!", + }, + { + name: "with Spanish name", + queryParam: "?name=María", + expectedGreet: "Hola, María!", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, err := http.NewRequest("GET", "/greet"+tt.queryParam, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(GreetingHandler) + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + var response map[string]string + if err := json.NewDecoder(rr.Body).Decode(&response); err != nil { + t.Fatalf("Failed to decode response: %v", err) + } + + if greeting, ok := response["greeting"]; !ok { + t.Error("response missing 'greeting' field") + } else if greeting != tt.expectedGreet { + t.Errorf("handler returned unexpected greeting: got %v want %v", + greeting, tt.expectedGreet) + } + }) + } +}