├── README.md ├── part-1 ├── cmd │ └── main.go ├── go.mod └── go.sum ├── part-2 ├── cmd │ └── main.go ├── go.mod ├── go.sum └── internal │ ├── handlers │ ├── createtodo.go │ ├── gettodos.go │ └── healthcheck.go │ └── store │ └── store.go └── part-3 ├── cmd └── main.go ├── go.mod ├── go.sum ├── internal └── handlers │ ├── createshorturl.go │ ├── createshorturl_test.go │ ├── getshorturl.go │ ├── getshorturl_test.go │ └── healthcheck.go └── store ├── dbstore └── shorturl.go ├── mock └── mock.go └── store.go /README.md: -------------------------------------------------------------------------------- 1 | # Go for TypeScript developers 2 | 3 | ## What you will learn 4 | * The basics of the Go language 5 | * How to structure a Go project 6 | * How to write basic unit tests for Go code 7 | 8 | ## Why learn Go? 9 | * It's fun 10 | * Fast 11 | * Easy to learn (difficult to master) 12 | * Great paying jobs 13 | 14 | 15 | ## Video structure 16 | 1. Overview of a basic TODO application 17 | 2. Refactor the TODO application 18 | 3. Build & test a URL shortener -------------------------------------------------------------------------------- /part-1/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/go-chi/chi/v5" 14 | "github.com/go-chi/chi/v5/middleware" 15 | ) 16 | 17 | type Todo struct { 18 | Title string `json:"title"` 19 | Done bool `json:"done"` 20 | } 21 | 22 | func main() { 23 | 24 | todos := []Todo{} 25 | 26 | r := chi.NewRouter() 27 | 28 | // Use chi's logger and recover middlewares for better error handling 29 | r.Use(middleware.Logger) 30 | 31 | r.Get("/healthcheck", func(w http.ResponseWriter, r *http.Request) { 32 | w.Write([]byte("OK")) 33 | }) 34 | 35 | r.Post("/todos", func(w http.ResponseWriter, r *http.Request) { 36 | todo := Todo{} 37 | err := json.NewDecoder(r.Body).Decode(&todo) 38 | 39 | if err != nil { 40 | w.WriteHeader(http.StatusBadRequest) 41 | return 42 | } 43 | 44 | todos = append(todos, todo) 45 | 46 | w.WriteHeader(http.StatusCreated) 47 | }) 48 | 49 | r.Get("/todos", func(w http.ResponseWriter, r *http.Request) { 50 | 51 | w.Header().Set("Content-Type", "application/json") 52 | err := json.NewEncoder(w).Encode(todos) 53 | 54 | if err != nil { 55 | w.WriteHeader(http.StatusInternalServerError) 56 | return 57 | } 58 | }) 59 | 60 | srv := &http.Server{ 61 | Addr: ":8080", 62 | Handler: r, 63 | } 64 | 65 | // Create a channel to listen for OS signals 66 | sigCh := make(chan os.Signal, 1) 67 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 68 | 69 | go func() { 70 | fmt.Println("Server is running on :8080") 71 | 72 | if err := srv.ListenAndServe(); err != nil { 73 | fmt.Printf("Error: %v\n", err) 74 | } 75 | }() 76 | 77 | fmt.Println("Press Ctrl+C to stop the server") 78 | 79 | // Wait for signals to gracefully shut down the server 80 | <-sigCh 81 | 82 | fmt.Println("Shutting down the server...") 83 | 84 | // Create a context with a timeout for the graceful shutdown 85 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 86 | defer cancel() 87 | 88 | if err := srv.Shutdown(ctx); err != nil { 89 | fmt.Printf("Error: %v\n", err) 90 | } 91 | 92 | fmt.Println("Server gracefully stopped") 93 | } 94 | -------------------------------------------------------------------------------- /part-1/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tomdoestech/go-for-ts-devs 2 | 3 | go 1.21.3 4 | 5 | require github.com/go-chi/chi/v5 v5.0.10 // indirect 6 | -------------------------------------------------------------------------------- /part-1/go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= 2 | github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 3 | -------------------------------------------------------------------------------- /part-2/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/go-chi/chi/v5" 13 | "github.com/go-chi/chi/v5/middleware" 14 | "github.com/tomdoestech/go-for-ts-devs/internal/handlers" 15 | "github.com/tomdoestech/go-for-ts-devs/internal/store" 16 | ) 17 | 18 | func main() { 19 | 20 | todos := []store.Todo{} 21 | 22 | r := chi.NewRouter() 23 | 24 | // Use chi's logger and recover middlewares for better error handling 25 | r.Use(middleware.Logger) 26 | 27 | r.Get("/healthcheck", handlers.NewHealthcheckHandler().ServerHTTP) 28 | 29 | r.Post("/todos", handlers.NewCreateTodoHandler(handlers.CreateTodoHandlerParams{ 30 | Todos: &todos, 31 | }).ServerHTTP) 32 | 33 | r.Get("/todos", handlers.NewGetTodosHandler(handlers.GetTodosHandlerParams{ 34 | Todos: &todos, 35 | }).ServerHTTP) 36 | 37 | srv := &http.Server{ 38 | Addr: ":8080", 39 | Handler: r, 40 | } 41 | 42 | // Create a channel to listen for OS signals 43 | sigCh := make(chan os.Signal, 1) 44 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 45 | 46 | go func() { 47 | fmt.Println("Server is running on :8080") 48 | 49 | if err := srv.ListenAndServe(); err != nil { 50 | fmt.Printf("Error: %v\n", err) 51 | } 52 | }() 53 | 54 | fmt.Println("Press Ctrl+C to stop the server") 55 | 56 | // Wait for signals to gracefully shut down the server 57 | <-sigCh 58 | 59 | fmt.Println("Shutting down the server...") 60 | 61 | // Create a context with a timeout for the graceful shutdown 62 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 63 | defer cancel() 64 | 65 | if err := srv.Shutdown(ctx); err != nil { 66 | fmt.Printf("Error: %v\n", err) 67 | } 68 | 69 | fmt.Println("Server gracefully stopped") 70 | } 71 | -------------------------------------------------------------------------------- /part-2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tomdoestech/go-for-ts-devs 2 | 3 | go 1.21.3 4 | 5 | require github.com/go-chi/chi/v5 v5.0.10 // indirect 6 | -------------------------------------------------------------------------------- /part-2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= 2 | github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 3 | -------------------------------------------------------------------------------- /part-2/internal/handlers/createtodo.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/tomdoestech/go-for-ts-devs/internal/store" 8 | ) 9 | 10 | type CreateTodoHandler struct { 11 | todos *[]store.Todo 12 | } 13 | 14 | type CreateTodoHandlerParams struct { 15 | Todos *[]store.Todo 16 | } 17 | 18 | func NewCreateTodoHandler(parama CreateTodoHandlerParams) *CreateTodoHandler { 19 | return &CreateTodoHandler{ 20 | todos: parama.Todos, 21 | } 22 | } 23 | 24 | func (h *CreateTodoHandler) ServerHTTP(w http.ResponseWriter, r *http.Request) { 25 | todo := store.Todo{} 26 | err := json.NewDecoder(r.Body).Decode(&todo) 27 | 28 | if err != nil { 29 | w.WriteHeader(http.StatusBadRequest) 30 | return 31 | } 32 | 33 | *h.todos = append(*h.todos, todo) 34 | 35 | w.WriteHeader(http.StatusCreated) 36 | } 37 | -------------------------------------------------------------------------------- /part-2/internal/handlers/gettodos.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/tomdoestech/go-for-ts-devs/internal/store" 8 | ) 9 | 10 | type GetTodosHandler struct { 11 | todos *[]store.Todo 12 | } 13 | 14 | type GetTodosHandlerParams struct { 15 | Todos *[]store.Todo 16 | } 17 | 18 | func NewGetTodosHandler(params GetTodosHandlerParams) *GetTodosHandler { 19 | return &GetTodosHandler{ 20 | todos: params.Todos, 21 | } 22 | } 23 | 24 | func (h *GetTodosHandler) ServerHTTP(w http.ResponseWriter, r *http.Request) { 25 | 26 | w.Header().Set("Content-Type", "application/json") 27 | err := json.NewEncoder(w).Encode(h.todos) 28 | 29 | if err != nil { 30 | w.WriteHeader(http.StatusInternalServerError) 31 | return 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /part-2/internal/handlers/healthcheck.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import "net/http" 4 | 5 | type HealthcheckHandler struct { 6 | } 7 | 8 | func NewHealthcheckHandler() *HealthcheckHandler { 9 | return &HealthcheckHandler{} 10 | } 11 | 12 | func (h *HealthcheckHandler) ServerHTTP(w http.ResponseWriter, r *http.Request) { 13 | w.Write([]byte("OK")) 14 | } 15 | -------------------------------------------------------------------------------- /part-2/internal/store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | type Todo struct { 4 | Title string `json:"title"` 5 | Done bool `json:"done"` 6 | } 7 | -------------------------------------------------------------------------------- /part-3/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "log/slog" 7 | "math/rand" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/go-chi/chi/v5" 15 | "github.com/tomanagle/url-shortener/internal/handlers" 16 | "github.com/tomanagle/url-shortener/store/dbstore" 17 | ) 18 | 19 | func generateSlug() string { 20 | 21 | const charSet = "abcdefghijklmnopqrstuvwxyz0123456789" 22 | result := make([]byte, 6) 23 | 24 | for i := range result { 25 | result[i] = charSet[rand.Intn(len(charSet))] 26 | } 27 | 28 | return string(result) 29 | } 30 | 31 | func main() { 32 | 33 | r := chi.NewRouter() 34 | logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) 35 | serverCtx, serverStopCtx := context.WithCancel(context.Background()) 36 | killSig := make(chan os.Signal, 1) 37 | signal.Notify(killSig, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) 38 | 39 | svr := &http.Server{ 40 | Addr: ":8080", 41 | Handler: r, 42 | } 43 | 44 | shortURLStore := dbstore.NewShortURLStore(dbstore.NewShortURLStoreParams{ 45 | Logger: logger, 46 | }) 47 | 48 | go func() { 49 | sig := <-killSig 50 | 51 | logger.Info("got kill signal - shutting down", slog.String("signal", sig.String())) 52 | 53 | shutdownCtx, cancel := context.WithTimeout(serverCtx, 5*time.Second) 54 | 55 | go func() { 56 | <-shutdownCtx.Done() 57 | if shutdownCtx.Err() == context.DeadlineExceeded { 58 | log.Fatal("shutdown deadline exceeded") 59 | } 60 | }() 61 | 62 | err := svr.Shutdown(shutdownCtx) 63 | 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | 68 | serverStopCtx() 69 | logger.Info("server shutting down") 70 | cancel() 71 | }() 72 | 73 | go func() { 74 | err := svr.ListenAndServe() 75 | 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | }() 80 | 81 | r.Get("/healthcheck", handlers.NewHealthHandler().ServeHTTP) 82 | 83 | r.Post("/shorturl", handlers.NewCreateShortURLHandler(handlers.CreateShortURLHandlerParams{ 84 | ShortURLStore: shortURLStore, 85 | GenerateSlug: generateSlug, 86 | }).ServeHTTP) 87 | 88 | r.Get("/{slug}", handlers.NewGetShortURLHandler(handlers.GetShortURLHandlerParams{ 89 | ShortURLStore: shortURLStore, 90 | }).ServeHTTP) 91 | 92 | logger.Info("read to work") 93 | 94 | <-serverCtx.Done() 95 | } 96 | -------------------------------------------------------------------------------- /part-3/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tomanagle/url-shortener 2 | 3 | go 1.21.3 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 // indirect 7 | github.com/go-chi/chi/v5 v5.0.11 // indirect 8 | github.com/pmezard/go-difflib v1.0.0 // indirect 9 | github.com/stretchr/objx v0.5.1 // indirect 10 | github.com/stretchr/testify v1.8.4 // indirect 11 | gopkg.in/yaml.v3 v3.0.1 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /part-3/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= 5 | github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 10 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 11 | github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= 12 | github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= 13 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 15 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 16 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 17 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 21 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | -------------------------------------------------------------------------------- /part-3/internal/handlers/createshorturl.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/tomanagle/url-shortener/store" 8 | ) 9 | 10 | type CreateShortURLHandler struct { 11 | shortURLStore store.ShortURLStore 12 | generateSlug func() string 13 | } 14 | 15 | type CreateShortURLHandlerParams struct { 16 | ShortURLStore store.ShortURLStore 17 | GenerateSlug func() string 18 | } 19 | 20 | func NewCreateShortURLHandler(params CreateShortURLHandlerParams) *CreateShortURLHandler { 21 | return &CreateShortURLHandler{ 22 | shortURLStore: params.ShortURLStore, 23 | generateSlug: params.GenerateSlug, 24 | } 25 | } 26 | 27 | func (h *CreateShortURLHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 28 | 29 | var requestData = struct { 30 | Destination string `json:"destination"` 31 | }{} 32 | 33 | err := json.NewDecoder(r.Body).Decode(&requestData) 34 | 35 | if err != nil { 36 | w.WriteHeader(http.StatusBadRequest) 37 | return 38 | } 39 | 40 | slug := h.generateSlug() 41 | 42 | createdShortURL, err := h.shortURLStore.CreateShortURL(store.CreateShortURLParams{ 43 | Destination: requestData.Destination, 44 | Slug: slug, 45 | }) 46 | 47 | if err != nil { 48 | w.WriteHeader(http.StatusInternalServerError) 49 | return 50 | } 51 | 52 | w.Header().Set("Content-Type", "application/json") 53 | w.WriteHeader(http.StatusCreated) 54 | 55 | err = json.NewEncoder(w).Encode(createdShortURL) 56 | 57 | if err != nil { 58 | w.WriteHeader(http.StatusInternalServerError) 59 | return 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /part-3/internal/handlers/createshorturl_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/tomanagle/url-shortener/store" 12 | storemock "github.com/tomanagle/url-shortener/store/mock" 13 | ) 14 | 15 | func TestCreateShortURL(t *testing.T) { 16 | 17 | testCases := []struct { 18 | name string 19 | createMockResult store.ShortURL 20 | expectedStatusCode int 21 | expectedBody []byte 22 | payload string 23 | expectedCreateParams store.CreateShortURLParams 24 | }{ 25 | { 26 | name: "success", 27 | expectedStatusCode: http.StatusCreated, 28 | createMockResult: store.ShortURL{ 29 | Slug: "abcdef", 30 | Destination: "https://www.google.com", 31 | }, 32 | expectedBody: []byte(`{"destination":"https://www.google.com", "id":0, "slug":"abcdef"}`), 33 | expectedCreateParams: store.CreateShortURLParams{ 34 | Destination: "https://www.google.com", 35 | Slug: "1234", 36 | }, 37 | payload: `{"destination": "https://www.google.com"}`, 38 | }, 39 | } 40 | 41 | for _, tc := range testCases { 42 | t.Run(tc.name, func(t *testing.T) { 43 | assert := assert.New(t) 44 | 45 | mockShortURLStore := &storemock.MockShortURLStore{} 46 | 47 | mockShortURLStore.On("CreateShortURL", tc.expectedCreateParams).Return(tc.createMockResult, nil) 48 | 49 | handler := NewCreateShortURLHandler(CreateShortURLHandlerParams{ 50 | ShortURLStore: mockShortURLStore, 51 | GenerateSlug: func() string { 52 | return "1234" 53 | }, 54 | }) 55 | 56 | request := httptest.NewRequest("POST", "/shorturl", strings.NewReader(tc.payload)) 57 | responseRecorder := httptest.NewRecorder() 58 | 59 | handler.ServeHTTP(responseRecorder, request) 60 | 61 | response := responseRecorder.Result() 62 | defer response.Body.Close() 63 | body, err := io.ReadAll(response.Body) 64 | assert.NoError(err) 65 | 66 | assert.Equal(tc.expectedStatusCode, response.StatusCode) 67 | assert.JSONEq(string(tc.expectedBody), string(body)) 68 | 69 | mockShortURLStore.AssertExpectations(t) 70 | }) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /part-3/internal/handlers/getshorturl.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/tomanagle/url-shortener/store" 8 | ) 9 | 10 | type GetShortURLHandler struct { 11 | shortURLStore store.ShortURLStore 12 | } 13 | 14 | type GetShortURLHandlerParams struct { 15 | ShortURLStore store.ShortURLStore 16 | } 17 | 18 | func NewGetShortURLHandler(params GetShortURLHandlerParams) *GetShortURLHandler { 19 | return &GetShortURLHandler{ 20 | shortURLStore: params.ShortURLStore, 21 | } 22 | } 23 | 24 | func (h *GetShortURLHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 25 | 26 | slug := r.URL.Path[1:] 27 | 28 | fmt.Println("slug", slug) 29 | 30 | shortURL, err := h.shortURLStore.GetShortURLBySlug(slug) 31 | 32 | if err != nil { 33 | w.WriteHeader(http.StatusNotFound) 34 | return 35 | } 36 | 37 | if shortURL == nil { 38 | w.WriteHeader(http.StatusNotFound) 39 | return 40 | } 41 | 42 | http.Redirect(w, r, shortURL.Destination, http.StatusMovedPermanently) 43 | } 44 | -------------------------------------------------------------------------------- /part-3/internal/handlers/getshorturl_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/tomanagle/url-shortener/store" 10 | storemock "github.com/tomanagle/url-shortener/store/mock" 11 | ) 12 | 13 | func TestGetShortURL(t *testing.T) { 14 | testCases := []struct { 15 | name string 16 | url string 17 | expectedStatusCode int 18 | expectedGetShortURLParams string 19 | getShortURLResult *store.ShortURL 20 | }{ 21 | { 22 | name: "success", 23 | url: "/123", 24 | expectedStatusCode: http.StatusMovedPermanently, 25 | expectedGetShortURLParams: "123", 26 | getShortURLResult: &store.ShortURL{ 27 | Destination: "http://google.com", 28 | }, 29 | }, 30 | { 31 | name: "fail with 404", 32 | url: "/123", 33 | expectedStatusCode: http.StatusNotFound, 34 | expectedGetShortURLParams: "123", 35 | getShortURLResult: nil, 36 | }, 37 | } 38 | 39 | for _, tc := range testCases { 40 | t.Run(tc.name, func(t *testing.T) { 41 | 42 | assert := assert.New(t) 43 | shortURLStoreMock := &storemock.MockShortURLStore{} 44 | 45 | shortURLStoreMock.On("GetShortURLBySlug", tc.expectedGetShortURLParams).Return(tc.getShortURLResult, nil) 46 | 47 | handler := NewGetShortURLHandler(GetShortURLHandlerParams{ 48 | ShortURLStore: shortURLStoreMock, 49 | }) 50 | 51 | request := httptest.NewRequest("GET", tc.url, nil) 52 | responseRecorder := httptest.NewRecorder() 53 | 54 | handler.ServeHTTP(responseRecorder, request) 55 | 56 | response := responseRecorder.Result() 57 | defer response.Body.Close() 58 | 59 | assert.Equal(tc.expectedStatusCode, response.StatusCode) 60 | 61 | shortURLStoreMock.AssertExpectations(t) 62 | }) 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /part-3/internal/handlers/healthcheck.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import "net/http" 4 | 5 | type HelthcheckHandler struct{} 6 | 7 | func NewHealthHandler() *HelthcheckHandler { 8 | return &HelthcheckHandler{} 9 | } 10 | 11 | func (h *HelthcheckHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 12 | w.Write([]byte("OK")) 13 | } 14 | -------------------------------------------------------------------------------- /part-3/store/dbstore/shorturl.go: -------------------------------------------------------------------------------- 1 | package dbstore 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/tomanagle/url-shortener/store" 7 | ) 8 | 9 | type ShortURLStore struct { 10 | shortURLs []store.ShortURL 11 | logger *slog.Logger 12 | } 13 | 14 | type NewShortURLStoreParams struct { 15 | Logger *slog.Logger 16 | } 17 | 18 | func NewShortURLStore(params NewShortURLStoreParams) *ShortURLStore { 19 | shortURLs := []store.ShortURL{} 20 | 21 | return &ShortURLStore{ 22 | shortURLs: shortURLs, 23 | logger: params.Logger, 24 | } 25 | } 26 | 27 | func (s *ShortURLStore) CreateShortURL(params store.CreateShortURLParams) (store.ShortURL, error) { 28 | 29 | shortURL := store.ShortURL{ 30 | Destination: params.Destination, 31 | Slug: params.Slug, 32 | ID: len(s.shortURLs), 33 | } 34 | 35 | s.shortURLs = append(s.shortURLs, shortURL) 36 | 37 | s.logger.Info("short URL created", slog.Any("values", shortURL)) 38 | 39 | return shortURL, nil 40 | } 41 | 42 | func (s *ShortURLStore) GetShortURLBySlug(slug string) (*store.ShortURL, error) { 43 | 44 | for _, shortURL := range s.shortURLs { 45 | if shortURL.Slug == slug { 46 | result := shortURL 47 | return &result, nil 48 | } 49 | } 50 | 51 | return nil, store.ErrShortURLNotFound 52 | } 53 | -------------------------------------------------------------------------------- /part-3/store/mock/mock.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | "github.com/tomanagle/url-shortener/store" 6 | ) 7 | 8 | type MockShortURLStore struct { 9 | mock.Mock 10 | } 11 | 12 | func (m *MockShortURLStore) CreateShortURL(params store.CreateShortURLParams) (store.ShortURL, error) { 13 | args := m.Called(params) 14 | return args.Get(0).(store.ShortURL), args.Error(1) 15 | } 16 | 17 | func (m *MockShortURLStore) GetShortURLBySlug(slug string) (*store.ShortURL, error) { 18 | args := m.Called(slug) 19 | return args.Get(0).(*store.ShortURL), args.Error(1) 20 | } 21 | -------------------------------------------------------------------------------- /part-3/store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "errors" 4 | 5 | var ErrShortURLNotFound = errors.New("short URL not found") 6 | 7 | type ShortURL struct { 8 | ID int `json:"id" bson:"shortURLID"` 9 | Destination string `json:"destination" bson:"dest"` 10 | Slug string `json:"slug" bson:"slug"` 11 | } 12 | 13 | type CreateShortURLParams struct { 14 | Destination string 15 | Slug string 16 | } 17 | 18 | type ShortURLStore interface { 19 | CreateShortURL(params CreateShortURLParams) (ShortURL, error) 20 | GetShortURLBySlug(slug string) (*ShortURL, error) 21 | } 22 | --------------------------------------------------------------------------------