Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1d5a376
update
Gemelovg Jun 25, 2026
e2b2bb7
added new worflow
Gemelovg Jun 25, 2026
412e582
Update ci.yml
Gemelovg Jun 25, 2026
68803db
new tests
Jun 27, 2026
c88a99c
fixed issue
Jun 27, 2026
d1d176a
cover flag added
Jun 27, 2026
6048599
added badge
Jun 27, 2026
191058c
Add style workflow file
Gemelovg Jun 28, 2026
0b4c5e8
Merge branch 'addtests' of github.com:Gemelovg/learn-cicd-starter int…
Gemelovg Jun 28, 2026
636b27f
updated ci.yml
Gemelovg Jun 28, 2026
af4d616
Update handler_user.go
Gemelovg Jun 28, 2026
0978845
changed name of job
Gemelovg Jun 28, 2026
8247645
change identation
Gemelovg Jun 28, 2026
5182fc7
change identation
Gemelovg Jun 28, 2026
0d339b4
update
Gemelovg Jun 28, 2026
27a2978
updted syle.yml
Gemelovg Jun 28, 2026
29a56b6
update stirngs
Gemelovg Jun 28, 2026
5128ba1
updated file name
Gemelovg Jun 28, 2026
ac4bfbe
added job
Gemelovg Jun 28, 2026
ed2e9f7
added staticcheck
Gemelovg Jun 30, 2026
ccd7c17
updated unused function
Gemelovg Jun 30, 2026
ecabd6d
updated ci.yml
Gemelovg Jul 1, 2026
c1fde9b
updated methods
Gemelovg Jul 1, 2026
e34d15e
adde func
Gemelovg Jul 1, 2026
961465f
new workflow
Gemelovg Jul 6, 2026
3bb9990
updated wf
Gemelovg Jul 6, 2026
07b304c
updated ci.yml
Gemelovg Jul 7, 2026
88aa36d
update
Gemelovg Jul 7, 2026
b01d626
update2
Gemelovg Jul 7, 2026
b09e237
updated stuff
Gemelovg Jul 9, 2026
e098cce
updated ci
Gemelovg Jul 9, 2026
7b64d1a
updated cd
Gemelovg Jul 9, 2026
f5498e1
updated ci.yml
Jul 12, 2026
b6af258
udpated ci.yml
Jul 12, 2026
463fa8a
com,mit
Jul 12, 2026
896c215
move env
Jul 12, 2026
0e9fefe
update ci.yml
Jul 12, 2026
03493aa
updated cdyml install goose
Jul 12, 2026
ba433c5
updated cd
Jul 12, 2026
ba35c76
uopdatec ccd
Jul 12, 2026
c74f418
updated cd
Jul 12, 2026
17ca0bb
Update index.html
Jul 16, 2026
f132bf4
Update cd.yml
Jul 16, 2026
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
43 changes: 43 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: cd

on:
push:
branches: [main]

jobs:
Deploy:
runs-on: ubuntu-latest
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version-file: "go.mod"

- name: Install goose
run: go install github.com/pressly/goose/v3/cmd/goose@latest

- name: Build application
run: bash scripts/buildprod.sh

- id: "auth"
uses: "google-github-actions/auth@v2"
with:
credentials_json: "${{ secrets.GCP_CREDENTIALS }}"

- name: "Set up Cloud SDK"
uses: "google-github-actions/setup-gcloud@v3"

- name: Build and Push Docker Image
run: gcloud builds submit --tag us-central1-docker.pkg.dev/notely-502219/notely-ar-repo/notaly:latest .

- name: run migrations
run: bash scripts/migrateup.sh

- name: Deploy to Cloud Run
run: gcloud run deploy notely --image us-central1-docker.pkg.dev/notely-502219/notely-ar-repo/notaly:latest --region us-central1 --allow-unauthenticated --project notely-502219 --max-instances=4
55 changes: 55 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: ci

on:
pull_request:
branches: [main]

jobs:
tests:
name: Tests
runs-on: ubuntu-latest

steps:
- name: Check out code
uses: actions/checkout@v6

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.26.0"

- name: install goose
run: go install github.com/pressly/goose/v3/cmd/goose@latest

- name: Force Failure
run: go test -cover ./...

- name: Style
run: test -z $(go fmt ./...)

- name: Install gosec
run: go install github.com/securego/gosec/v2/cmd/gosec@latest

- name: Run gosec
run: gosec ./...

style:
name: Style
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "stable"

- name: Check formatting
run: test -z "$(go fmt ./...)"

- name: Install staticcheck
run: go install honnef.co/go/tools/cmd/staticcheck@latest

- name: Run staticcheck
run: staticcheck ./...
22 changes: 22 additions & 0 deletions .github/workflows/style.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Style

on:
pull_request:
branches: [main]

jobs:
tests:
name: Tests
runs-on: ubuntu-latest

steps:
- name: Check out code
uses: actions/checkout@v6

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.26.0"

