Skip to content
Merged
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
25 changes: 14 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
FROM alpine:3.12
# Add Maintainer Info
LABEL maintainer="Nofar Bluestein <nofarb@gmail.com>"
# Copy the Go binary into the image. The Go binary must be
# statically compiled with CGO disabled. Use the following
# build command:
#
# CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags netgo
#
ADD go-sample-app /bin/
# Command to run the executable
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o go-sample-app

# Runtime stage
FROM alpine:3.19
LABEL maintainer="Harness Demo <demo@harness.io>"
WORKDIR /bin
COPY --from=builder /app/go-sample-app .
EXPOSE 8080
ENTRYPOINT ["/bin/go-sample-app"]
160 changes: 141 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,150 @@
# HelloWorldGoServer
GO + Docker + unit tests

A simple Go HTTP server demo for Harness CI/CD integration. Features graceful shutdown, query parameter handling, and comprehensive test coverage.

## Running the app locally
## Prerequisites

- Go 1.21 or higher
- Docker (optional, for containerization)

## Building

### Local Build
```bash
go build -o go-sample-app
```

### Docker Build
```bash
docker build -t go-sample-app:latest .
```

The multi-stage Dockerfile optimizes layer caching for faster builds:
- **Build stage**: Uses `golang:1.21-alpine` to compile
- **Runtime stage**: Minimal `alpine:3.19` image (~15MB)

## Running the App

### Locally
```bash
./go-sample-app
# Output: Starting Server
```

### With Docker
```bash
docker run -p 8080:8080 go-sample-app:latest
```

### Testing Requests
```bash
curl http://localhost:8080?name=Alice
# Output: Hello, Alice

curl http://localhost:8080
# Output: Hello, Guest
```

## Running Tests

### Quick Test Run
```bash
go test -v
```

### Generate JUnit XML Report
```bash
go install github.com/jstemmer/go-junit-report/v2@latest
go test -v 2>&1 | go-junit-report -set-exit-code > test-results.xml
```

### Generate Coverage Reports

#### HTML Coverage Report
```bash
$ go build
$ ./go-sample-app
2019/02/03 11:38:11 Starting Server
go test -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html
```

#### LCOV Format (for CI integrations)
```bash
$ curl http://localhost:8080?name=Nofar
Hello, Nofar
Test1
```
go install github.com/jandelgado/gcov2lcov@latest
go test -coverprofile=coverage.out
gcov2lcov -infile=coverage.out -outfile=coverage.lcov
```

### Combined: Tests + JUnit + Coverage
```bash
go test -v -coverprofile=coverage.out 2>&1 | tee test-output.log
go-junit-report -set-exit-code -in test-output.log -out test-results.xml
gcov2lcov -infile=coverage.out -outfile=coverage.lcov
```

## Test Files

- **hello_server_test.go**: Original tests for greeting functionality
- **utils_test.go**: String utility and format tests
- **handlers_test.go**: HTTP handler tests with test recorder
- **validation_test.go**: Input validation and UTF-8 compliance tests
- **edge_cases_test.go**: Tests for special characters, long names, whitespace
- **integration_test.go**: Multi-request and query parsing tests

## Harness CI Integration

Example `.harness/` pipeline stages:

```yaml
# Build
- step:
type: Run
spec:
command: go build -o go-sample-app

# Test with JUnit
- step:
type: Run
spec:
command: |
go install github.com/jstemmer/go-junit-report/v2@latest
go test -v 2>&1 | go-junit-report -set-exit-code > test-results.xml
reports:
type: JUnit
spec:
paths:
- test-results.xml

# Coverage with LCOV
- step:
type: Run
spec:
command: |
go install github.com/jandelgado/gcov2lcov@latest
go test -coverprofile=coverage.out
gcov2lcov -infile=coverage.out -outfile=coverage.lcov
reports:
type: Codecov
spec:
paths:
- coverage.lcov

