Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

02-04. add Get Api #2

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions 02-04-domain-validations/internal/course.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"strings"
"unicode"

"github.com/google/uuid"
)
Expand Down Expand Up @@ -33,6 +35,29 @@ func (id CourseID) String() string {
}

var ErrEmptyCourseName = errors.New("the field Course Name can not be empty")
var ErrInvalidCharactersCourseName = errors.New("the field Course Name can not have numbers characters")
var ErrMinimumCharactersCourseName = errors.New("the longitude of field Course Name has to be more than 5")


func checkName(value string) error {
if value == "" {
return ErrEmptyCourseName
}
if len(value) < 5 {
return ErrMinimumCharactersCourseName
}

words := strings.Fields(value)
for _, word := range words {
for _, l := range word {
if !unicode.IsLetter(l) {
return ErrInvalidCharactersCourseName
}
}
}
return nil
}


// CourseName represents the course name.
type CourseName struct {
Expand All @@ -41,15 +66,17 @@ type CourseName struct {

// NewCourseName instantiate VO for CourseName
func NewCourseName(value string) (CourseName, error) {
if value == "" {
return CourseName{}, ErrEmptyCourseName
err := checkName(value)
if err != nil {
return CourseName{}, err
}

return CourseName{
value: value,
}, nil
}


// String type converts the CourseName into string.
func (name CourseName) String() string {
return name.value
Expand Down Expand Up @@ -80,6 +107,7 @@ func (duration CourseDuration) String() string {
// CourseRepository defines the expected behaviour from a course storage.
type CourseRepository interface {
Save(ctx context.Context, course Course) error
GetAll(ctx context.Context) ([]Course, error)
}

//go:generate mockery --case=snake --outpkg=storagemocks --output=platform/storage/storagemocks --name=CourseRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func CreateHandler(courseRepository mooc.CourseRepository) gin.HandlerFunc {

course, err := mooc.NewCourse(req.ID, req.Name, req.Duration)
if err != nil {
ctx.JSON(http.StatusBadRequest, err.Error())
ctx.JSON(http.StatusPreconditionFailed, err.Error())
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,46 +22,38 @@ func TestHandler_Create(t *testing.T) {
r := gin.New()
r.POST("/courses", CreateHandler(courseRepository))

t.Run("given an invalid request it returns 400", func(t *testing.T) {
createCourseReq := createRequest{
Name: "Demo Course",
Duration: "10 months",
}

b, err := json.Marshal(createCourseReq)
require.NoError(t, err)

req, err := http.NewRequest(http.MethodPost, "/courses", bytes.NewBuffer(b))
require.NoError(t, err)

rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)

res := rec.Result()
defer res.Body.Close()

assert.Equal(t, http.StatusBadRequest, res.StatusCode)
})

t.Run("given a valid request it returns 201", func(t *testing.T) {
createCourseReq := createRequest{
ID: "8a1c5cdc-ba57-445a-994d-aa412d23723f",
Name: "Demo Course",
Duration: "10 months",
}

b, err := json.Marshal(createCourseReq)
require.NoError(t, err)

req, err := http.NewRequest(http.MethodPost, "/courses", bytes.NewBuffer(b))
require.NoError(t, err)

rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)

res := rec.Result()
defer res.Body.Close()

assert.Equal(t, http.StatusCreated, res.StatusCode)
})
tests := map[string]struct {
id string
name string
duration string
want int
}{
"given an invalid request it returns 400": {name: "Demo Course", duration: "10 months", want: http.StatusBadRequest},
"given an invalid name it returns 412": {id: "8a1c5cdc-ba57-445a-994d-aa412d23723f", name: "412 Course", duration: "10 months", want: http.StatusPreconditionFailed},
"given a valid request it returns 201": {id: "8a1c5cdc-ba57-445a-994d-aa412d23723f", name: "Demo Course", duration: "10 months", want: http.StatusCreated},
}
for key, value := range tests {
t.Run(key, func(t *testing.T) {

createCourseReq := createRequest{
ID: value.id,
Name: value.name,
Duration: value.duration,
}

b, err := json.Marshal(createCourseReq)
require.NoError(t, err)

req, err := http.NewRequest(http.MethodPost, "/courses", bytes.NewBuffer(b))
require.NoError(t, err)

rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)

res := rec.Result()
defer res.Body.Close()

assert.Equal(t, value.want, res.StatusCode)
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package courses

import (
"net/http"

mooc "github.com/CodelyTV/go-hexagonal_http_api-course/02-04-domain-validations/internal"
"github.com/gin-gonic/gin"
)

type getResponse struct {
Id string `json:"id"`
Name string `json:"name"`
Duration string `json:"duration"`
}

// GetHandler returns an HTTP handler for courses.
func GetHandler(courseRepository mooc.CourseRepository) gin.HandlerFunc {
return func(ctx *gin.Context) {
var courses, err = courseRepository.GetAll(ctx)
if err != nil {
// Si quiero devolver error en ves de la lista se rompe me genera un error de unmarshal
ctx.JSON(http.StatusInternalServerError, []getResponse{})
return
}
response := make([]getResponse, 0, len(courses))
for _, course := range courses {
response = append(response, getResponse{
Id: course.ID().String(),
Name: course.Name().String(),
Duration: course.Duration().String(),
})
}
ctx.JSON(http.StatusOK, response)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package courses

import (
"encoding/json"
"errors"
mooc "github.com/CodelyTV/go-hexagonal_http_api-course/02-04-domain-validations/internal"
"github.com/CodelyTV/go-hexagonal_http_api-course/02-04-domain-validations/internal/platform/storage/storagemocks"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"log"
"net/http"
"net/http/httptest"
"testing"
)

func TestGetHandler(t *testing.T) {
tests := map[string]struct {
mockData []mooc.Course
mockError error
expectedResponse []getResponse
expectedStatus int
}{
"Empty repo return 200 with empty list": {
mockData: []mooc.Course{},
mockError: nil,
expectedStatus:http.StatusOK,
expectedResponse: []getResponse{}},
"Repo error return 500": {
mockData: []mooc.Course{},
mockError: errors.New("the field Duration can not be empty"),
expectedStatus:http.StatusInternalServerError,
expectedResponse: []getResponse{}},
"Fully repo return 200 with list of courses":{
mockData: []mooc.Course{ mockCourse("8a1c5cdc-ba57-445a-994d-aa412d23723f", "Courses Complete", "123")},
mockError: nil,
expectedStatus:http.StatusOK,
expectedResponse: []getResponse{{Id: "8a1c5cdc-ba57-445a-994d-aa412d23723f", Name: "Courses Complete", Duration: "123"}}},
}
for key, value := range tests {
t.Run(key, func(t *testing.T) {

courseRepository := new(storagemocks.CourseRepository)
courseRepository.On("GetAll", mock.Anything).Return(value.mockData, value.mockError)
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/courses", GetHandler(courseRepository))

req, err := http.NewRequest(http.MethodGet, "/courses", nil)
require.NoError(t, err)

rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)

res := rec.Result()
defer res.Body.Close()

assert.Equal(t, value.expectedStatus, res.StatusCode)
var response []getResponse
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
log.Fatalln(err)
}

assert.Equal(t, value.expectedResponse, response)
})
}
}

func mockCourse(id string, name string, duration string) mooc.Course {
course, err := mooc.NewCourse(id, name, duration)
if err != nil{
log.Fatalln(err)
}
return course
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ func (s *Server) Run() error {
func (s *Server) registerRoutes() {
s.engine.GET("/health", health.CheckHandler())
s.engine.POST("/courses", courses.CreateHandler(s.courseRepository))
s.engine.GET("/courses", courses.GetHandler(s.courseRepository))
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,33 @@ func (r *CourseRepository) Save(ctx context.Context, course mooc.Course) error {

return nil
}

// Save implements the mooc.CourseRepository interface.
func (r *CourseRepository) GetAll(ctx context.Context) (courses []mooc.Course, err error) {
courseSQLStruct := sqlbuilder.NewSelectBuilder()
courseSQLStruct.Select("id", "name", "duration")
courseSQLStruct.From(sqlCourseTable)

sqlQuery, args := courseSQLStruct.Build()


rows, err := r.db.QueryContext(ctx, sqlQuery, args...)
if err != nil {
return nil, fmt.Errorf("error trying to get course on database: %v", err)
}
defer rows.Close()
courses = []mooc.Course{}
for rows.Next() {
var sqlCourse sqlCourse
err := rows.Scan(sqlCourse.ID, sqlCourse.Name, sqlCourse.Duration)
if err != nil {
return nil, err
}
course, err := mooc.NewCourse(sqlCourse.ID, sqlCourse.Name, sqlCourse.Duration)
if err != nil {
return nil, err
}
courses = append(courses, course)
}
return courses, nil
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ require (
github.com/huandu/go-sqlbuilder v1.10.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/stretchr/testify v1.4.0
github.com/vektra/mockery/v2 v2.7.4 // indirect
)
Loading