- name: Style
run: test -z $(go fmt ./...)
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
![alt text goes here](https://github.com/Gemelovg/learn-cicd-starter/actions/workflows/ci.yml/badge.svg)
# learn-cicd-starter (Notely)

This repo contains the starter code for the "Notely" application for the "Learn CICD" course on [Boot.dev](https://boot.dev).
Expand All @@ -21,3 +22,4 @@ go build -o notely && ./notely
*This starts the server in non-database mode.* It will serve a simple webpage at `http://localhost:8080`.

You do *not* need to set up a database or any interactivity on the webpage yet. Instructions for that will come later in the course!
MYNAME's version of Boot.dev's Notely app.
114 changes: 114 additions & 0 deletions internal/auth/get_api_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package auth

import (
"net/http"
"testing"
)

func TestGetAPIKey(t *testing.T) {
t.Run("returns error when no authorization header", func(t *testing.T) {
headers := http.Header{}

key, err := GetAPIKey(headers)

if err != ErrNoAuthHeaderIncluded {
t.Errorf("expected ErrNoAuthHeaderIncluded, got %v", err)
}
if key != "" {
t.Errorf("expected empty key, got %q", key)
}
})

t.Run("returns error for empty authorization header", func(t *testing.T) {
headers := http.Header{
"Authorization": []string{""},
}

key, err := GetAPIKey(headers)

if err == nil {
t.Fatal("expected error for empty header, got nil")
}
if key != "" {
t.Errorf("expected empty key, got %q", key)
}
})

t.Run("returns error for header without 'ApiKey' scheme", func(t *testing.T) {
headers := http.Header{
"Authorization": []string{"Bearer token123"},
}

key, err := GetAPIKey(headers)

if err == nil {
t.Fatal("expected error for Bearer scheme, got nil")
}
if key != "" {
t.Errorf("expected empty key, got %q", key)
}
})

t.Run("returns error for malformed header with only one part", func(t *testing.T) {
headers := http.Header{
"Authorization": []string{"ApiKey"},
}

key, err := GetAPIKey(headers)

if err == nil {
t.Fatal("expected error for missing key portion, got nil")
}
if key != "" {
t.Errorf("expected empty key, got %q", key)
}
})

t.Run("successfully extracts API key from valid header", func(t *testing.T) {
expectedKey := "my-secret-api-key-12345"
headers := http.Header{
"Authorization": []string{"ApiKey " + expectedKey},
}

key, err := GetAPIKey(headers)

if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if key != expectedKey {
t.Errorf("expected %q, got %q", expectedKey, key)
}
})

t.Run("extra whitespace causes issues with Split - known limitation", func(t *testing.T) {
headers := http.Header{
"Authorization": []string{"ApiKey api-key"}, // Two spaces
}

key, err := GetAPIKey(headers)

// Current Split behavior: ["ApiKey", "", "api-key"], so splitAuth[1] = ""
if err == nil && key == "" {
// Documented quirk - production code needs fixing for robust parsing
return
}
if err != nil {
t.Errorf("got error %v, expected none or empty key", err)
}
})

t.Run("header case-sensitivity check - lowercase apischeme fails", func(t *testing.T) {
headers := http.Header{
"Authorization": []string{"apikey secret123"},
}

key, err := GetAPIKey(headers)

if err == nil {
t.Fatal("expected error for lowercase 'apikey', got nil")
}
if key != "" {
t.Errorf("expected empty key, got %q", key)
}
})
}
6 changes: 5 additions & 1 deletion json.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,9 @@ func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
return
}
w.WriteHeader(code)
w.Write(dat)

_, err = w.Write(dat)
if err != nil {
log.Printf("warning: failed to write response: %v", err)
}
}
24 changes: 21 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"log"
"net/http"
"os"
"strings"
"time"

"github.com/go-chi/chi"
"github.com/go-chi/cors"
Expand All @@ -24,6 +26,18 @@ type apiConfig struct {
//go:embed static/*
var staticFiles embed.FS

func sanitizeLogString(input string) string {
s := strings.ReplaceAll(input, "\r", "\\r")
s = strings.ReplaceAll(s, "\n", "\\n")
result := ""
for _, ch := range s {
if ch >= 32 || ch == '\t' {
result += string(ch)
}
}
return result
}

func main() {
err := godotenv.Load(".env")
if err != nil {
Expand Down Expand Up @@ -89,10 +103,14 @@ func main() {

router.Mount("/v1", v1Router)
srv := &http.Server{
Addr: ":" + port,
Handler: router,
Addr: ":" + port,
Handler: router,
ReadHeaderTimeout: 5 * time.Second, // Prevents Slowloris attacks
ReadTimeout: 10 * time.Second, // Prevents slow readers
WriteTimeout: 30 * time.Second, // Limits write duration
IdleTimeout: 60 * time.Second, // Closes idle connections
}

log.Printf("Serving on port: %s\n", port)
log.Printf("Serving on port: %s\n", sanitizeLogString(port))
log.Fatal(srv.ListenAndServe())
}
2 changes: 2 additions & 0 deletions sql/queries/notes.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ SELECT * FROM notes WHERE id = ?;
-- name: GetNotesForUser :many
SELECT * FROM notes WHERE user_id = ?;
--
--
__
Loading