Skip to content

Commit b5d5278

Browse files
committed
Add lesson02 / video03
1 parent 6c0867f commit b5d5278

File tree

11 files changed

+323
-0
lines changed

11 files changed

+323
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package bootstrap
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
7+
"github.com/CodelyTV/go-hexagonal_http_api-course/02-03-controller-test/internal/platform/server"
8+
"github.com/CodelyTV/go-hexagonal_http_api-course/02-03-controller-test/internal/platform/storage/mysql"
9+
_ "github.com/go-sql-driver/mysql"
10+
)
11+
12+
const (
13+
host = "localhost"
14+
port = 8080
15+
16+
dbUser = "codely"
17+
dbPass = "codely"
18+
dbHost = "localhost"
19+
dbPort = "3306"
20+
dbName = "codely"
21+
)
22+
23+
func Run() error {
24+
mysqlURI := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUser, dbPass, dbHost, dbPort, dbName)
25+
db, err := sql.Open("mysql", mysqlURI)
26+
if err != nil {
27+
return err
28+
}
29+
30+
courseRepository := mysql.NewCourseRepository(db)
31+
32+
srv := server.New(host, port, courseRepository)
33+
return srv.Run()
34+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package main
2+
3+
import (
4+
"log"
5+
6+
"github.com/CodelyTV/go-hexagonal_http_api-course/02-03-controller-test/cmd/api/bootstrap"
7+
)
8+
9+
func main() {
10+
if err := bootstrap.Run(); err != nil {
11+
log.Fatal(err)
12+
}
13+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package mooc
2+
3+
import "context"
4+
5+
// CourseRepository defines the expected behaviour from a course storage.
6+
type CourseRepository interface {
7+
Save(ctx context.Context, course Course) error
8+
}
9+
10+
//go:generate mockery --case=snake --outpkg=storagemocks --output=platform/storage/storagemocks --name=CourseRepository
11+
12+
// Course is the data structure that represents a course.
13+
type Course struct {
14+
id string
15+
name string
16+
duration string
17+
}
18+
19+
// NewCourse creates a new course.
20+
func NewCourse(id, name, duration string) Course {
21+
return Course{
22+
id: id,
23+
name: name,
24+
duration: duration,
25+
}
26+
}
27+
28+
// ID returns the course unique identifier.
29+
func (c Course) ID() string {
30+
return c.id
31+
}
32+
33+
// Name returns the course name.
34+
func (c Course) Name() string {
35+
return c.name
36+
}
37+
38+
// Duration returns the course duration.
39+
func (c Course) Duration() string {
40+
return c.duration
41+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package courses
2+
3+
import (
4+
"net/http"
5+
6+
mooc "github.com/CodelyTV/go-hexagonal_http_api-course/02-03-controller-test/internal"
7+
"github.com/gin-gonic/gin"
8+
)
9+
10+
type createRequest struct {
11+
ID string `json:"id" binding:"required"`
12+
Name string `json:"name" binding:"required"`
13+
Duration string `json:"duration" binding:"required"`
14+
}
15+
16+
// CreateHandler returns an HTTP handler for courses creation.
17+
func CreateHandler(courseRepository mooc.CourseRepository) gin.HandlerFunc {
18+
return func(ctx *gin.Context) {
19+
var req createRequest
20+
if err := ctx.BindJSON(&req); err != nil {
21+
ctx.JSON(http.StatusBadRequest, err.Error())
22+
return
23+
}
24+
25+
course := mooc.NewCourse(req.ID, req.Name, req.Duration)
26+
if err := courseRepository.Save(ctx, course); err != nil {
27+
ctx.JSON(http.StatusInternalServerError, err.Error())
28+
return
29+
}
30+
31+
ctx.Status(http.StatusCreated)
32+
}
33+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package courses
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/CodelyTV/go-hexagonal_http_api-course/02-03-controller-test/internal/platform/storage/storagemocks"
11+
"github.com/gin-gonic/gin"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/mock"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestHandler_Create(t *testing.T) {
18+
courseRepository := new(storagemocks.CourseRepository)
19+
courseRepository.On("Save", mock.Anything, mock.AnythingOfType("mooc.Course")).Return(nil)
20+
21+
gin.SetMode(gin.TestMode)
22+
r := gin.New()
23+
r.POST("/courses", CreateHandler(courseRepository))
24+
25+
t.Run("given an invalid request it returns 400", func(t *testing.T) {
26+
createCourseReq := createRequest{
27+
ID: "8a1c5cdc-ba57-445a-994d-aa412d23723f",
28+
Name: "Demo Course",
29+
}
30+
31+
b, err := json.Marshal(createCourseReq)
32+
require.NoError(t, err)
33+
34+
req, err := http.NewRequest(http.MethodPost, "/courses", bytes.NewBuffer(b))
35+
require.NoError(t, err)
36+
37+
rec := httptest.NewRecorder()
38+
r.ServeHTTP(rec, req)
39+
40+
res := rec.Result()
41+
defer res.Body.Close()
42+
43+
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
44+
})
45+
46+
t.Run("given a valid request it returns 201", func(t *testing.T) {
47+
createCourseReq := createRequest{
48+
ID: "8a1c5cdc-ba57-445a-994d-aa412d23723f",
49+
Name: "Demo Course",
50+
Duration: "10 months",
51+
}
52+
53+
b, err := json.Marshal(createCourseReq)
54+
require.NoError(t, err)
55+
56+
req, err := http.NewRequest(http.MethodPost, "/courses", bytes.NewBuffer(b))
57+
require.NoError(t, err)
58+
59+
rec := httptest.NewRecorder()
60+
r.ServeHTTP(rec, req)
61+
62+
res := rec.Result()
63+
defer res.Body.Close()
64+
65+
assert.Equal(t, http.StatusCreated, res.StatusCode)
66+
})
67+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package health
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/gin-gonic/gin"
7+
)
8+
9+
// CheckHandler returns an HTTP handler to perform health checks.
10+
func CheckHandler() gin.HandlerFunc {
11+
return func(ctx *gin.Context) {
12+
ctx.String(http.StatusOK, "everything is ok!")
13+
}
14+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package server
2+
3+
import (
4+
"fmt"
5+
"log"
6+
7+
mooc "github.com/CodelyTV/go-hexagonal_http_api-course/02-03-controller-test/internal"
8+
"github.com/CodelyTV/go-hexagonal_http_api-course/02-03-controller-test/internal/platform/server/handler/courses"
9+
"github.com/CodelyTV/go-hexagonal_http_api-course/02-03-controller-test/internal/platform/server/handler/health"
10+
"github.com/gin-gonic/gin"
11+
)
12+
13+
type Server struct {
14+
httpAddr string
15+
engine *gin.Engine
16+
17+
// deps
18+
courseRepository mooc.CourseRepository
19+
}
20+
21+
func New(host string, port uint, courseRepository mooc.CourseRepository) Server {
22+
srv := Server{
23+
engine: gin.New(),
24+
httpAddr: fmt.Sprintf("%s:%d", host, port),
25+
26+
courseRepository: courseRepository,
27+
}
28+
29+
srv.registerRoutes()
30+
return srv
31+
}
32+
33+
func (s *Server) Run() error {
34+
log.Println("Server running on", s.httpAddr)
35+
return s.engine.Run(s.httpAddr)
36+
}
37+
38+
func (s *Server) registerRoutes() {
39+
s.engine.GET("/health", health.CheckHandler())
40+
s.engine.POST("/courses", courses.CreateHandler(s.courseRepository))
41+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package mysql
2+
3+
const (
4+
sqlCourseTable = "courses"
5+
)
6+
7+
type sqlCourse struct {
8+
ID string `db:"id"`
9+
Name string `db:"name"`
10+
Duration string `db:"duration"`
11+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package mysql
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
8+
mooc "github.com/CodelyTV/go-hexagonal_http_api-course/02-03-controller-test/internal"
9+
"github.com/huandu/go-sqlbuilder"
10+
)
11+
12+
// CourseRepository is a MySQL mooc.CourseRepository implementation.
13+
type CourseRepository struct {
14+
db *sql.DB
15+
}
16+
17+
// NewCourseRepository initializes a MySQL-based implementation of mooc.CourseRepository.
18+
func NewCourseRepository(db *sql.DB) *CourseRepository {
19+
return &CourseRepository{
20+
db: db,
21+
}
22+
}
23+
24+
// Save implements the mooc.CourseRepository interface.
25+
func (r *CourseRepository) Save(ctx context.Context, course mooc.Course) error {
26+
courseSQLStruct := sqlbuilder.NewStruct(new(sqlCourse))
27+
query, args := courseSQLStruct.InsertInto(sqlCourseTable, sqlCourse{
28+
ID: course.ID(),
29+
Name: course.Name(),
30+
Duration: course.Duration(),
31+
}).Build()
32+
33+
_, err := r.db.ExecContext(ctx, query, args...)
34+
if err != nil {
35+
return fmt.Errorf("error trying to persist course on database: %v", err)
36+
}
37+
38+
return nil
39+
}

02-03-controller-test/internal/platform/storage/storagemocks/course_repository.go

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)