Skip to content

Commit dcc5aa3

Browse files
committed
Add lesson04 / video02
1 parent c1dabc3 commit dcc5aa3

File tree

14 files changed

+579
-2
lines changed

14 files changed

+579
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package bootstrap
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
7+
"github.com/CodelyTV/go-hexagonal_http_api-course/04-02-application-service-test/internal/creating"
8+
"github.com/CodelyTV/go-hexagonal_http_api-course/04-02-application-service-test/internal/platform/server"
9+
"github.com/CodelyTV/go-hexagonal_http_api-course/04-02-application-service-test/internal/platform/storage/mysql"
10+
_ "github.com/go-sql-driver/mysql"
11+
)
12+
13+
const (
14+
host = "localhost"
15+
port = 8080
16+
17+
dbUser = "codely"
18+
dbPass = "codely"
19+
dbHost = "localhost"
20+
dbPort = "3306"
21+
dbName = "codely"
22+
)
23+
24+
func Run() error {
25+
mysqlURI := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUser, dbPass, dbHost, dbPort, dbName)
26+
db, err := sql.Open("mysql", mysqlURI)
27+
if err != nil {
28+
return err
29+
}
30+
31+
courseRepository := mysql.NewCourseRepository(db)
32+
33+
creatingCourseService := creating.NewCourseService(courseRepository)
34+
35+
srv := server.New(host, port, creatingCourseService)
36+
return srv.Run()
37+
}
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/04-02-application-service-test/cmd/api/bootstrap"
7+
)
8+
9+
func main() {
10+
if err := bootstrap.Run(); err != nil {
11+
log.Fatal(err)
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package mooc
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/google/uuid"
9+
)
10+
11+
var ErrInvalidCourseID = errors.New("invalid Course ID")
12+
13+
// CourseID represents the course unique identifier.
14+
type CourseID struct {
15+
value string
16+
}
17+
18+
// NewCourseID instantiate the VO for CourseID
19+
func NewCourseID(value string) (CourseID, error) {
20+
v, err := uuid.Parse(value)
21+
if err != nil {
22+
return CourseID{}, fmt.Errorf("%w: %s", ErrInvalidCourseID, value)
23+
}
24+
25+
return CourseID{
26+
value: v.String(),
27+
}, nil
28+
}
29+
30+
// String type converts the CourseID into string.
31+
func (id CourseID) String() string {
32+
return id.value
33+
}
34+
35+
var ErrEmptyCourseName = errors.New("the field Course Name can not be empty")
36+
37+
// CourseName represents the course name.
38+
type CourseName struct {
39+
value string
40+
}
41+
42+
// NewCourseName instantiate VO for CourseName
43+
func NewCourseName(value string) (CourseName, error) {
44+
if value == "" {
45+
return CourseName{}, ErrEmptyCourseName
46+
}
47+
48+
return CourseName{
49+
value: value,
50+
}, nil
51+
}
52+
53+
// String type converts the CourseName into string.
54+
func (name CourseName) String() string {
55+
return name.value
56+
}
57+
58+
var ErrEmptyDuration = errors.New("the field Duration can not be empty")
59+
60+
// CourseDuration represents the course duration.
61+
type CourseDuration struct {
62+
value string
63+
}
64+
65+
func NewCourseDuration(value string) (CourseDuration, error) {
66+
if value == "" {
67+
return CourseDuration{}, ErrEmptyDuration
68+
}
69+
70+
return CourseDuration{
71+
value: value,
72+
}, nil
73+
}
74+
75+
// String type converts the CourseDuration into string.
76+
func (duration CourseDuration) String() string {
77+
return duration.value
78+
}
79+
80+
// Course is the data structure that represents a course.
81+
type Course struct {
82+
id CourseID
83+
name CourseName
84+
duration CourseDuration
85+
}
86+
87+
// CourseRepository defines the expected behaviour from a course storage.
88+
type CourseRepository interface {
89+
Save(ctx context.Context, course Course) error
90+
}
91+
92+
//go:generate mockery --case=snake --outpkg=storagemocks --output=platform/storage/storagemocks --name=CourseRepository
93+
94+
// NewCourse creates a new course.
95+
func NewCourse(id, name, duration string) (Course, error) {
96+
idVO, err := NewCourseID(id)
97+
if err != nil {
98+
return Course{}, err
99+
}
100+
101+
nameVO, err := NewCourseName(name)
102+
if err != nil {
103+
return Course{}, err
104+
}
105+
106+
durationVO, err := NewCourseDuration(duration)
107+
if err != nil {
108+
return Course{}, err
109+
}
110+
111+
return Course{
112+
id: idVO,
113+
name: nameVO,
114+
duration: durationVO,
115+
}, nil
116+
}
117+
118+
// ID returns the course unique identifier.
119+
func (c Course) ID() CourseID {
120+
return c.id
121+
}
122+
123+
// Name returns the course name.
124+
func (c Course) Name() CourseName {
125+
return c.name
126+
}
127+
128+
// Duration returns the course duration.
129+
func (c Course) Duration() CourseDuration {
130+
return c.duration
131+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package creating
2+
3+
import (
4+
"context"
5+
6+
mooc "github.com/CodelyTV/go-hexagonal_http_api-course/04-02-application-service-test/internal"
7+
)
8+
9+
// CourseService is the default CourseService interface
10+
// implementation returned by creating.NewCourseService.
11+
type CourseService struct {
12+
courseRepository mooc.CourseRepository
13+
}
14+
15+
// NewCourseService returns the default Service interface implementation.
16+
func NewCourseService(courseRepository mooc.CourseRepository) CourseService {
17+
return CourseService{
18+
courseRepository: courseRepository,
19+
}
20+
}
21+
22+
// CreateCourse implements the creating.CourseService interface.
23+
func (s CourseService) CreateCourse(ctx context.Context, id, name, duration string) error {
24+
course, err := mooc.NewCourse(id, name, duration)
25+
if err != nil {
26+
return err
27+
}
28+
return s.courseRepository.Save(ctx, course)
29+
}
30+
31+
32+
33+
34+
35+
36+
37+
38+
39+

04-01-application-service/internal/creating/service_test.go renamed to 04-02-application-service-test/internal/creating/service_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ package creating
33
import (
44
"context"
55
"errors"
6-
mooc "github.com/CodelyTV/go-hexagonal_http_api-course/04-01-application-service/internal"
7-
"github.com/CodelyTV/go-hexagonal_http_api-course/04-01-application-service/internal/platform/storage/storagemocks"
6+
mooc "github.com/CodelyTV/go-hexagonal_http_api-course/04-02-application-service-test/internal"
7+
"github.com/CodelyTV/go-hexagonal_http_api-course/04-02-application-service-test/internal/platform/storage/storagemocks"
88
"testing"
99

1010
"github.com/stretchr/testify/mock"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package courses
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
7+
mooc "github.com/CodelyTV/go-hexagonal_http_api-course/04-02-application-service-test/internal"
8+
"github.com/CodelyTV/go-hexagonal_http_api-course/04-02-application-service-test/internal/creating"
9+
"github.com/gin-gonic/gin"
10+
)
11+
12+
type createRequest struct {
13+
ID string `json:"id" binding:"required"`
14+
Name string `json:"name" binding:"required"`
15+
Duration string `json:"duration" binding:"required"`
16+
}
17+
18+
// CreateHandler returns an HTTP handler for courses creation.
19+
func CreateHandler(creatingCourseService creating.CourseService) gin.HandlerFunc {
20+
return func(ctx *gin.Context) {
21+
var req createRequest
22+
if err := ctx.BindJSON(&req); err != nil {
23+
ctx.JSON(http.StatusBadRequest, err.Error())
24+
return
25+
}
26+
27+
err := creatingCourseService.CreateCourse(ctx, req.ID, req.Name, req.Duration)
28+
29+
if err != nil {
30+
switch {
31+
case errors.Is(err, mooc.ErrInvalidCourseID),
32+
errors.Is(err, mooc.ErrEmptyCourseName), errors.Is(err, mooc.ErrInvalidCourseID):
33+
ctx.JSON(http.StatusBadRequest, err.Error())
34+
return
35+
default:
36+
ctx.JSON(http.StatusInternalServerError, err.Error())
37+
return
38+
}
39+
}
40+
41+
ctx.Status(http.StatusCreated)
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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/04-02-application-service-test/internal/creating"
11+
"github.com/CodelyTV/go-hexagonal_http_api-course/04-02-application-service-test/internal/platform/storage/storagemocks"
12+
"github.com/gin-gonic/gin"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/mock"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
func TestHandler_Create(t *testing.T) {
19+
repositoryMock := new(storagemocks.CourseRepository)
20+
repositoryMock.On(
21+
"Save",
22+
mock.Anything,
23+
mock.Anything,
24+
).Return(nil)
25+
26+
createCourseSrv := creating.NewCourseService(repositoryMock)
27+
28+
gin.SetMode(gin.TestMode)
29+
r := gin.New()
30+
r.POST("/courses", CreateHandler(createCourseSrv))
31+
32+
t.Run("given an invalid request it returns 400", func(t *testing.T) {
33+
createCourseReq := createRequest{
34+
Name: "Demo Course",
35+
Duration: "10 months",
36+
}
37+
38+
b, err := json.Marshal(createCourseReq)
39+
require.NoError(t, err)
40+
41+
req, err := http.NewRequest(http.MethodPost, "/courses", bytes.NewBuffer(b))
42+
require.NoError(t, err)
43+
44+
rec := httptest.NewRecorder()
45+
r.ServeHTTP(rec, req)
46+
47+
res := rec.Result()
48+
defer res.Body.Close()
49+
50+
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
51+
})
52+
53+
t.Run("given a valid request it returns 201", func(t *testing.T) {
54+
createCourseReq := createRequest{
55+
ID: "8a1c5cdc-ba57-445a-994d-aa412d23723f",
56+
Name: "Demo Course",
57+
Duration: "10 months",
58+
}
59+
60+
b, err := json.Marshal(createCourseReq)
61+
require.NoError(t, err)
62+
63+
req, err := http.NewRequest(http.MethodPost, "/courses", bytes.NewBuffer(b))
64+
require.NoError(t, err)
65+
66+
rec := httptest.NewRecorder()
67+
r.ServeHTTP(rec, req)
68+
69+
res := rec.Result()
70+
defer res.Body.Close()
71+
72+
assert.Equal(t, http.StatusCreated, res.StatusCode)
73+
})
74+
75+
t.Run("given a valid request with invalid id returns 400", func(t *testing.T) {
76+
createCourseReq := createRequest{
77+
ID: "ba57",
78+
Name: "Demo Course",
79+
Duration: "10 months",
80+
}
81+
82+
b, err := json.Marshal(createCourseReq)
83+
require.NoError(t, err)
84+
85+
req, err := http.NewRequest(http.MethodPost, "/courses", bytes.NewBuffer(b))
86+
require.NoError(t, err)
87+
88+
rec := httptest.NewRecorder()
89+
r.ServeHTTP(rec, req)
90+
91+
res := rec.Result()
92+
defer res.Body.Close()
93+
94+
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
95+
})
96+
}
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+
}

0 commit comments

Comments
 (0)