Skip to content
Open
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
61 changes: 61 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: cd

on:
push:
branches: [addtests]

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4

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

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

deploy:
runs-on: ubuntu-latest
needs: build
steps:
- name: Check out code
uses: actions/checkout@v4

- name: Install goose
run: |
curl -fsSL https://raw.githubusercontent.com/pressly/goose/master/install.sh | sh

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

- name: Build binary
run: ./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: "Use gcloud CLI"
run: "gcloud info"

- name: Push to GCP
run: gcloud builds submit --tag us-central1-docker.pkg.dev/notely-471713/notely-ar-repo/notely:latest .

- name: Run database migrations
run: ./scripts/migrateup.sh
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}

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

on:
pull_request:
branches: [main]

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

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

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

- name: Run unit tests
run: go test ./... --cover

- 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: Check out code
uses: actions/checkout@v4

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

- name: Install and run staticcheck
run: |
go install honnef.co/go/tools/cmd/staticcheck@latest
export PATH="$(go env GOPATH)/bin:$PATH"
staticcheck ./...
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ out
.env
learn-cicd-starter
notely
scripts/gcloud.sh
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
![Tests:](https://github.com/Yusu-f/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 +23,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!
Ishola's version of Boot.dev's Notely app
177 changes: 177 additions & 0 deletions internal/auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package auth

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

func TestGetAPIKey(t *testing.T) {
tests := []struct {
name string
headers map[string]string
expectedAPIKey string
expectedError error
}{
{
name: "valid API key",
headers: map[string]string{"Authorization": "ApiKey valid-api-key-123"},
expectedAPIKey: "valid-api-key-123",
expectedError: nil,
},
{
name: "valid API key with extra spaces",
headers: map[string]string{"Authorization": "ApiKey another-valid-key"},
expectedAPIKey: "", // strings.Split returns empty string for the second element
expectedError: nil,
},
{
name: "valid API key with special characters",
headers: map[string]string{"Authorization": "ApiKey key-with-special-chars!@#$%"},
expectedAPIKey: "key-with-special-chars!@#$%",
expectedError: nil,
},
{
name: "valid API key with multiple parts",
headers: map[string]string{"Authorization": "ApiKey key with multiple parts"},
expectedAPIKey: "key", // strings.Split takes only the second element
expectedError: nil,
},
{
name: "missing authorization header",
headers: map[string]string{},
expectedAPIKey: "",
expectedError: ErrNoAuthHeaderIncluded,
},
{
name: "empty authorization header",
headers: map[string]string{"Authorization": ""},
expectedAPIKey: "",
expectedError: ErrNoAuthHeaderIncluded,
},
{
name: "malformed header - wrong prefix",
headers: map[string]string{"Authorization": "Bearer some-token"},
expectedAPIKey: "",
expectedError: errors.New("malformed authorization header"),
},
{
name: "malformed header - wrong prefix case",
headers: map[string]string{"Authorization": "apikey some-key"},
expectedAPIKey: "",
expectedError: errors.New("malformed authorization header"),
},
{
name: "malformed header - no space",
headers: map[string]string{"Authorization": "ApiKey"},
expectedAPIKey: "",
expectedError: errors.New("malformed authorization header"),
},
{
name: "malformed header - only ApiKey with space",
headers: map[string]string{"Authorization": "ApiKey "},
expectedAPIKey: "", // strings.Split returns empty string for the second element
expectedError: nil,
},
{
name: "malformed header - multiple spaces before key",
headers: map[string]string{"Authorization": "ApiKey "},
expectedAPIKey: "", // strings.Split returns empty string for the second element
expectedError: nil,
},
{
name: "case insensitive authorization header key",
headers: map[string]string{"authorization": "ApiKey some-key"},
expectedAPIKey: "some-key",
expectedError: nil,
},
{
name: "mixed case authorization header key",
headers: map[string]string{"AUTHORIZATION": "ApiKey some-key"},
expectedAPIKey: "some-key",
expectedError: nil,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create headers from the test case
headers := make(http.Header)
for key, value := range tt.headers {
headers.Set(key, value)
}

// Call the function
apiKey, err := GetAPIKey(headers)

// Check the API key
if apiKey != tt.expectedAPIKey {
t.Errorf("GetAPIKey() apiKey = %v, want %v", apiKey, tt.expectedAPIKey)
}

// Check the error
if tt.expectedError == nil {
if err != nil {
t.Errorf("GetAPIKey() error = %v, want nil", err)
}
} else {
if err == nil {
t.Errorf("GetAPIKey() error = nil, want %v", tt.expectedError)
} else if err.Error() != tt.expectedError.Error() {
t.Errorf("GetAPIKey() error = %v, want %v", err, tt.expectedError)
}
}
})
}
}

