Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .github/workflows/static.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
runs-on: ubuntu-22.04
permissions:
contents: write
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
steps:
Expand All @@ -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" +
"<details>\n<summary>Full Test Output</summary>\n\n```\n" + output + "\n```\n</details>";


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
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,9 @@ dist-ssr
*.njsproj
*.sln
*.sw?

# Go coverage files
*.out

# Test output files
server-tests-output.txt
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions server/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/simihablo/simihablo.github.io/server

go 1.24.11
47 changes: 47 additions & 0 deletions server/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
93 changes: 93 additions & 0 deletions server/main_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}