# Docker Build
- step:
type: BuildAndPush
spec:
dockerfile: Dockerfile
repo: <your-registry>/go-sample-app
tags:
- latest
- <+pipeline.executionId>
```

## Architecture

- **Server**: Gorilla Mux router, graceful shutdown on SIGINT/SIGTERM
- **Handler**: Query parameter parsing with default value ("Guest")
- **Logging**: Standard library logging
- **Timeout**: 10s read/write timeouts

## License











MIT
38 changes: 38 additions & 0 deletions edge_cases_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"strings"
"testing"
)

func TestEdgeCaseLongName(t *testing.T) {
longName := strings.Repeat("A", 1000)
greeting := CreateGreeting(longName)
if !strings.Contains(greeting, longName) {
t.Error("greeting should contain long name")
}
}

func TestEdgeCaseSpecialCharacters(t *testing.T) {
names := []string{"O'Brien", "Jean-Paul", "María", "李明", "😀"}
for _, name := range names {
greeting := CreateGreeting(name)
if !strings.Contains(greeting, name) {
t.Errorf("greeting should contain %q", name)
}
}
}

func TestEdgeCaseWhitespace(t *testing.T) {
greeting := CreateGreeting("\t\n ")
if len(greeting) == 0 {
t.Error("greeting with whitespace should not be empty")
}
}

func TestEdgeCaseNumbers(t *testing.T) {
greeting := CreateGreeting("123")
if !strings.Contains(greeting, "123") {
t.Error("greeting should contain numbers")
}
}
44 changes: 44 additions & 0 deletions handlers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)

func TestHandlerWithName(t *testing.T) {
req := httptest.NewRequest("GET", "/?name=Alice", nil)
w := httptest.NewRecorder()

handler(w, req)

resp := w.Result()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
}

func TestHandlerWithoutName(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()

handler(w, req)

resp := w.Result()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
}

func TestHandlerResponseBody(t *testing.T) {
req := httptest.NewRequest("GET", "/?name=TestUser", nil)
w := httptest.NewRecorder()

handler(w, req)

body := w.Body.String()
if !strings.Contains(body, "TestUser") {
t.Errorf("expected body to contain 'TestUser', got: %s", body)
}
}
66 changes: 66 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package main

import (
"net/http"
"net/http/httptest"
"testing"
)

func TestIntegrationMultipleRequests(t *testing.T) {
requests := []string{"/?name=User1", "/?name=User2", "/"}

for _, reqPath := range requests {
req := httptest.NewRequest("GET", reqPath, nil)
w := httptest.NewRecorder()

handler(w, req)

if w.Code != http.StatusOK {
t.Errorf("request %s returned %d, expected 200", reqPath, w.Code)
}
}
}

func TestIntegrationQueryParsing(t *testing.T) {
tests := []struct {
path string
hasName bool
contains string
}{
{"/?name=TestA", true, "TestA"},
{"/?name=", false, "Guest"},
{"/", false, "Guest"},
}

for _, tt := range tests {
req := httptest.NewRequest("GET", tt.path, nil)
w := httptest.NewRecorder()

handler(w, req)

body := w.Body.String()
if !contains(body, tt.contains) {
t.Errorf("path %s: expected %q in %q", tt.path, tt.contains, body)
}
}
}

func TestIntegrationHTTPMethods(t *testing.T) {
req := httptest.NewRequest("GET", "/?name=Test", nil)
w := httptest.NewRecorder()

handler(w, req)

if w.Code != http.StatusOK {
t.Errorf("GET request failed with status %d", w.Code)
}
}

func contains(s, substr string) bool {
for i := 0; i < len(s)-len(substr)+1; i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
36 changes: 36 additions & 0 deletions utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package main

import (
"strings"
"testing"
)

func TestStringUtils(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"empty", "", "Hello, Guest\n"},
{"with spaces", " Alice ", "Hello, Alice \n"},
{"special chars", "Bob@123", "Hello, Bob@123\n"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := CreateGreeting(tt.input)
if strings.TrimSpace(result) != strings.TrimSpace(tt.expected) {
t.Errorf("got %q, want %q", result, tt.expected)
}
})
}
}

func TestGreetingFormat(t *testing.T) {
greeting := CreateGreeting("Test")
if !strings.HasPrefix(greeting, "Hello,") {
t.Errorf("greeting should start with 'Hello,', got: %s", greeting)
}
if !strings.HasSuffix(greeting, "\n") {
t.Errorf("greeting should end with newline, got: %s", greeting)
}
}
Loading
Loading