Skip to content

Commit 5347e14

Browse files
committed
session 6
1 parent d5183ed commit 5347e14

File tree

15 files changed

+606
-0
lines changed

15 files changed

+606
-0
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,38 @@ Miki Tebeka
299299
- [Secure Code Slides](_extra/secure-go.pdf)
300300
- [journal.tar.gz](_extra/journal.tar.gz)
301301

302+
---
303+
## Session 6: Testing
304+
305+
### Agenda
306+
307+
- Running services
308+
- Mocking and when to avoid them
309+
- Reading test data from files
310+
- Fuzzing
311+
312+
### Code
313+
314+
- [unter](session_6/unter)
315+
316+
### Links
317+
318+
- Linters
319+
- [staticcheck](https://staticcheck.dev/)
320+
- [golangci-lint](https://golangci-lint.run/)
321+
- [x/tools/analysis](https://pkg.go.dev/golang.org/x/tools/go/analysis) - Write your own
322+
- [Getting Started with Fuzzing](https://go.dev/doc/tutorial/fuzz)
323+
- [Testcontainers for Go](https://golang.testcontainers.org/)
324+
- [distroless](https://github.com/GoogleContainerTools/distroless) - Hardened Docker images
325+
- [go-faker](https://github.com/go-faker/faker/blob/main/example_with_tags_test.go) - Generate fake data (email ...)
326+
- [RESP Client for VSCode](https://marketplace.visualstudio.com/items?itemName=humao.rest-client)
327+
328+
329+
### Data & Other
330+
331+
```
332+
¯\_(ツ)_/¯
333+
```
334+
335+
302336
![](https://pixel-73339669570.me-west1.run.app/p/para1/p.png)

session_6/unter/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.db

session_6/unter/Dockerfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
FROM golang:1.25 AS build
2+
WORKDIR /app
3+
COPY go.* ./
4+
RUN go mod download
5+
COPY . .
6+
ENV CGO_ENABLED=0
7+
RUN go build -o unter
8+
9+
FROM debian:trixie-slim
10+
COPY --from=build /app/unter /usr/local/bin
11+
RUN groupadd -r unter && useradd --no-log-init -r -g unter unter
12+
RUN mkdir /opt/unter
13+
RUN chown unter /opt/unter
14+
ENV UNTER_DB=/opt/unter/unter.db
15+
USER unter
16+
CMD ["unter"]

session_6/unter/Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
tag = unter/httpd
2+
3+
build-docker:
4+
docker build -t $(tag) .
5+
6+
7+
run-docker:
8+
docker run --rm -p 8080:8080 $(tag)

session_6/unter/api.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"log/slog"
7+
"net/http"
8+
)
9+
10+
type API struct {
11+
log *slog.Logger
12+
db *DB
13+
}
14+
15+
func (a *API) Health(w http.ResponseWriter, r *http.Request) {
16+
// TODO: Health check
17+
fmt.Fprintln(w, "OK")
18+
}
19+
20+
func (a *API) Add(w http.ResponseWriter, r *http.Request) {
21+
var rd Ride
22+
if err := json.NewDecoder(r.Body).Decode(&rd); err != nil {
23+
a.log.Error("decode", "error", err)
24+
http.Error(w, "bad record", http.StatusBadRequest)
25+
return
26+
}
27+
28+
if err := rd.Validate(); err != nil {
29+
a.log.Error("validate", "error", err)
30+
http.Error(w, "bad record", http.StatusBadRequest)
31+
return
32+
}
33+
34+
if err := a.db.Insert(rd); err != nil {
35+
a.log.Error("insert", "error", err)
36+
http.Error(w, "can't insert", http.StatusInternalServerError)
37+
return
38+
}
39+
a.log.Info("added", "id", rd.ID)
40+
41+
resp := map[string]any{
42+
"id": rd.ID,
43+
}
44+
a.sendJSON(w, resp)
45+
}
46+
47+
func (a *API) Get(w http.ResponseWriter, r *http.Request) {
48+
id := r.PathValue("id")
49+
if id == "" {
50+
http.Error(w, "missing ID", http.StatusBadRequest)
51+
return
52+
}
53+
54+
rd, err := a.db.Get(id)
55+
if err != nil {
56+
a.log.Error("scan", "error", err)
57+
http.Error(w, "can't get rides", http.StatusInternalServerError)
58+
return
59+
}
60+
61+
resp := map[string]any{
62+
"id": rd.ID,
63+
"distance": rd.Distance,
64+
"shared": rd.Shared,
65+
"price": RidePrice(rd.Distance, rd.Shared),
66+
}
67+
a.sendJSON(w, resp)
68+
}
69+
70+
func (a *API) sendJSON(w http.ResponseWriter, resp any) {
71+
w.Header().Set("content-type", "application/json")
72+
if err := json.NewEncoder(w).Encode(resp); err != nil {
73+
a.log.Error("encode", "error", err)
74+
return
75+
}
76+
}

session_6/unter/backoffice.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package main
2+
3+
import "math"
4+
5+
// RidePrice returns ride price in ¢
6+
func RidePrice(distance float64, shared bool) int {
7+
price := 250 // initial fare
8+
price += int(math.Ceil(distance)) * 150
9+
10+
if shared {
11+
price = int(float64(price) * 0.9)
12+
}
13+
14+
return price
15+
}

session_6/unter/client.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/url"
7+
)
8+
9+
type Client struct {
10+
baseURL string
11+
c http.Client
12+
}
13+
14+
func NewClient(baseURL string) Client {
15+
return Client{baseURL: baseURL}
16+
}
17+
18+
func (c *Client) Health() error {
19+
url, err := url.JoinPath(c.baseURL, "/health")
20+
if err != nil {
21+
return err
22+
}
23+
24+
resp, err := c.c.Get(url)
25+
if err != nil {
26+
return err
27+
}
28+
29+
defer resp.Body.Close()
30+
31+
if resp.StatusCode != http.StatusOK {
32+
return fmt.Errorf("%s: bad status - %s", url, resp.Status)
33+
}
34+
35+
return nil
36+
}

session_6/unter/db.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/gob"
6+
"fmt"
7+
8+
"go.etcd.io/bbolt"
9+
)
10+
11+
var (
12+
bucketName = []byte("rides")
13+
)
14+
15+
type DB struct {
16+
conn *bbolt.DB
17+
}
18+
19+
func NewDB(fileName string) (*DB, error) {
20+
conn, err := bbolt.Open(fileName, 0666, nil)
21+
if err != nil {
22+
return nil, err
23+
}
24+
25+
err = conn.Update(func(tx *bbolt.Tx) error {
26+
_, err := tx.CreateBucketIfNotExists(bucketName)
27+
return err
28+
})
29+
30+
if err != nil {
31+
conn.Close()
32+
return nil, err
33+
}
34+
35+
db := DB{conn}
36+
return &db, nil
37+
}
38+
39+
func (db *DB) Close() error {
40+
return db.conn.Close()
41+
}
42+
43+
func (db *DB) Insert(r Ride) error {
44+
var buf bytes.Buffer
45+
if err := gob.NewEncoder(&buf).Encode(r); err != nil {
46+
return err
47+
}
48+
49+
err := db.conn.Update(func(tx *bbolt.Tx) error {
50+
b := tx.Bucket(bucketName)
51+
return b.Put([]byte(r.ID), buf.Bytes())
52+
})
53+
return err
54+
}
55+
56+
func (db *DB) Get(id string) (Ride, error) {
57+
var r Ride
58+
59+
err := db.conn.View(func(tx *bbolt.Tx) error {
60+
b := tx.Bucket(bucketName)
61+
data := b.Get([]byte(id))
62+
if data == nil {
63+
return fmt.Errorf("%q not found", id)
64+
}
65+
66+
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&r); err != nil {
67+
return err
68+
}
69+
70+
return nil
71+
})
72+
73+
if err != nil {
74+
return Ride{}, err
75+
}
76+
77+
return r, nil
78+
}

session_6/unter/go.mod

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
module unter
2+
3+
go 1.25
4+
5+
require (
6+
github.com/goccy/go-yaml v1.19.1
7+
github.com/stretchr/testify v1.11.1
8+
github.com/testcontainers/testcontainers-go v0.40.0
9+
go.etcd.io/bbolt v1.4.3
10+
)
11+
12+
require (
13+
dario.cat/mergo v1.0.2 // indirect
14+
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
15+
github.com/Microsoft/go-winio v0.6.2 // indirect
16+
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
17+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
18+
github.com/containerd/errdefs v1.0.0 // indirect
19+
github.com/containerd/errdefs/pkg v0.3.0 // indirect
20+
github.com/containerd/log v0.1.0 // indirect
21+
github.com/containerd/platforms v0.2.1 // indirect
22+
github.com/cpuguy83/dockercfg v0.3.2 // indirect
23+
github.com/davecgh/go-spew v1.1.1 // indirect
24+
github.com/distribution/reference v0.6.0 // indirect
25+
github.com/docker/docker v28.5.1+incompatible // indirect
26+
github.com/docker/go-connections v0.6.0 // indirect
27+
github.com/docker/go-units v0.5.0 // indirect
28+
github.com/ebitengine/purego v0.8.4 // indirect
29+
github.com/felixge/httpsnoop v1.0.4 // indirect
30+
github.com/go-logr/logr v1.4.3 // indirect
31+
github.com/go-logr/stdr v1.2.2 // indirect
32+
github.com/go-ole/go-ole v1.2.6 // indirect
33+
github.com/google/uuid v1.6.0 // indirect
34+
github.com/klauspost/compress v1.18.0 // indirect
35+
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
36+
github.com/magiconair/properties v1.8.10 // indirect
37+
github.com/moby/docker-image-spec v1.3.1 // indirect
38+
github.com/moby/go-archive v0.1.0 // indirect
39+
github.com/moby/patternmatcher v0.6.0 // indirect
40+
github.com/moby/sys/sequential v0.6.0 // indirect
41+
github.com/moby/sys/user v0.4.0 // indirect
42+
github.com/moby/sys/userns v0.1.0 // indirect
43+
github.com/moby/term v0.5.0 // indirect
44+
github.com/morikuni/aec v1.0.0 // indirect
45+
github.com/opencontainers/go-digest v1.0.0 // indirect
46+
github.com/opencontainers/image-spec v1.1.1 // indirect
47+
github.com/pkg/errors v0.9.1 // indirect
48+
github.com/pmezard/go-difflib v1.0.0 // indirect
49+
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
50+
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
51+
github.com/sirupsen/logrus v1.9.3 // indirect
52+
github.com/tklauser/go-sysconf v0.3.12 // indirect
53+
github.com/tklauser/numcpus v0.6.1 // indirect
54+
github.com/yusufpapurcu/wmi v1.2.4 // indirect
55+
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
56+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
57+
go.opentelemetry.io/otel v1.39.0 // indirect
58+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
59+
go.opentelemetry.io/otel/metric v1.39.0 // indirect
60+
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
61+
go.opentelemetry.io/otel/trace v1.39.0 // indirect
62+
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
63+
golang.org/x/crypto v0.43.0 // indirect
64+
golang.org/x/sys v0.39.0 // indirect
65+
google.golang.org/protobuf v1.36.10 // indirect
66+
gopkg.in/yaml.v3 v3.0.1 // indirect
67+
)

0 commit comments

Comments
 (0)