func TestGetAPIKey_EdgeCases(t *testing.T) {
t.Run("nil headers", func(t *testing.T) {
// This shouldn't happen in practice, but let's test it
var headers http.Header
apiKey, err := GetAPIKey(headers)

if apiKey != "" {
t.Errorf("GetAPIKey() with nil headers apiKey = %v, want empty string", apiKey)
}
if err != ErrNoAuthHeaderIncluded {
t.Errorf("GetAPIKey() with nil headers error = %v, want %v", err, ErrNoAuthHeaderIncluded)
}
})

t.Run("very long API key", func(t *testing.T) {
longKey := "a" + string(make([]byte, 1000)) // 1001 character key
headers := make(http.Header)
headers.Set("Authorization", "ApiKey "+longKey)

apiKey, err := GetAPIKey(headers)

if err != nil {
t.Errorf("GetAPIKey() with long key error = %v, want nil", err)
}
if apiKey != longKey {
t.Errorf("GetAPIKey() with long key length = %d, want %d", len(apiKey), len(longKey))
}
})
}

// Benchmark tests
func BenchmarkGetAPIKey_Valid(b *testing.B) {
headers := make(http.Header)
headers.Set("Authorization", "ApiKey test-api-key")

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = GetAPIKey(headers)
}
}

func BenchmarkGetAPIKey_Invalid(b *testing.B) {
headers := make(http.Header)
headers.Set("Authorization", "Bearer test-token")

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = GetAPIKey(headers)
}
}
4 changes: 3 additions & 1 deletion json.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@ func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
return
}
w.WriteHeader(code)
w.Write(dat)
if _, err := w.Write(dat); err != nil {
log.Printf("Error writing response: %s", err)
}
}
6 changes: 4 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"log"
"net/http"
"os"
"time"

"github.com/go-chi/chi"
"github.com/go-chi/cors"
Expand Down Expand Up @@ -89,8 +90,9 @@ func main() {

router.Mount("/v1", v1Router)
srv := &http.Server{
Addr: ":" + port,
Handler: router,
Addr: ":" + port,
Handler: router,
ReadHeaderTimeout: 60 * time.Second,
}

log.Printf("Serving on port: %s\n", port)
Expand Down
6 changes: 3 additions & 3 deletions static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@
</head>

<body class="section">
<h1>Notely</h1>
<h1>Welcome to Notely</h1>

<div id="userCreationContainer" class="section">
<input id="nameField" type="text" placeholder="Enter your name">
<button id="createUserButton" onclick="createUser()">Create User</button>
<button id="createUserButton" onclick="createUser()">Create new user, Daddy</button>
</div>

<div id="noteSection" class="section" style="display: none;">
<p id="greetingMessage"></p>

<textarea id="newNoteContent"></textarea>
<button id="createNoteButton" onclick="createNote()">Create Note</button>
<button id="createNoteButton" onclick="createNote()">Create new note, Daddy</button>

<h2>Your Notes</h2>
<div id="notes"></div>
Expand Down