diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..4052297f9c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: + - main + - ci-test + pull_request: + branches: + - main + +jobs: + tests: + name: Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.24 + + - name: Run tests with coverage + run: | + echo "'Tests' job: go test" + go test -cover ./... diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000..b58b603fea --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000000..105ce2da2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/learn-cicd-starter.iml b/.idea/learn-cicd-starter.iml new file mode 100644 index 0000000000..8388dbc88e --- /dev/null +++ b/.idea/learn-cicd-starter.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..812ab5a681 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000..d0adac68cd --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..35eb1ddfbb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index c2bec0368b..854e6efba6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Tests](https://github.com/robaweku-bit/learn-cicd-starter/actions/workflows/ci.yml/badge.svg)](https://github.com/robaweku-bit/learn-cicd-starter/actions) # 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). @@ -21,3 +22,7 @@ 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! + + +ev's Notely app." +"robbieMYNAME's version of Boot.dev's Notely app."app." diff --git a/internal/auth/auth.go b/internal/auth/auth.go index f969aacf63..b86f4693fa 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -6,18 +6,30 @@ import ( "strings" ) -var ErrNoAuthHeaderIncluded = errors.New("no authorization header included") +// GetAPIKey extracts an API key from the Authorization header. +// The expected format is: "Authorization: ApiKey " +func GetAPIKey(r *http.Request) (string, error) { + const prefix = "ApiKey " -// GetAPIKey - -func GetAPIKey(headers http.Header) (string, error) { - authHeader := headers.Get("Authorization") + // Get the Authorization header value + authHeader := r.Header.Get("Authorization") if authHeader == "" { - return "", ErrNoAuthHeaderIncluded + return "", errors.New("authorization header is missing") } - splitAuth := strings.Split(authHeader, " ") - if len(splitAuth) < 2 || splitAuth[0] != "ApiKey" { - return "", errors.New("malformed authorization header") + + // Check for the correct prefix + if !strings.HasPrefix(authHeader, prefix) { + return "", errors.New("authorization header must start with 'ApiKey '") } - return splitAuth[1], nil + // Trim the prefix to get the actual key + apiKey := strings.TrimPrefix(authHeader, prefix) + apiKey = strings.TrimSpace(apiKey) + + if apiKey == "" { + return "", errors.New("API key is missing after prefix") + } + + return apiKey, nil } + diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go new file mode 100644 index 0000000000..dc2a67eeba --- /dev/null +++ b/internal/auth/auth_test.go @@ -0,0 +1,82 @@ +package auth + +import ( + "net/http" + "testing" +) + +func TestGetAPIKey(t *testing.T) { + tests := []struct { + name string + authHeader string + wantKey string + wantErrSubstr string + }{ + { + name: "missing header", + authHeader: "", + wantErrSubstr: "authorization header is missing", + }, + { + name: "wrong prefix", + authHeader: "Bearer abc123", + wantErrSubstr: "must start with 'ApiKey '", + }, + { + name: "empty key after prefix", + authHeader: "ApiKey ", + wantErrSubstr: "API key is missing", + }, + { + name: "Valid key", + authHeader: "ApiKey my-secret-key", + wantKey: "my-secret-key", //correct + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "/", nil) + if tc.authHeader != "" { + req.Header.Set("Authorization", tc.authHeader) + } + + gotKey, err := GetAPIKey(req) + + if tc.wantErrSubstr != "" { + if err == nil || !contains(err.Error(), tc.wantErrSubstr) { + t.Errorf("expected error containing %q, got %v", tc.wantErrSubstr, err) + } + return + } + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if gotKey != tc.wantKey { + t.Errorf("expected key %q, got %q", tc.wantKey, gotKey) + } + }) + } +} + +func contains(s, substr string) bool { + return len(substr) == 0 || (len(s) >= len(substr) && stringContains(s, substr)) +} + +func stringContains(s, substr string) bool { + return len(substr) <= len(s) && (indexOf(s, substr) >= 0) +} + +func indexOf(s, substr string) int { + for i := range s { + if len(s)-i < len(substr) { + return -1 + } + if s[i:i+len(substr)] == substr { + return i + } + } + return -1 +} +// trigger CI diff --git a/middleware_auth.go b/middleware_auth.go index 6cbe03f867..3aa872402f 100644 --- a/middleware_auth.go +++ b/middleware_auth.go @@ -7,22 +7,24 @@ import ( "github.com/bootdotdev/learn-cicd-starter/internal/database" ) -type authedHandler func(http.ResponseWriter, *http.Request, database.User) - -func (cfg *apiConfig) middlewareAuth(handler authedHandler) http.HandlerFunc { +func (cfg *apiConfig) middlewareAuth( + handler func(http.ResponseWriter, *http.Request, database.User), +) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - apiKey, err := auth.GetAPIKey(r.Header) + apiKey, err := auth.GetAPIKey(r) if err != nil { - respondWithError(w, http.StatusUnauthorized, "Couldn't find api key", err) + http.Error(w, err.Error(), http.StatusUnauthorized) return } + // Look up the user in the database user, err := cfg.DB.GetUser(r.Context(), apiKey) if err != nil { - respondWithError(w, http.StatusNotFound, "Couldn't get user", err) + http.Error(w, "invalid API key", http.StatusUnauthorized) return } + // Call the actual handler with the authenticated user handler(w, r, user) } }