Skip to content

Commit 47b1406

Browse files
committed
session 6
1 parent 4ac239b commit 47b1406

File tree

12 files changed

+342
-0
lines changed

12 files changed

+342
-0
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,5 +286,35 @@ Miki Tebeka
286286
- [Secure Code Slides](_extra/secure-go.pdf)
287287
- [journal.tar.gz](_extra/journal.tar.gz)
288288

289+
---
290+
## Session 6: Testing
291+
292+
### Agenda
293+
294+
- Running services
295+
- Mocking and when to avoid them
296+
- Reading test data from files
297+
- Fuzzing
298+
299+
### Code
300+
301+
TBD
302+
303+
### Links
304+
305+
- Linters
306+
- [staticcheck](https://staticcheck.dev/)
307+
- [golangci-lint](https://golangci-lint.run/)
308+
- [x/tools/analysis](https://pkg.go.dev/golang.org/x/tools/go/analysis) - Write your own
309+
- [Getting Started with Fuzzing](https://go.dev/doc/tutorial/fuzz)
310+
- [Testcontainers for Go](https://golang.testcontainers.org/)
311+
312+
313+
### Data & Other
314+
315+
```
316+
¯\_(ツ)_/¯
317+
```
318+
289319

290320
![](https://pixel-73339669570.me-west1.run.app/p/para2/p.png)

unter/.gitignore

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

unter/Makefile

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

unter/api.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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) Get(w http.ResponseWriter, r *http.Request) {
21+
id := r.PathValue("id")
22+
if id == "" {
23+
http.Error(w, "missing ID", http.StatusBadRequest)
24+
return
25+
}
26+
27+
rd, err := a.db.Get(id)
28+
if err != nil {
29+
a.log.Error("scan", "error", err)
30+
http.Error(w, "can't get rides", http.StatusInternalServerError)
31+
return
32+
}
33+
34+
w.Header().Set("content-type", "application/json")
35+
if err := json.NewEncoder(w).Encode(rd); err != nil {
36+
a.log.Error("encode", "error", err)
37+
return
38+
}
39+
}
40+
41+
func (a *API) Add(w http.ResponseWriter, r *http.Request) {
42+
var rd Ride
43+
if err := json.NewDecoder(r.Body).Decode(&rd); err != nil {
44+
a.log.Error("decode", "error", err)
45+
http.Error(w, "bad record", http.StatusBadRequest)
46+
return
47+
}
48+
49+
if err := rd.Validate(); err != nil {
50+
a.log.Error("validate", "error", err)
51+
http.Error(w, "bad record", http.StatusBadRequest)
52+
return
53+
}
54+
55+
if err := a.db.Insert(rd); err != nil {
56+
a.log.Error("insert", "error", err)
57+
http.Error(w, "can't insert", http.StatusInternalServerError)
58+
return
59+
}
60+
61+
json.NewEncoder(w).Encode(map[string]any{
62+
"id": rd.ID,
63+
})
64+
}

unter/auth.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package main
2+
3+
type User struct {
4+
Login string
5+
}
6+
7+
type Auth struct {
8+
}
9+
10+
func NewAuth() *Auth {
11+
a := Auth{}
12+
return &a
13+
}
14+
15+
func (a *Auth) Login(user, passwd string) (User, bool) {
16+
if user == "joe" && passwd == "baz00ka" {
17+
return User{"joe"}, true
18+
}
19+
20+
return User{}, false
21+
}

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+
}

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+
}

unter/go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module unter
2+
3+
go 1.25
4+
5+
require go.etcd.io/bbolt v1.4.3
6+
7+
require (
8+
github.com/stretchr/testify v1.11.1 // indirect
9+
golang.org/x/sys v0.39.0 // indirect
10+
)

unter/go.sum

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
6+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
7+
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
8+
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
9+
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
10+
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
11+
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
12+
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
13+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
14+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

unter/main.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
"net/http"
7+
"os"
8+
)
9+
10+
func main() {
11+
log := slog.Default().With("app", "cars")
12+
dbFile := os.Getenv("CARS_DB")
13+
if dbFile == "" {
14+
dbFile = "cars.db"
15+
}
16+
addr := os.Getenv("CARS_ADDR")
17+
if addr == "" {
18+
addr = ":8080"
19+
}
20+
21+
db, err := NewDB(dbFile)
22+
if err != nil {
23+
fmt.Fprintf(os.Stderr, "error: can't create DB (%s)\n", err)
24+
os.Exit(1)
25+
}
26+
27+
api := API{
28+
log: log,
29+
db: db,
30+
}
31+
mux := http.NewServeMux()
32+
mux.HandleFunc("GET /health", api.Health)
33+
mux.HandleFunc("POST /rides", api.Add)
34+
mux.HandleFunc("GET /ride/{id}", api.Get)
35+
36+
srv := http.Server{
37+
Addr: addr,
38+
Handler: mux,
39+
}
40+
log.Info("server starting", "address", srv.Addr)
41+
if err := srv.ListenAndServe(); err != nil {
42+
log.Error("serve", "error", err)
43+
os.Exit(1)
44+
}
45+
}

0 commit comments

Comments
 (0)