diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..9fd206cb36 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + tests: + name: Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Run tests + run: go test ./... --cover + + - name: Install gosec + run: go install github.com/securego/gosec/v2/cmd/gosec@latest + + - name: Run gosec security scan + run: gosec ./... \ No newline at end of file diff --git a/README.md b/README.md index c2bec0368b..133458ba21 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,17 @@ -# learn-cicd-starter (Notely) +[![CI Status](https://github.com/sadiaaschrafi/learn-cicd-starter/actions/workflows/ci.yml/badge.svg)](https://github.com/sadiaaschrafi/learn-cicd-starter/actions) -This repo contains the starter code for the "Notely" application for the "Learn CICD" course on [Boot.dev](https://boot.dev). +# learn-cicd-starter -## Local Development +This is a starter project for learning CI/CD concepts. -Make sure you're on Go version 1.22+. +## Features +- Automated testing with Go +- CI/CD pipeline with GitHub Actions +- Code formatting checks -Create a `.env` file in the root of the project with the following contents: +## Getting Started +1. Clone the repository +2. Run `go mod tidy` +3. Run `go test ./...` to run tests -```bash -PORT="8080" -``` - -Run the server: - -```bash -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! +sadiaaschrafi's version of Boot.dev's Notely app. diff --git a/handler_notes.go b/handler_notes.go index 85a8e3415d..d53d4316fb 100644 --- a/handler_notes.go +++ b/handler_notes.go @@ -1,6 +1,9 @@ package main import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" "encoding/json" "net/http" "time" @@ -9,25 +12,9 @@ import ( "github.com/google/uuid" ) -func (cfg *apiConfig) handlerNotesGet(w http.ResponseWriter, r *http.Request, user database.User) { - posts, err := cfg.DB.GetNotesForUser(r.Context(), user.ID) - if err != nil { - respondWithError(w, http.StatusInternalServerError, "Couldn't get posts for user", err) - return - } - - postsResp, err := databasePostsToPosts(posts) - if err != nil { - respondWithError(w, http.StatusInternalServerError, "Couldn't convert posts", err) - return - } - - respondWithJSON(w, http.StatusOK, postsResp) -} - -func (cfg *apiConfig) handlerNotesCreate(w http.ResponseWriter, r *http.Request, user database.User) { +func (cfg *apiConfig) handlerUsersCreate(w http.ResponseWriter, r *http.Request) { type parameters struct { - Note string `json:"note"` + Name string `json:"name"` } decoder := json.NewDecoder(r.Body) params := parameters{} @@ -37,30 +24,56 @@ func (cfg *apiConfig) handlerNotesCreate(w http.ResponseWriter, r *http.Request, return } - id := uuid.New().String() - err = cfg.DB.CreateNote(r.Context(), database.CreateNoteParams{ - ID: id, + apiKey, err := generateRandomSHA256Hash() + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't gen apikey", err) + return + } + + err = cfg.DB.CreateUser(r.Context(), database.CreateUserParams{ + ID: uuid.New().String(), CreatedAt: time.Now().UTC().Format(time.RFC3339), UpdatedAt: time.Now().UTC().Format(time.RFC3339), - Note: params.Note, - UserID: user.ID, + Name: params.Name, + ApiKey: apiKey, }) if err != nil { - respondWithError(w, http.StatusInternalServerError, "Couldn't create note", err) + respondWithError(w, http.StatusInternalServerError, "Couldn't create user", err) + return + } + + user, err := cfg.DB.GetUser(r.Context(), apiKey) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't get user", err) return } - note, err := cfg.DB.GetNote(r.Context(), id) + userResp, err := databaseUserToUser(user) if err != nil { - respondWithError(w, http.StatusNotFound, "Couldn't get note", err) + respondWithError(w, http.StatusInternalServerError, "Couldn't convert user", err) return } + respondWithJSON(w, http.StatusCreated, userResp) +} + +func generateRandomSHA256Hash() (string, error) { + randomBytes := make([]byte, 32) + _, err := rand.Read(randomBytes) + if err != nil { + return "", err + } + hash := sha256.Sum256(randomBytes) + hashString := hex.EncodeToString(hash[:]) + return hashString, nil +} + +func (cfg *apiConfig) handlerUsersGet(w http.ResponseWriter, r *http.Request, user database.User) { - noteResp, err := databaseNoteToNote(note) + userResp, err := databaseUserToUser(user) if err != nil { - respondWithError(w, http.StatusInternalServerError, "Couldn't convert note", err) + respondWithError(w, http.StatusInternalServerError, "Couldn't convert user", err) return } - respondWithJSON(w, http.StatusCreated, noteResp) + respondWithJSON(w, http.StatusOK, userResp) } diff --git a/handler_user.go b/handler_user.go index d53d4316fb..b207254805 100644 --- a/handler_user.go +++ b/handler_user.go @@ -1,79 +1,37 @@ package main import ( - "crypto/rand" - "crypto/sha256" - "encoding/hex" "encoding/json" "net/http" - "time" "github.com/bootdotdev/learn-cicd-starter/internal/database" - "github.com/google/uuid" ) func (cfg *apiConfig) handlerUsersCreate(w http.ResponseWriter, r *http.Request) { type parameters struct { - Name string `json:"name"` + Email string `json:"email"` + Password string `json:"password"` } + decoder := json.NewDecoder(r.Body) params := parameters{} - err := decoder.Decode(¶ms) - if err != nil { - respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters", err) + if err := decoder.Decode(¶ms); err != nil { + respondWithError(w, http.StatusBadRequest, "invalid JSON body") return } - apiKey, err := generateRandomSHA256Hash() - if err != nil { - respondWithError(w, http.StatusInternalServerError, "Couldn't gen apikey", err) - return - } - - err = cfg.DB.CreateUser(r.Context(), database.CreateUserParams{ - ID: uuid.New().String(), - CreatedAt: time.Now().UTC().Format(time.RFC3339), - UpdatedAt: time.Now().UTC().Format(time.RFC3339), - Name: params.Name, - ApiKey: apiKey, + user := cfg.DB.CreateUser(r.Context(), database.CreateUserParams{ + Email: params.Email, + HashedPassword: params.Password, }) - if err != nil { - respondWithError(w, http.StatusInternalServerError, "Couldn't create user", err) - return - } - - user, err := cfg.DB.GetUser(r.Context(), apiKey) - if err != nil { - respondWithError(w, http.StatusInternalServerError, "Couldn't get user", err) - return - } - userResp, err := databaseUserToUser(user) - if err != nil { - respondWithError(w, http.StatusInternalServerError, "Couldn't convert user", err) - return - } - respondWithJSON(w, http.StatusCreated, userResp) -} - -func generateRandomSHA256Hash() (string, error) { - randomBytes := make([]byte, 32) - _, err := rand.Read(randomBytes) - if err != nil { - return "", err - } - hash := sha256.Sum256(randomBytes) - hashString := hex.EncodeToString(hash[:]) - return hashString, nil + respondWithJSON(w, http.StatusCreated, user) } -func (cfg *apiConfig) handlerUsersGet(w http.ResponseWriter, r *http.Request, user database.User) { - - userResp, err := databaseUserToUser(user) - if err != nil { - respondWithError(w, http.StatusInternalServerError, "Couldn't convert user", err) - return - } - - respondWithJSON(w, http.StatusOK, userResp) +func (cfg *apiConfig) handlerUsersGet( + w http.ResponseWriter, + r *http.Request, + user database.User, +) { + respondWithJSON(w, http.StatusOK, user) } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go new file mode 100644 index 0000000000..e0583ae322 --- /dev/null +++ b/internal/auth/auth_test.go @@ -0,0 +1,38 @@ +package auth + +import ( + "net/http" + "testing" +) + +func TestGetAPIKey(t *testing.T) { + t.Run("returns API key when header is valid", func(t *testing.T) { + headers := http.Header{} + headers.Set("Authorization", "ApiKey test123") + + key, err := GetAPIKey(headers) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if key != "test123" { + t.Errorf("Expected 'test123', got %s", key) + } + }) + + t.Run("returns error when no authorization header", func(t *testing.T) { + headers := http.Header{} + + key, err := GetAPIKey(headers) + + if err == nil { + t.Error("Expected error, got nil") + } + if err != ErrNoAuthHeaderIncluded { + t.Errorf("Expected ErrNoAuthHeaderIncluded, got %v", err) + } + if key != "" { + t.Errorf("Expected empty key, got %s", key) + } + }) +} diff --git a/internal/database/db.go b/internal/database/db.go index 61f5bf46c8..85d4b8c654 100644 --- a/internal/database/db.go +++ b/internal/database/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.30.0 package database diff --git a/internal/database/models.go b/internal/database/models.go index 70333ba1ab..834aabbb89 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -1,11 +1,9 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.30.0 package database -import () - type Note struct { ID string CreatedAt string diff --git a/internal/database/notes.sql.go b/internal/database/notes.sql.go index 234dd4c131..3dd01b0c66 100644 --- a/internal/database/notes.sql.go +++ b/internal/database/notes.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.30.0 // source: notes.sql package database diff --git a/internal/database/users.sql.go b/internal/database/users.sql.go index 737b0d1d5f..4bfb5cde12 100644 --- a/internal/database/users.sql.go +++ b/internal/database/users.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.30.0 // source: users.sql package database @@ -26,6 +26,8 @@ type CreateUserParams struct { UpdatedAt string Name string ApiKey string + Email string + HashedPassword string } func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) error { diff --git a/json.go b/json.go index 1e6e7985e1..8296efca95 100644 --- a/json.go +++ b/json.go @@ -6,16 +6,11 @@ import ( "net/http" ) -func respondWithError(w http.ResponseWriter, code int, msg string, logErr error) { - if logErr != nil { - log.Println(logErr) - } - if code > 499 { - log.Printf("Responding with 5XX error: %s", msg) - } - type errorResponse struct { - Error string `json:"error"` - } +type errorResponse struct { + Error string `json:"error"` +} + +func respondWithError(w http.ResponseWriter, code int, msg string) { respondWithJSON(w, code, errorResponse{ Error: msg, }) @@ -23,12 +18,16 @@ func respondWithError(w http.ResponseWriter, code int, msg string, logErr error) func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { w.Header().Set("Content-Type", "application/json") - dat, err := json.Marshal(payload) + + data, err := json.Marshal(payload) if err != nil { log.Printf("Error marshalling JSON: %s", err) - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) return } + w.WriteHeader(code) - w.Write(dat) + if _, err := w.Write(data); err != nil { + log.Println("error writing response:", err) + } } diff --git a/main.go b/main.go index 19d7366c5f..15ee531c5d 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "log" "net/http" "os" + "time" "github.com/go-chi/chi" "github.com/go-chi/cors" @@ -89,8 +90,9 @@ func main() { router.Mount("/v1", v1Router) srv := &http.Server{ - Addr: ":" + port, - Handler: router, + Addr: ":" + port, + Handler: router, + ReadHeaderTimeout: 5 * time.Second, } log.Printf("Serving on port: %s\n", port)