From eaaf18d972c1d5db63d7d2ef2361fb38dd5a4f61 Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 5 Aug 2025 15:07:51 +0800 Subject: [PATCH 01/16] refactor: remove repo interfaces --- boost/router.go | 4 +-- handler/categories.go | 12 +++---- handler/categories_posts_test.go | 57 +++++++++++++------------------- handler/posts.go | 16 ++++----- 4 files changed, 35 insertions(+), 54 deletions(-) diff --git a/boost/router.go b/boost/router.go index 95c88a91..cde37298 100644 --- a/boost/router.go +++ b/boost/router.go @@ -33,7 +33,7 @@ func (r *Router) PipelineFor(apiHandler http.ApiHandler) baseHttp.HandlerFunc { func (r *Router) Posts() { repo := repository.Posts{DB: r.Db} - abstract := handler.MakePostsHandler(&repo) + abstract := handler.MakePostsHandler(repo.GetAll, repo.FindBy) index := r.PipelineFor(abstract.Index) show := r.PipelineFor(abstract.Show) @@ -44,7 +44,7 @@ func (r *Router) Posts() { func (r *Router) Categories() { repo := repository.Categories{DB: r.Db} - abstract := handler.MakeCategoriesHandler(&repo) + abstract := handler.MakeCategoriesHandler(repo.GetAll) index := r.PipelineFor(abstract.Index) diff --git a/handler/categories.go b/handler/categories.go index 38212c6d..5009935b 100644 --- a/handler/categories.go +++ b/handler/categories.go @@ -11,20 +11,16 @@ import ( baseHttp "net/http" ) -type categoriesRepo interface { - GetAll(pagination.Paginate) (*pagination.Pagination[database.Category], error) -} - type CategoriesHandler struct { - Categories categoriesRepo + GetAll func(pagination.Paginate) (*pagination.Pagination[database.Category], error) } -func MakeCategoriesHandler(categories categoriesRepo) CategoriesHandler { - return CategoriesHandler{Categories: categories} +func MakeCategoriesHandler(getAll func(pagination.Paginate) (*pagination.Pagination[database.Category], error)) CategoriesHandler { + return CategoriesHandler{GetAll: getAll} } func (h *CategoriesHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - result, err := h.Categories.GetAll( + result, err := h.GetAll( paginate.MakeFrom(r.URL, 5), ) diff --git a/handler/categories_posts_test.go b/handler/categories_posts_test.go index b4fa20a7..0e2c411d 100644 --- a/handler/categories_posts_test.go +++ b/handler/categories_posts_test.go @@ -14,37 +14,15 @@ import ( "github.com/oullin/handler/payload" ) -// stubCategories simulates repository.Categories -type stubCategories struct { - result *pagination.Pagination[database.Category] - err error -} - -func (s stubCategories) GetAll(p pagination.Paginate) (*pagination.Pagination[database.Category], error) { - return s.result, s.err -} - -// stubPosts simulates repository.Posts -type stubPosts struct { - list *pagination.Pagination[database.Post] - err error - item *database.Post -} - -func (s stubPosts) GetAll(filters queries.PostFilters, p pagination.Paginate) (*pagination.Pagination[database.Post], error) { - return s.list, s.err -} - -func (s stubPosts) FindBy(slug string) *database.Post { - return s.item -} - func TestCategoriesHandlerIndex(t *testing.T) { pag := pagination.Paginate{Page: 1, Limit: 5} pag.SetNumItems(1) cats := []database.Category{{UUID: "1", Name: "Cat", Slug: "cat", Description: "desc"}} - repo := stubCategories{result: pagination.MakePagination(cats, pag)} - h := MakeCategoriesHandler(&repo) + result := pagination.MakePagination(cats, pag) + repoErr := error(nil) + h := MakeCategoriesHandler(func(p pagination.Paginate) (*pagination.Pagination[database.Category], error) { + return result, repoErr + }) req := httptest.NewRequest("GET", "/categories", nil) rec := httptest.NewRecorder() @@ -65,7 +43,7 @@ func TestCategoriesHandlerIndex(t *testing.T) { t.Fatalf("unexpected resp %#v", resp) } - repo.err = errors.New("fail") + repoErr = errors.New("fail") rec2 := httptest.NewRecorder() if h.Index(rec2, req) == nil { t.Fatalf("expected error") @@ -76,8 +54,14 @@ func TestPostsHandlerIndex(t *testing.T) { post := database.Post{UUID: "p1", Slug: "slug", Title: "title"} pag := pagination.Paginate{Page: 1, Limit: 10} pag.SetNumItems(1) - repo := stubPosts{list: pagination.MakePagination([]database.Post{post}, pag)} - h := MakePostsHandler(&repo) + list := pagination.MakePagination([]database.Post{post}, pag) + repoErr := error(nil) + h := MakePostsHandler( + func(filters queries.PostFilters, p pagination.Paginate) (*pagination.Pagination[database.Post], error) { + return list, repoErr + }, + func(slug string) *database.Post { return &post }, + ) body, _ := json.Marshal(payload.IndexRequestBody{Title: "title"}) req := httptest.NewRequest("POST", "/posts", bytes.NewReader(body)) @@ -89,7 +73,7 @@ func TestPostsHandlerIndex(t *testing.T) { t.Fatalf("status %d", rec.Code) } - repo.err = errors.New("fail") + repoErr = errors.New("fail") rec2 := httptest.NewRecorder() if h.Index(rec2, req) == nil { t.Fatalf("expected error") @@ -104,8 +88,13 @@ func TestPostsHandlerIndex(t *testing.T) { func TestPostsHandlerShow(t *testing.T) { post := database.Post{UUID: "p1", Slug: "slug", Title: "title"} - repo := stubPosts{item: &post} - h := MakePostsHandler(&repo) + item := &post + h := MakePostsHandler( + func(filters queries.PostFilters, p pagination.Paginate) (*pagination.Pagination[database.Post], error) { + return nil, nil + }, + func(slug string) *database.Post { return item }, + ) req := httptest.NewRequest("GET", "/posts/slug", nil) req.SetPathValue("slug", "slug") @@ -123,7 +112,7 @@ func TestPostsHandlerShow(t *testing.T) { t.Fatalf("expected bad request") } - repo.item = nil + item = nil req3 := httptest.NewRequest("GET", "/posts/slug", nil) req3.SetPathValue("slug", "slug") rec3 := httptest.NewRecorder() diff --git a/handler/posts.go b/handler/posts.go index c1fbeb60..e2c066a3 100644 --- a/handler/posts.go +++ b/handler/posts.go @@ -14,17 +14,13 @@ import ( baseHttp "net/http" ) -type postsRepo interface { - GetAll(queries.PostFilters, pagination.Paginate) (*pagination.Pagination[database.Post], error) - FindBy(slug string) *database.Post -} - type PostsHandler struct { - Posts postsRepo + GetAll func(queries.PostFilters, pagination.Paginate) (*pagination.Pagination[database.Post], error) + FindBy func(slug string) *database.Post } -func MakePostsHandler(posts postsRepo) PostsHandler { - return PostsHandler{Posts: posts} +func MakePostsHandler(getAll func(queries.PostFilters, pagination.Paginate) (*pagination.Pagination[database.Post], error), findBy func(string) *database.Post) PostsHandler { + return PostsHandler{GetAll: getAll, FindBy: findBy} } func (h *PostsHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { @@ -38,7 +34,7 @@ func (h *PostsHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *ht return http.InternalError("There was an issue reading the request. Please, try again later.") } - result, err := h.Posts.GetAll( + result, err := h.GetAll( payload.GetPostsFiltersFrom(requestBody), paginate.MakeFrom(r.URL, 10), ) @@ -70,7 +66,7 @@ func (h *PostsHandler) Show(w baseHttp.ResponseWriter, r *baseHttp.Request) *htt return http.BadRequestError("Slugs are required to show posts content") } - post := h.Posts.FindBy(slug) + post := h.FindBy(slug) if post == nil { return http.NotFound(fmt.Sprintf("The given post '%s' was not found", slug)) } From a2e85dd05ed4fb5a316fb12731cd19d703ec7233 Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 5 Aug 2025 15:24:33 +0800 Subject: [PATCH 02/16] refactor: simplify categories handler --- boost/router.go | 2 +- handler/categories.go | 12 ++++++++---- handler/categories_posts_test.go | 13 +++++++++++-- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/boost/router.go b/boost/router.go index cde37298..15265825 100644 --- a/boost/router.go +++ b/boost/router.go @@ -44,7 +44,7 @@ func (r *Router) Posts() { func (r *Router) Categories() { repo := repository.Categories{DB: r.Db} - abstract := handler.MakeCategoriesHandler(repo.GetAll) + abstract := handler.MakeCategoriesHandler(repo) index := r.PipelineFor(abstract.Index) diff --git a/handler/categories.go b/handler/categories.go index 5009935b..77fd1f3f 100644 --- a/handler/categories.go +++ b/handler/categories.go @@ -12,15 +12,19 @@ import ( ) type CategoriesHandler struct { - GetAll func(pagination.Paginate) (*pagination.Pagination[database.Category], error) + repo interface { + GetAll(pagination.Paginate) (*pagination.Pagination[database.Category], error) + } } -func MakeCategoriesHandler(getAll func(pagination.Paginate) (*pagination.Pagination[database.Category], error)) CategoriesHandler { - return CategoriesHandler{GetAll: getAll} +func MakeCategoriesHandler(repo interface { + GetAll(pagination.Paginate) (*pagination.Pagination[database.Category], error) +}) CategoriesHandler { + return CategoriesHandler{repo: repo} } func (h *CategoriesHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - result, err := h.GetAll( + result, err := h.repo.GetAll( paginate.MakeFrom(r.URL, 5), ) diff --git a/handler/categories_posts_test.go b/handler/categories_posts_test.go index 0e2c411d..2d9a9d74 100644 --- a/handler/categories_posts_test.go +++ b/handler/categories_posts_test.go @@ -14,15 +14,24 @@ import ( "github.com/oullin/handler/payload" ) +type fakeCategoriesRepo struct { + getAll func(pagination.Paginate) (*pagination.Pagination[database.Category], error) +} + +func (f fakeCategoriesRepo) GetAll(p pagination.Paginate) (*pagination.Pagination[database.Category], error) { + return f.getAll(p) +} + func TestCategoriesHandlerIndex(t *testing.T) { pag := pagination.Paginate{Page: 1, Limit: 5} pag.SetNumItems(1) cats := []database.Category{{UUID: "1", Name: "Cat", Slug: "cat", Description: "desc"}} result := pagination.MakePagination(cats, pag) repoErr := error(nil) - h := MakeCategoriesHandler(func(p pagination.Paginate) (*pagination.Pagination[database.Category], error) { + repo := fakeCategoriesRepo{getAll: func(p pagination.Paginate) (*pagination.Pagination[database.Category], error) { return result, repoErr - }) + }} + h := MakeCategoriesHandler(repo) req := httptest.NewRequest("GET", "/categories", nil) rec := httptest.NewRecorder() From 0f019d1710edc772d96aaf4b9d6bb41613ebeaad Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 5 Aug 2025 17:22:02 +0800 Subject: [PATCH 03/16] refactor: inject categories repo struct --- boost/router.go | 2 +- handler/categories.go | 15 +++++------ handler/categories_posts_test.go | 45 -------------------------------- 3 files changed, 8 insertions(+), 54 deletions(-) diff --git a/boost/router.go b/boost/router.go index 15265825..edf16b52 100644 --- a/boost/router.go +++ b/boost/router.go @@ -44,7 +44,7 @@ func (r *Router) Posts() { func (r *Router) Categories() { repo := repository.Categories{DB: r.Db} - abstract := handler.MakeCategoriesHandler(repo) + abstract := handler.MakeCategoriesHandler(&repo) index := r.PipelineFor(abstract.Index) diff --git a/handler/categories.go b/handler/categories.go index 77fd1f3f..1ef2c451 100644 --- a/handler/categories.go +++ b/handler/categories.go @@ -3,6 +3,7 @@ package handler import ( "encoding/json" "github.com/oullin/database" + "github.com/oullin/database/repository" "github.com/oullin/database/repository/pagination" "github.com/oullin/handler/paginate" "github.com/oullin/handler/payload" @@ -12,19 +13,17 @@ import ( ) type CategoriesHandler struct { - repo interface { - GetAll(pagination.Paginate) (*pagination.Pagination[database.Category], error) - } + Categories *repository.Categories } -func MakeCategoriesHandler(repo interface { - GetAll(pagination.Paginate) (*pagination.Pagination[database.Category], error) -}) CategoriesHandler { - return CategoriesHandler{repo: repo} +func MakeCategoriesHandler(categories *repository.Categories) CategoriesHandler { + return CategoriesHandler{ + Categories: categories, + } } func (h *CategoriesHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - result, err := h.repo.GetAll( + result, err := h.Categories.GetAll( paginate.MakeFrom(r.URL, 5), ) diff --git a/handler/categories_posts_test.go b/handler/categories_posts_test.go index 2d9a9d74..d5589392 100644 --- a/handler/categories_posts_test.go +++ b/handler/categories_posts_test.go @@ -14,51 +14,6 @@ import ( "github.com/oullin/handler/payload" ) -type fakeCategoriesRepo struct { - getAll func(pagination.Paginate) (*pagination.Pagination[database.Category], error) -} - -func (f fakeCategoriesRepo) GetAll(p pagination.Paginate) (*pagination.Pagination[database.Category], error) { - return f.getAll(p) -} - -func TestCategoriesHandlerIndex(t *testing.T) { - pag := pagination.Paginate{Page: 1, Limit: 5} - pag.SetNumItems(1) - cats := []database.Category{{UUID: "1", Name: "Cat", Slug: "cat", Description: "desc"}} - result := pagination.MakePagination(cats, pag) - repoErr := error(nil) - repo := fakeCategoriesRepo{getAll: func(p pagination.Paginate) (*pagination.Pagination[database.Category], error) { - return result, repoErr - }} - h := MakeCategoriesHandler(repo) - - req := httptest.NewRequest("GET", "/categories", nil) - rec := httptest.NewRecorder() - if err := h.Index(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - if rec.Code != http.StatusOK { - t.Fatalf("status %d", rec.Code) - } - - var resp struct { - Data []struct{ Slug string } `json:"data"` - } - if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - if len(resp.Data) != 1 || resp.Data[0].Slug != "cat" { - t.Fatalf("unexpected resp %#v", resp) - } - - repoErr = errors.New("fail") - rec2 := httptest.NewRecorder() - if h.Index(rec2, req) == nil { - t.Fatalf("expected error") - } -} - func TestPostsHandlerIndex(t *testing.T) { post := database.Post{UUID: "p1", Slug: "slug", Title: "title"} pag := pagination.Paginate{Page: 1, Limit: 10} From 74d0a4b011b953a9b575d03278e6c6161f7da0f9 Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 6 Aug 2025 09:50:49 +0800 Subject: [PATCH 04/16] refactor: inject posts repository --- boost/router.go | 2 +- handler/categories_posts_test.go | 16 ++++++++-------- handler/posts.go | 5 +++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/boost/router.go b/boost/router.go index edf16b52..95c88a91 100644 --- a/boost/router.go +++ b/boost/router.go @@ -33,7 +33,7 @@ func (r *Router) PipelineFor(apiHandler http.ApiHandler) baseHttp.HandlerFunc { func (r *Router) Posts() { repo := repository.Posts{DB: r.Db} - abstract := handler.MakePostsHandler(repo.GetAll, repo.FindBy) + abstract := handler.MakePostsHandler(&repo) index := r.PipelineFor(abstract.Index) show := r.PipelineFor(abstract.Show) diff --git a/handler/categories_posts_test.go b/handler/categories_posts_test.go index d5589392..27e33e5e 100644 --- a/handler/categories_posts_test.go +++ b/handler/categories_posts_test.go @@ -20,12 +20,12 @@ func TestPostsHandlerIndex(t *testing.T) { pag.SetNumItems(1) list := pagination.MakePagination([]database.Post{post}, pag) repoErr := error(nil) - h := MakePostsHandler( - func(filters queries.PostFilters, p pagination.Paginate) (*pagination.Pagination[database.Post], error) { + h := PostsHandler{ + GetAll: func(filters queries.PostFilters, p pagination.Paginate) (*pagination.Pagination[database.Post], error) { return list, repoErr }, - func(slug string) *database.Post { return &post }, - ) + FindBy: func(slug string) *database.Post { return &post }, + } body, _ := json.Marshal(payload.IndexRequestBody{Title: "title"}) req := httptest.NewRequest("POST", "/posts", bytes.NewReader(body)) @@ -53,12 +53,12 @@ func TestPostsHandlerIndex(t *testing.T) { func TestPostsHandlerShow(t *testing.T) { post := database.Post{UUID: "p1", Slug: "slug", Title: "title"} item := &post - h := MakePostsHandler( - func(filters queries.PostFilters, p pagination.Paginate) (*pagination.Pagination[database.Post], error) { + h := PostsHandler{ + GetAll: func(filters queries.PostFilters, p pagination.Paginate) (*pagination.Pagination[database.Post], error) { return nil, nil }, - func(slug string) *database.Post { return item }, - ) + FindBy: func(slug string) *database.Post { return item }, + } req := httptest.NewRequest("GET", "/posts/slug", nil) req.SetPathValue("slug", "slug") diff --git a/handler/posts.go b/handler/posts.go index e2c066a3..14ee1d12 100644 --- a/handler/posts.go +++ b/handler/posts.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "github.com/oullin/database" + "github.com/oullin/database/repository" "github.com/oullin/database/repository/pagination" "github.com/oullin/database/repository/queries" "github.com/oullin/handler/paginate" @@ -19,8 +20,8 @@ type PostsHandler struct { FindBy func(slug string) *database.Post } -func MakePostsHandler(getAll func(queries.PostFilters, pagination.Paginate) (*pagination.Pagination[database.Post], error), findBy func(string) *database.Post) PostsHandler { - return PostsHandler{GetAll: getAll, FindBy: findBy} +func MakePostsHandler(repo *repository.Posts) PostsHandler { + return PostsHandler{GetAll: repo.GetAll, FindBy: repo.FindBy} } func (h *PostsHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { From f5ae0d1bb9c03a5e0f007eed2792ad60b32643c6 Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 6 Aug 2025 10:12:12 +0800 Subject: [PATCH 05/16] refactor: inject posts repository pointer --- handler/categories_posts_test.go | 86 -------------------------------- handler/posts.go | 11 ++-- handler/posts_test.go | 27 ++++++++++ 3 files changed, 31 insertions(+), 93 deletions(-) delete mode 100644 handler/categories_posts_test.go create mode 100644 handler/posts_test.go diff --git a/handler/categories_posts_test.go b/handler/categories_posts_test.go deleted file mode 100644 index 27e33e5e..00000000 --- a/handler/categories_posts_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package handler - -import ( - "bytes" - "encoding/json" - "errors" - "net/http" - "net/http/httptest" - "testing" - - "github.com/oullin/database" - "github.com/oullin/database/repository/pagination" - "github.com/oullin/database/repository/queries" - "github.com/oullin/handler/payload" -) - -func TestPostsHandlerIndex(t *testing.T) { - post := database.Post{UUID: "p1", Slug: "slug", Title: "title"} - pag := pagination.Paginate{Page: 1, Limit: 10} - pag.SetNumItems(1) - list := pagination.MakePagination([]database.Post{post}, pag) - repoErr := error(nil) - h := PostsHandler{ - GetAll: func(filters queries.PostFilters, p pagination.Paginate) (*pagination.Pagination[database.Post], error) { - return list, repoErr - }, - FindBy: func(slug string) *database.Post { return &post }, - } - - body, _ := json.Marshal(payload.IndexRequestBody{Title: "title"}) - req := httptest.NewRequest("POST", "/posts", bytes.NewReader(body)) - rec := httptest.NewRecorder() - if err := h.Index(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - if rec.Code != http.StatusOK { - t.Fatalf("status %d", rec.Code) - } - - repoErr = errors.New("fail") - rec2 := httptest.NewRecorder() - if h.Index(rec2, req) == nil { - t.Fatalf("expected error") - } - - badReq := httptest.NewRequest("POST", "/posts", bytes.NewReader([]byte("{"))) - rec3 := httptest.NewRecorder() - if h.Index(rec3, badReq) == nil { - t.Fatalf("expected parse error") - } -} - -func TestPostsHandlerShow(t *testing.T) { - post := database.Post{UUID: "p1", Slug: "slug", Title: "title"} - item := &post - h := PostsHandler{ - GetAll: func(filters queries.PostFilters, p pagination.Paginate) (*pagination.Pagination[database.Post], error) { - return nil, nil - }, - FindBy: func(slug string) *database.Post { return item }, - } - - req := httptest.NewRequest("GET", "/posts/slug", nil) - req.SetPathValue("slug", "slug") - rec := httptest.NewRecorder() - if err := h.Show(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - if rec.Code != http.StatusOK { - t.Fatalf("status %d", rec.Code) - } - - req2 := httptest.NewRequest("GET", "/posts/", nil) - rec2 := httptest.NewRecorder() - if h.Show(rec2, req2) == nil { - t.Fatalf("expected bad request") - } - - item = nil - req3 := httptest.NewRequest("GET", "/posts/slug", nil) - req3.SetPathValue("slug", "slug") - rec3 := httptest.NewRecorder() - if h.Show(rec3, req3) == nil { - t.Fatalf("expected not found") - } -} diff --git a/handler/posts.go b/handler/posts.go index 14ee1d12..e6866f45 100644 --- a/handler/posts.go +++ b/handler/posts.go @@ -3,10 +3,8 @@ package handler import ( "encoding/json" "fmt" - "github.com/oullin/database" "github.com/oullin/database/repository" "github.com/oullin/database/repository/pagination" - "github.com/oullin/database/repository/queries" "github.com/oullin/handler/paginate" "github.com/oullin/handler/payload" "github.com/oullin/pkg" @@ -16,12 +14,11 @@ import ( ) type PostsHandler struct { - GetAll func(queries.PostFilters, pagination.Paginate) (*pagination.Pagination[database.Post], error) - FindBy func(slug string) *database.Post + Posts *repository.Posts } func MakePostsHandler(repo *repository.Posts) PostsHandler { - return PostsHandler{GetAll: repo.GetAll, FindBy: repo.FindBy} + return PostsHandler{Posts: repo} } func (h *PostsHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { @@ -35,7 +32,7 @@ func (h *PostsHandler) Index(w baseHttp.ResponseWriter, r *baseHttp.Request) *ht return http.InternalError("There was an issue reading the request. Please, try again later.") } - result, err := h.GetAll( + result, err := h.Posts.GetAll( payload.GetPostsFiltersFrom(requestBody), paginate.MakeFrom(r.URL, 10), ) @@ -67,7 +64,7 @@ func (h *PostsHandler) Show(w baseHttp.ResponseWriter, r *baseHttp.Request) *htt return http.BadRequestError("Slugs are required to show posts content") } - post := h.FindBy(slug) + post := h.Posts.FindBy(slug) if post == nil { return http.NotFound(fmt.Sprintf("The given post '%s' was not found", slug)) } diff --git a/handler/posts_test.go b/handler/posts_test.go new file mode 100644 index 00000000..ed9da213 --- /dev/null +++ b/handler/posts_test.go @@ -0,0 +1,27 @@ +package handler + +import ( + "bytes" + "net/http/httptest" + "testing" + + "github.com/oullin/database/repository" +) + +func TestPostsHandlerIndex_ParseError(t *testing.T) { + h := PostsHandler{Posts: &repository.Posts{}} + badReq := httptest.NewRequest("POST", "/posts", bytes.NewReader([]byte("{"))) + rec := httptest.NewRecorder() + if h.Index(rec, badReq) == nil { + t.Fatalf("expected parse error") + } +} + +func TestPostsHandlerShow_MissingSlug(t *testing.T) { + h := PostsHandler{Posts: &repository.Posts{}} + req := httptest.NewRequest("GET", "/posts/", nil) + rec := httptest.NewRecorder() + if h.Show(rec, req) == nil { + t.Fatalf("expected bad request") + } +} From 5e064913644d5a07673f797358282b0d21e2f5e9 Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 6 Aug 2025 10:30:40 +0800 Subject: [PATCH 06/16] test: split handlers and payload tests --- handler/education_test.go | 17 +++ handler/experience_test.go | 17 +++ handler/file_handler_test.go | 67 +++++++++++ handler/file_handlers_more_test.go | 104 ------------------ handler/file_handlers_test.go | 82 -------------- handler/payload/categories_test.go | 8 -- handler/payload/posts_filters_test.go | 11 ++ .../{posts_test.go => posts_response_test.go} | 17 --- handler/payload/posts_slug_test.go | 14 +++ handler/payload/tags_test.go | 15 +++ handler/profile_test.go | 17 +++ handler/projects_test.go | 17 +++ handler/recommendations_test.go | 17 +++ handler/social_test.go | 17 +++ handler/talks_test.go | 17 +++ 15 files changed, 226 insertions(+), 211 deletions(-) create mode 100644 handler/education_test.go create mode 100644 handler/experience_test.go create mode 100644 handler/file_handler_test.go delete mode 100644 handler/file_handlers_more_test.go delete mode 100644 handler/file_handlers_test.go create mode 100644 handler/payload/posts_filters_test.go rename handler/payload/{posts_test.go => posts_response_test.go} (61%) create mode 100644 handler/payload/posts_slug_test.go create mode 100644 handler/payload/tags_test.go create mode 100644 handler/profile_test.go create mode 100644 handler/projects_test.go create mode 100644 handler/recommendations_test.go create mode 100644 handler/social_test.go create mode 100644 handler/talks_test.go diff --git a/handler/education_test.go b/handler/education_test.go new file mode 100644 index 00000000..8db719b6 --- /dev/null +++ b/handler/education_test.go @@ -0,0 +1,17 @@ +package handler + +import ( + "net/http" + "testing" + + pkghttp "github.com/oullin/pkg/http" +) + +func TestEducationHandlerHandle(t *testing.T) { + runFileHandlerTest(t, "/education", []map[string]string{{"uuid": "1"}}, func(p string) interface { + Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError + } { + h := MakeEducationHandler(p) + return h + }) +} diff --git a/handler/experience_test.go b/handler/experience_test.go new file mode 100644 index 00000000..aca6a479 --- /dev/null +++ b/handler/experience_test.go @@ -0,0 +1,17 @@ +package handler + +import ( + "net/http" + "testing" + + pkghttp "github.com/oullin/pkg/http" +) + +func TestExperienceHandlerHandle(t *testing.T) { + runFileHandlerTest(t, "/experience", []map[string]string{{"uuid": "1"}}, func(p string) interface { + Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError + } { + h := MakeExperienceHandler(p) + return h + }) +} diff --git a/handler/file_handler_test.go b/handler/file_handler_test.go new file mode 100644 index 00000000..610737e4 --- /dev/null +++ b/handler/file_handler_test.go @@ -0,0 +1,67 @@ +package handler + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "testing" + + pkghttp "github.com/oullin/pkg/http" +) + +type testEnvelope struct { + Version string `json:"version"` + Data interface{} `json:"data"` +} + +func writeJSON(t *testing.T, v interface{}) string { + f, err := os.CreateTemp("", "data.json") + if err != nil { + t.Fatalf("tmp: %v", err) + } + enc := json.NewEncoder(f) + if err := enc.Encode(v); err != nil { + t.Fatalf("encode: %v", err) + } + f.Close() + return f.Name() +} + +func runFileHandlerTest(t *testing.T, path string, data interface{}, makeFn func(string) interface { + Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError +}) { + file := writeJSON(t, testEnvelope{Version: "v1", Data: data}) + defer os.Remove(file) + h := makeFn(file) + + req := httptest.NewRequest("GET", path, nil) + rec := httptest.NewRecorder() + if err := h.Handle(rec, req); err != nil { + t.Fatalf("err: %v", err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status %d", rec.Code) + } + + req2 := httptest.NewRequest("GET", path, nil) + req2.Header.Set("If-None-Match", "\"v1\"") + rec2 := httptest.NewRecorder() + if err := h.Handle(rec2, req2); err != nil { + t.Fatalf("err: %v", err) + } + if rec2.Code != http.StatusNotModified { + t.Fatalf("status %d", rec2.Code) + } + + badF, _ := os.CreateTemp("", "bad.json") + badF.WriteString("{") + badF.Close() + defer os.Remove(badF.Name()) + bad := makeFn(badF.Name()) + rec3 := httptest.NewRecorder() + req3 := httptest.NewRequest("GET", path, nil) + if bad.Handle(rec3, req3) == nil { + t.Fatalf("expected error") + } +} diff --git a/handler/file_handlers_more_test.go b/handler/file_handlers_more_test.go deleted file mode 100644 index a2677560..00000000 --- a/handler/file_handlers_more_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package handler - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "testing" - - pkghttp "github.com/oullin/pkg/http" -) - -type testData struct { - Version string `json:"version"` - Data any `json:"data"` -} - -func writeJSON(t *testing.T, v any) string { - f, err := os.CreateTemp("", "data.json") - if err != nil { - t.Fatalf("tmp: %v", err) - } - enc := json.NewEncoder(f) - if err := enc.Encode(v); err != nil { - t.Fatalf("encode: %v", err) - } - f.Close() - return f.Name() -} - -func runFileHandlerTest(t *testing.T, makeFn func(string) interface { - Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError -}, path string) { - file := writeJSON(t, testData{Version: "v1", Data: []map[string]string{{"id": "1"}}}) - defer os.Remove(file) - h := makeFn(file) - - req := httptest.NewRequest("GET", path, nil) - rec := httptest.NewRecorder() - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - if rec.Code != http.StatusOK { - t.Fatalf("status %d", rec.Code) - } - - req2 := httptest.NewRequest("GET", path, nil) - req2.Header.Set("If-None-Match", "\"v1\"") - rec2 := httptest.NewRecorder() - if err := h.Handle(rec2, req2); err != nil { - t.Fatalf("err: %v", err) - } - if rec2.Code != http.StatusNotModified { - t.Fatalf("status %d", rec2.Code) - } - - badF, _ := os.CreateTemp("", "bad.json") - badF.WriteString("{") - badF.Close() - defer os.Remove(badF.Name()) - bad := makeFn(badF.Name()) - rec3 := httptest.NewRecorder() - req3 := httptest.NewRequest("GET", path, nil) - if bad.Handle(rec3, req3) == nil { - t.Fatalf("expected error") - } -} - -func TestAdditionalFileHandlers(t *testing.T) { - runFileHandlerTest(t, func(p string) interface { - Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError - } { - h := MakeEducationHandler(p) - return h - }, "/education") - - runFileHandlerTest(t, func(p string) interface { - Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError - } { - h := MakeExperienceHandler(p) - return h - }, "/experience") - - runFileHandlerTest(t, func(p string) interface { - Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError - } { - h := MakeProjectsHandler(p) - return h - }, "/projects") - - runFileHandlerTest(t, func(p string) interface { - Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError - } { - h := MakeRecommendationsHandler(p) - return h - }, "/recommendations") - - runFileHandlerTest(t, func(p string) interface { - Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError - } { - h := MakeTalksHandler(p) - return h - }, "/talks") -} diff --git a/handler/file_handlers_test.go b/handler/file_handlers_test.go deleted file mode 100644 index bbcd6414..00000000 --- a/handler/file_handlers_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package handler - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "testing" -) - -type profileData struct { - Version string `json:"version"` - Data any `json:"data"` -} - -func writeTempJSON(t *testing.T, v any) string { - f, err := os.CreateTemp("", "data.json") - if err != nil { - t.Fatalf("tmp: %v", err) - } - enc := json.NewEncoder(f) - if err := enc.Encode(v); err != nil { - t.Fatalf("encode: %v", err) - } - f.Close() - return f.Name() -} - -func TestProfileHandlerHandle(t *testing.T) { - file := writeTempJSON(t, profileData{Version: "v1", Data: map[string]string{"nickname": "nick"}}) - defer os.Remove(file) - h := MakeProfileHandler(file) - - // ok response - req := httptest.NewRequest("GET", "/profile", nil) - rec := httptest.NewRecorder() - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - if rec.Code != http.StatusOK { - t.Fatalf("status %d", rec.Code) - } - - // cached request - req2 := httptest.NewRequest("GET", "/profile", nil) - req2.Header.Set("If-None-Match", "\"v1\"") - rec2 := httptest.NewRecorder() - if err := h.Handle(rec2, req2); err != nil { - t.Fatalf("err: %v", err) - } - if rec2.Code != http.StatusNotModified { - t.Fatalf("status %d", rec2.Code) - } - - // error on parse - badF, _ := os.CreateTemp("", "bad.json") - badF.WriteString("{invalid") - badF.Close() - badFile := badF.Name() - defer os.Remove(badFile) - bad := MakeProfileHandler(badFile) - req3 := httptest.NewRequest("GET", "/profile", nil) - rec3 := httptest.NewRecorder() - if bad.Handle(rec3, req3) == nil { - t.Fatalf("expected error") - } -} - -func TestSocialHandlerHandle(t *testing.T) { - file := writeTempJSON(t, profileData{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) - defer os.Remove(file) - h := MakeSocialHandler(file) - - req := httptest.NewRequest("GET", "/social", nil) - rec := httptest.NewRecorder() - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - if rec.Code != http.StatusOK { - t.Fatalf("status %d", rec.Code) - } -} diff --git a/handler/payload/categories_test.go b/handler/payload/categories_test.go index 8883969d..41f93858 100644 --- a/handler/payload/categories_test.go +++ b/handler/payload/categories_test.go @@ -13,11 +13,3 @@ func TestGetCategoriesResponse(t *testing.T) { t.Fatalf("unexpected %#v", r) } } - -func TestGetTagsResponse(t *testing.T) { - tags := []database.Tag{{UUID: "1", Name: "n", Slug: "s", Description: "d"}} - r := GetTagsResponse(tags) - if len(r) != 1 || r[0].Slug != "s" { - t.Fatalf("unexpected %#v", r) - } -} diff --git a/handler/payload/posts_filters_test.go b/handler/payload/posts_filters_test.go new file mode 100644 index 00000000..e24aff91 --- /dev/null +++ b/handler/payload/posts_filters_test.go @@ -0,0 +1,11 @@ +package payload + +import "testing" + +func TestGetPostsFiltersFrom(t *testing.T) { + req := IndexRequestBody{Title: "t", Author: "a", Category: "c", Tag: "g", Text: "x"} + f := GetPostsFiltersFrom(req) + if f.Title != "t" || f.Author != "a" || f.Category != "c" || f.Tag != "g" || f.Text != "x" { + t.Fatalf("unexpected filters: %+v", f) + } +} diff --git a/handler/payload/posts_test.go b/handler/payload/posts_response_test.go similarity index 61% rename from handler/payload/posts_test.go rename to handler/payload/posts_response_test.go index d3b08111..addc9b70 100644 --- a/handler/payload/posts_test.go +++ b/handler/payload/posts_response_test.go @@ -1,29 +1,12 @@ package payload import ( - "net/http/httptest" "testing" "time" "github.com/oullin/database" ) -func TestGetPostsFiltersFrom(t *testing.T) { - req := IndexRequestBody{Title: "t", Author: "a", Category: "c", Tag: "g", Text: "x"} - f := GetPostsFiltersFrom(req) - if f.Title != "t" || f.Author != "a" || f.Category != "c" || f.Tag != "g" || f.Text != "x" { - t.Fatalf("unexpected filters: %+v", f) - } -} - -func TestGetSlugFrom(t *testing.T) { - r := httptest.NewRequest("GET", "/posts/s", nil) - r.SetPathValue("slug", " SLUG ") - if s := GetSlugFrom(r); s != "slug" { - t.Fatalf("slug %s", s) - } -} - func TestGetPostsResponse(t *testing.T) { now := time.Now() p := database.Post{ diff --git a/handler/payload/posts_slug_test.go b/handler/payload/posts_slug_test.go new file mode 100644 index 00000000..7c1e464c --- /dev/null +++ b/handler/payload/posts_slug_test.go @@ -0,0 +1,14 @@ +package payload + +import ( + "net/http/httptest" + "testing" +) + +func TestGetSlugFrom(t *testing.T) { + r := httptest.NewRequest("GET", "/posts/s", nil) + r.SetPathValue("slug", " SLUG ") + if s := GetSlugFrom(r); s != "slug" { + t.Fatalf("slug %s", s) + } +} diff --git a/handler/payload/tags_test.go b/handler/payload/tags_test.go new file mode 100644 index 00000000..6efe8a92 --- /dev/null +++ b/handler/payload/tags_test.go @@ -0,0 +1,15 @@ +package payload + +import ( + "testing" + + "github.com/oullin/database" +) + +func TestGetTagsResponse(t *testing.T) { + tags := []database.Tag{{UUID: "1", Name: "n", Slug: "s", Description: "d"}} + r := GetTagsResponse(tags) + if len(r) != 1 || r[0].Slug != "s" { + t.Fatalf("unexpected %#v", r) + } +} diff --git a/handler/profile_test.go b/handler/profile_test.go new file mode 100644 index 00000000..dc11cba8 --- /dev/null +++ b/handler/profile_test.go @@ -0,0 +1,17 @@ +package handler + +import ( + "net/http" + "testing" + + pkghttp "github.com/oullin/pkg/http" +) + +func TestProfileHandlerHandle(t *testing.T) { + runFileHandlerTest(t, "/profile", map[string]string{"nickname": "nick"}, func(p string) interface { + Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError + } { + h := MakeProfileHandler(p) + return h + }) +} diff --git a/handler/projects_test.go b/handler/projects_test.go new file mode 100644 index 00000000..938545fc --- /dev/null +++ b/handler/projects_test.go @@ -0,0 +1,17 @@ +package handler + +import ( + "net/http" + "testing" + + pkghttp "github.com/oullin/pkg/http" +) + +func TestProjectsHandlerHandle(t *testing.T) { + runFileHandlerTest(t, "/projects", []map[string]string{{"uuid": "1"}}, func(p string) interface { + Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError + } { + h := MakeProjectsHandler(p) + return h + }) +} diff --git a/handler/recommendations_test.go b/handler/recommendations_test.go new file mode 100644 index 00000000..be41fda5 --- /dev/null +++ b/handler/recommendations_test.go @@ -0,0 +1,17 @@ +package handler + +import ( + "net/http" + "testing" + + pkghttp "github.com/oullin/pkg/http" +) + +func TestRecommendationsHandlerHandle(t *testing.T) { + runFileHandlerTest(t, "/recommendations", []map[string]string{{"uuid": "1"}}, func(p string) interface { + Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError + } { + h := MakeRecommendationsHandler(p) + return h + }) +} diff --git a/handler/social_test.go b/handler/social_test.go new file mode 100644 index 00000000..83e7eaa3 --- /dev/null +++ b/handler/social_test.go @@ -0,0 +1,17 @@ +package handler + +import ( + "net/http" + "testing" + + pkghttp "github.com/oullin/pkg/http" +) + +func TestSocialHandlerHandle(t *testing.T) { + runFileHandlerTest(t, "/social", []map[string]string{{"uuid": "1"}}, func(p string) interface { + Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError + } { + h := MakeSocialHandler(p) + return h + }) +} diff --git a/handler/talks_test.go b/handler/talks_test.go new file mode 100644 index 00000000..5391f779 --- /dev/null +++ b/handler/talks_test.go @@ -0,0 +1,17 @@ +package handler + +import ( + "net/http" + "testing" + + pkghttp "github.com/oullin/pkg/http" +) + +func TestTalksHandlerHandle(t *testing.T) { + runFileHandlerTest(t, "/talks", []map[string]string{{"uuid": "1"}}, func(p string) interface { + Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError + } { + h := MakeTalksHandler(p) + return h + }) +} From 01c91095407984f0fd914d5655f861adc825a3ae Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 6 Aug 2025 10:47:20 +0800 Subject: [PATCH 07/16] test: assert file handler payloads --- handler/file_handler_test.go | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/handler/file_handler_test.go b/handler/file_handler_test.go index 610737e4..35a9976b 100644 --- a/handler/file_handler_test.go +++ b/handler/file_handler_test.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httptest" "os" + "reflect" "testing" pkghttp "github.com/oullin/pkg/http" @@ -44,6 +45,33 @@ func runFileHandlerTest(t *testing.T, path string, data interface{}, makeFn func t.Fatalf("status %d", rec.Code) } + var resp testEnvelope + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if resp.Version != "v1" { + t.Fatalf("version %s", resp.Version) + } + expectedBytes, err := json.Marshal(data) + if err != nil { + t.Fatalf("marshal expected: %v", err) + } + var expectedVal interface{} + if err := json.Unmarshal(expectedBytes, &expectedVal); err != nil { + t.Fatalf("unmarshal expected: %v", err) + } + gotBytes, err := json.Marshal(resp.Data) + if err != nil { + t.Fatalf("marshal got: %v", err) + } + var gotVal interface{} + if err := json.Unmarshal(gotBytes, &gotVal); err != nil { + t.Fatalf("unmarshal got: %v", err) + } + if !subset(expectedVal, gotVal) { + t.Fatalf("payload %v does not contain %v", gotVal, expectedVal) + } + req2 := httptest.NewRequest("GET", path, nil) req2.Header.Set("If-None-Match", "\"v1\"") rec2 := httptest.NewRecorder() @@ -65,3 +93,32 @@ func runFileHandlerTest(t *testing.T, path string, data interface{}, makeFn func t.Fatalf("expected error") } } + +func subset(expected, got interface{}) bool { + switch e := expected.(type) { + case map[string]interface{}: + g, ok := got.(map[string]interface{}) + if !ok { + return false + } + for k, v := range e { + if !subset(v, g[k]) { + return false + } + } + return true + case []interface{}: + g, ok := got.([]interface{}) + if !ok || len(e) != len(g) { + return false + } + for i := range e { + if !subset(e[i], g[i]) { + return false + } + } + return true + default: + return reflect.DeepEqual(expected, got) + } +} From 0c43cf384cad88ac870e10cb1ff5e02497d8e3ee Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 6 Aug 2025 11:05:10 +0800 Subject: [PATCH 08/16] test: add happy path coverage --- go.mod | 4 +- go.sum | 4 ++ handler/education_test.go | 29 +++++++++++++ handler/experience_test.go | 29 +++++++++++++ handler/posts_test.go | 89 ++++++++++++++++++++++++++++++++++++++ handler/profile_test.go | 25 +++++++++++ handler/projects_test.go | 29 +++++++++++++ handler/social_test.go | 29 +++++++++++++ handler/talks_test.go | 29 +++++++++++++ 9 files changed, 266 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index d2758dd8..3a9206dd 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 + github.com/rs/cors v1.11.1 github.com/testcontainers/testcontainers-go v0.38.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.38.0 golang.org/x/crypto v0.40.0 @@ -15,6 +16,7 @@ require ( golang.org/x/text v0.27.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.6.0 + gorm.io/driver/sqlite v1.5.5 gorm.io/gorm v1.30.0 ) @@ -52,6 +54,7 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.1.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect @@ -65,7 +68,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/rs/cors v1.11.1 // indirect github.com/shirou/gopsutil/v4 v4.25.5 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/stretchr/testify v1.10.0 // indirect diff --git a/go.sum b/go.sum index 5920ec74..b7602bfe 100644 --- a/go.sum +++ b/go.sum @@ -95,6 +95,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -235,6 +237,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= +gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= diff --git a/handler/education_test.go b/handler/education_test.go index 8db719b6..0620bff4 100644 --- a/handler/education_test.go +++ b/handler/education_test.go @@ -1,7 +1,10 @@ package handler import ( + "encoding/json" "net/http" + "net/http/httptest" + "os" "testing" pkghttp "github.com/oullin/pkg/http" @@ -15,3 +18,29 @@ func TestEducationHandlerHandle(t *testing.T) { return h }) } + +func TestEducationHandlerHandle_Payload(t *testing.T) { + file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + defer os.Remove(file) + + h := MakeEducationHandler(file) + req := httptest.NewRequest("GET", "/education", nil) + rec := httptest.NewRecorder() + + if err := h.Handle(rec, req); err != nil { + t.Fatalf("err: %v", err) + } + + var resp testEnvelope + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + arr, ok := resp.Data.([]interface{}) + if !ok || len(arr) != 1 { + t.Fatalf("unexpected data: %+v", resp.Data) + } + m, ok := arr[0].(map[string]interface{}) + if !ok || m["uuid"] != "1" { + t.Fatalf("unexpected payload: %+v", resp.Data) + } +} diff --git a/handler/experience_test.go b/handler/experience_test.go index aca6a479..ebb82e47 100644 --- a/handler/experience_test.go +++ b/handler/experience_test.go @@ -1,7 +1,10 @@ package handler import ( + "encoding/json" "net/http" + "net/http/httptest" + "os" "testing" pkghttp "github.com/oullin/pkg/http" @@ -15,3 +18,29 @@ func TestExperienceHandlerHandle(t *testing.T) { return h }) } + +func TestExperienceHandlerHandle_Payload(t *testing.T) { + file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + defer os.Remove(file) + + h := MakeExperienceHandler(file) + req := httptest.NewRequest("GET", "/experience", nil) + rec := httptest.NewRecorder() + + if err := h.Handle(rec, req); err != nil { + t.Fatalf("err: %v", err) + } + + var resp testEnvelope + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + arr, ok := resp.Data.([]interface{}) + if !ok || len(arr) != 1 { + t.Fatalf("unexpected data: %+v", resp.Data) + } + m, ok := arr[0].(map[string]interface{}) + if !ok || m["uuid"] != "1" { + t.Fatalf("unexpected payload: %+v", resp.Data) + } +} diff --git a/handler/posts_test.go b/handler/posts_test.go index ed9da213..86b95a42 100644 --- a/handler/posts_test.go +++ b/handler/posts_test.go @@ -2,10 +2,20 @@ package handler import ( "bytes" + "encoding/json" + "net/http" "net/http/httptest" + "reflect" "testing" + "time" + "unsafe" + "github.com/oullin/database" "github.com/oullin/database/repository" + "github.com/oullin/database/repository/pagination" + "github.com/oullin/handler/payload" + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) func TestPostsHandlerIndex_ParseError(t *testing.T) { @@ -25,3 +35,82 @@ func TestPostsHandlerShow_MissingSlug(t *testing.T) { t.Fatalf("expected bad request") } } + +func makePostsRepo(t *testing.T) *repository.Posts { + t.Helper() + + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("open db: %v", err) + } + + if err := db.AutoMigrate(&database.User{}, &database.Post{}, &database.Category{}, &database.Tag{}, &database.PostCategory{}, &database.PostTag{}); err != nil { + t.Fatalf("migrate: %v", err) + } + + conn := &database.Connection{} + rv := reflect.ValueOf(conn).Elem() + driverField := rv.FieldByName("driver") + reflect.NewAt(driverField.Type(), unsafe.Pointer(driverField.UnsafeAddr())).Elem().Set(reflect.ValueOf(db)) + nameField := rv.FieldByName("driverName") + reflect.NewAt(nameField.Type(), unsafe.Pointer(nameField.UnsafeAddr())).Elem().SetString("sqlite") + + author := database.User{ID: 1, UUID: "u1", Username: "user", FirstName: "F", LastName: "L", Email: "u@example.com", PasswordHash: "x"} + if err := db.Create(&author).Error; err != nil { + t.Fatalf("create user: %v", err) + } + published := time.Now() + post := database.Post{UUID: "p1", AuthorID: author.ID, Slug: "hello", Title: "Hello", Excerpt: "Ex", Content: "Body", PublishedAt: &published} + if err := db.Create(&post).Error; err != nil { + t.Fatalf("create post: %v", err) + } + + return &repository.Posts{DB: conn} +} + +func TestPostsHandlerIndex_Success(t *testing.T) { + repo := makePostsRepo(t) + h := MakePostsHandler(repo) + + req := httptest.NewRequest("POST", "/posts", bytes.NewReader([]byte("{}"))) + rec := httptest.NewRecorder() + + if err := h.Index(rec, req); err != nil { + t.Fatalf("index err: %v", err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status %d", rec.Code) + } + + var resp pagination.Pagination[payload.PostResponse] + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if len(resp.Data) != 1 || resp.Data[0].Slug != "hello" { + t.Fatalf("unexpected data: %+v", resp.Data) + } +} + +func TestPostsHandlerShow_Success(t *testing.T) { + repo := makePostsRepo(t) + h := MakePostsHandler(repo) + + req := httptest.NewRequest("GET", "/posts/hello", nil) + req.SetPathValue("slug", "hello") + rec := httptest.NewRecorder() + + if err := h.Show(rec, req); err != nil { + t.Fatalf("show err: %v", err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status %d", rec.Code) + } + + var resp payload.PostResponse + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if resp.Slug != "hello" { + t.Fatalf("unexpected slug: %s", resp.Slug) + } +} diff --git a/handler/profile_test.go b/handler/profile_test.go index dc11cba8..4243473d 100644 --- a/handler/profile_test.go +++ b/handler/profile_test.go @@ -1,7 +1,10 @@ package handler import ( + "encoding/json" "net/http" + "net/http/httptest" + "os" "testing" pkghttp "github.com/oullin/pkg/http" @@ -15,3 +18,25 @@ func TestProfileHandlerHandle(t *testing.T) { return h }) } + +func TestProfileHandlerHandle_Payload(t *testing.T) { + file := writeJSON(t, testEnvelope{Version: "v1", Data: map[string]string{"nickname": "nick"}}) + defer os.Remove(file) + + h := MakeProfileHandler(file) + req := httptest.NewRequest("GET", "/profile", nil) + rec := httptest.NewRecorder() + + if err := h.Handle(rec, req); err != nil { + t.Fatalf("err: %v", err) + } + + var resp testEnvelope + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + obj, ok := resp.Data.(map[string]interface{}) + if !ok || obj["nickname"] != "nick" { + t.Fatalf("unexpected payload: %+v", resp.Data) + } +} diff --git a/handler/projects_test.go b/handler/projects_test.go index 938545fc..12d66fa1 100644 --- a/handler/projects_test.go +++ b/handler/projects_test.go @@ -1,7 +1,10 @@ package handler import ( + "encoding/json" "net/http" + "net/http/httptest" + "os" "testing" pkghttp "github.com/oullin/pkg/http" @@ -15,3 +18,29 @@ func TestProjectsHandlerHandle(t *testing.T) { return h }) } + +func TestProjectsHandlerHandle_Payload(t *testing.T) { + file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + defer os.Remove(file) + + h := MakeProjectsHandler(file) + req := httptest.NewRequest("GET", "/projects", nil) + rec := httptest.NewRecorder() + + if err := h.Handle(rec, req); err != nil { + t.Fatalf("err: %v", err) + } + + var resp testEnvelope + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + arr, ok := resp.Data.([]interface{}) + if !ok || len(arr) != 1 { + t.Fatalf("unexpected data: %+v", resp.Data) + } + m, ok := arr[0].(map[string]interface{}) + if !ok || m["uuid"] != "1" { + t.Fatalf("unexpected payload: %+v", resp.Data) + } +} diff --git a/handler/social_test.go b/handler/social_test.go index 83e7eaa3..a3238cd0 100644 --- a/handler/social_test.go +++ b/handler/social_test.go @@ -1,7 +1,10 @@ package handler import ( + "encoding/json" "net/http" + "net/http/httptest" + "os" "testing" pkghttp "github.com/oullin/pkg/http" @@ -15,3 +18,29 @@ func TestSocialHandlerHandle(t *testing.T) { return h }) } + +func TestSocialHandlerHandle_Payload(t *testing.T) { + file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + defer os.Remove(file) + + h := MakeSocialHandler(file) + req := httptest.NewRequest("GET", "/social", nil) + rec := httptest.NewRecorder() + + if err := h.Handle(rec, req); err != nil { + t.Fatalf("err: %v", err) + } + + var resp testEnvelope + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + arr, ok := resp.Data.([]interface{}) + if !ok || len(arr) != 1 { + t.Fatalf("unexpected data: %+v", resp.Data) + } + m, ok := arr[0].(map[string]interface{}) + if !ok || m["uuid"] != "1" { + t.Fatalf("unexpected payload: %+v", resp.Data) + } +} diff --git a/handler/talks_test.go b/handler/talks_test.go index 5391f779..29207575 100644 --- a/handler/talks_test.go +++ b/handler/talks_test.go @@ -1,7 +1,10 @@ package handler import ( + "encoding/json" "net/http" + "net/http/httptest" + "os" "testing" pkghttp "github.com/oullin/pkg/http" @@ -15,3 +18,29 @@ func TestTalksHandlerHandle(t *testing.T) { return h }) } + +func TestTalksHandlerHandle_Payload(t *testing.T) { + file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + defer os.Remove(file) + + h := MakeTalksHandler(file) + req := httptest.NewRequest("GET", "/talks", nil) + rec := httptest.NewRecorder() + + if err := h.Handle(rec, req); err != nil { + t.Fatalf("err: %v", err) + } + + var resp testEnvelope + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + arr, ok := resp.Data.([]interface{}) + if !ok || len(arr) != 1 { + t.Fatalf("unexpected data: %+v", resp.Data) + } + m, ok := arr[0].(map[string]interface{}) + if !ok || m["uuid"] != "1" { + t.Fatalf("unexpected payload: %+v", resp.Data) + } +} From e5b66f49b0309c55b7a314a1551aa802b00a1800 Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 6 Aug 2025 11:16:42 +0800 Subject: [PATCH 09/16] test: run posts handler against postgres container --- go.mod | 2 -- go.sum | 4 --- handler/posts_test.go | 66 ++++++++++++++++++++++++++++++++----------- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index 3a9206dd..047b8e5d 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,6 @@ require ( golang.org/x/text v0.27.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.6.0 - gorm.io/driver/sqlite v1.5.5 gorm.io/gorm v1.30.0 ) @@ -54,7 +53,6 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect - github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.1.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect diff --git a/go.sum b/go.sum index b7602bfe..5920ec74 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,6 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= -github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -237,8 +235,6 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= -gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= -gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= diff --git a/handler/posts_test.go b/handler/posts_test.go index 86b95a42..55f1a989 100644 --- a/handler/posts_test.go +++ b/handler/posts_test.go @@ -2,20 +2,21 @@ package handler import ( "bytes" + "context" "encoding/json" "net/http" "net/http/httptest" - "reflect" + "os/exec" "testing" "time" - "unsafe" "github.com/oullin/database" "github.com/oullin/database/repository" "github.com/oullin/database/repository/pagination" + "github.com/oullin/env" "github.com/oullin/handler/payload" - "gorm.io/driver/sqlite" - "gorm.io/gorm" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" ) func TestPostsHandlerIndex_ParseError(t *testing.T) { @@ -39,29 +40,62 @@ func TestPostsHandlerShow_MissingSlug(t *testing.T) { func makePostsRepo(t *testing.T) *repository.Posts { t.Helper() - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if _, err := exec.LookPath("docker"); err != nil { + t.Skip("docker not installed") + } + + ctx := context.Background() + pg, err := postgres.RunContainer(ctx, + testcontainers.WithImage("postgres:16-alpine"), + postgres.WithDatabase("testdb"), + postgres.WithUsername("test"), + postgres.WithPassword("secret"), + postgres.BasicWaitStrategies(), + ) if err != nil { - t.Fatalf("open db: %v", err) + t.Fatalf("container run err: %v", err) } + t.Cleanup(func() { pg.Terminate(ctx) }) - if err := db.AutoMigrate(&database.User{}, &database.Post{}, &database.Category{}, &database.Tag{}, &database.PostCategory{}, &database.PostTag{}); err != nil { - t.Fatalf("migrate: %v", err) + host, err := pg.Host(ctx) + if err != nil { + t.Fatalf("host err: %v", err) + } + port, err := pg.MappedPort(ctx, "5432/tcp") + if err != nil { + t.Fatalf("port err: %v", err) } - conn := &database.Connection{} - rv := reflect.ValueOf(conn).Elem() - driverField := rv.FieldByName("driver") - reflect.NewAt(driverField.Type(), unsafe.Pointer(driverField.UnsafeAddr())).Elem().Set(reflect.ValueOf(db)) - nameField := rv.FieldByName("driverName") - reflect.NewAt(nameField.Type(), unsafe.Pointer(nameField.UnsafeAddr())).Elem().SetString("sqlite") + e := &env.Environment{ + DB: env.DBEnvironment{ + UserName: "test", + UserPassword: "secret", + DatabaseName: "testdb", + Port: port.Int(), + Host: host, + DriverName: database.DriverName, + SSLMode: "disable", + TimeZone: "UTC", + }, + } + + conn, err := database.MakeConnection(e) + if err != nil { + t.Fatalf("make connection: %v", err) + } + t.Cleanup(func() { conn.Close() }) + + if err := conn.Sql().AutoMigrate(&database.User{}, &database.Post{}, &database.Category{}, &database.Tag{}, &database.PostCategory{}, &database.PostTag{}); err != nil { + t.Fatalf("migrate: %v", err) + } author := database.User{ID: 1, UUID: "u1", Username: "user", FirstName: "F", LastName: "L", Email: "u@example.com", PasswordHash: "x"} - if err := db.Create(&author).Error; err != nil { + if err := conn.Sql().Create(&author).Error; err != nil { t.Fatalf("create user: %v", err) } published := time.Now() post := database.Post{UUID: "p1", AuthorID: author.ID, Slug: "hello", Title: "Hello", Excerpt: "Ex", Content: "Body", PublishedAt: &published} - if err := db.Create(&post).Error; err != nil { + if err := conn.Sql().Create(&post).Error; err != nil { t.Fatalf("create post: %v", err) } From 404b2a125f338a7c8c9f02cb8b988b864b8e7bed Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 6 Aug 2025 11:32:53 +0800 Subject: [PATCH 10/16] test: add categories postgres test helpers --- handler/categories_test.go | 117 ++++++++++++++++++++++++++++++ handler/education_test.go | 51 ++++++++++--- handler/experience_test.go | 51 ++++++++++--- handler/file_handler_test.go | 124 -------------------------------- handler/profile_test.go | 51 ++++++++++--- handler/projects_test.go | 51 ++++++++++--- handler/recommendations_test.go | 54 +++++++++++--- handler/social_test.go | 51 ++++++++++--- handler/talks_test.go | 51 ++++++++++--- handler/test_helpers_test.go | 25 +++++++ 10 files changed, 446 insertions(+), 180 deletions(-) create mode 100644 handler/categories_test.go delete mode 100644 handler/file_handler_test.go create mode 100644 handler/test_helpers_test.go diff --git a/handler/categories_test.go b/handler/categories_test.go new file mode 100644 index 00000000..9e9b249a --- /dev/null +++ b/handler/categories_test.go @@ -0,0 +1,117 @@ +package handler + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "os/exec" + "testing" + "time" + + "github.com/oullin/database" + "github.com/oullin/database/repository" + "github.com/oullin/database/repository/pagination" + "github.com/oullin/env" + "github.com/oullin/handler/payload" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" +) + +func makeCategoriesRepo(t *testing.T) *repository.Categories { + t.Helper() + + if _, err := exec.LookPath("docker"); err != nil { + t.Skip("docker not installed") + } + + ctx := context.Background() + pg, err := postgres.RunContainer(ctx, + testcontainers.WithImage("postgres:16-alpine"), + postgres.WithDatabase("testdb"), + postgres.WithUsername("test"), + postgres.WithPassword("secret"), + postgres.BasicWaitStrategies(), + ) + if err != nil { + t.Fatalf("container run err: %v", err) + } + t.Cleanup(func() { pg.Terminate(ctx) }) + + host, err := pg.Host(ctx) + if err != nil { + t.Fatalf("host err: %v", err) + } + port, err := pg.MappedPort(ctx, "5432/tcp") + if err != nil { + t.Fatalf("port err: %v", err) + } + + e := &env.Environment{ + DB: env.DBEnvironment{ + UserName: "test", + UserPassword: "secret", + DatabaseName: "testdb", + Port: port.Int(), + Host: host, + DriverName: database.DriverName, + SSLMode: "disable", + TimeZone: "UTC", + }, + } + + conn, err := database.MakeConnection(e) + if err != nil { + t.Fatalf("make connection: %v", err) + } + t.Cleanup(func() { conn.Close() }) + + if err := conn.Sql().AutoMigrate(&database.User{}, &database.Post{}, &database.Category{}, &database.PostCategory{}); err != nil { + t.Fatalf("migrate: %v", err) + } + + author := database.User{ID: 1, UUID: "u1", Username: "user", FirstName: "F", LastName: "L", Email: "u@example.com", PasswordHash: "x"} + if err := conn.Sql().Create(&author).Error; err != nil { + t.Fatalf("create user: %v", err) + } + published := time.Now() + post := database.Post{UUID: "p1", AuthorID: author.ID, Slug: "hello", Title: "Hello", Excerpt: "Ex", Content: "Body", PublishedAt: &published} + if err := conn.Sql().Create(&post).Error; err != nil { + t.Fatalf("create post: %v", err) + } + + cat := database.Category{UUID: "c1", Name: "Cat", Slug: "cat", Description: "desc"} + if err := conn.Sql().Create(&cat).Error; err != nil { + t.Fatalf("create category: %v", err) + } + + link := database.PostCategory{PostID: post.ID, CategoryID: cat.ID} + if err := conn.Sql().Create(&link).Error; err != nil { + t.Fatalf("create link: %v", err) + } + + return &repository.Categories{DB: conn} +} + +func TestCategoriesHandlerIndex_Success(t *testing.T) { + repo := makeCategoriesRepo(t) + h := MakeCategoriesHandler(repo) + + req := httptest.NewRequest("GET", "/categories", nil) + rec := httptest.NewRecorder() + + if err := h.Index(rec, req); err != nil { + t.Fatalf("index err: %v", err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status %d", rec.Code) + } + + var resp pagination.Pagination[payload.CategoryResponse] + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if len(resp.Data) != 1 || resp.Data[0].Slug != "cat" { + t.Fatalf("unexpected data: %+v", resp.Data) + } +} diff --git a/handler/education_test.go b/handler/education_test.go index 0620bff4..73bae288 100644 --- a/handler/education_test.go +++ b/handler/education_test.go @@ -6,17 +6,52 @@ import ( "net/http/httptest" "os" "testing" - - pkghttp "github.com/oullin/pkg/http" ) func TestEducationHandlerHandle(t *testing.T) { - runFileHandlerTest(t, "/education", []map[string]string{{"uuid": "1"}}, func(p string) interface { - Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError - } { - h := MakeEducationHandler(p) - return h - }) + file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + defer os.Remove(file) + + h := MakeEducationHandler(file) + req := httptest.NewRequest("GET", "/education", nil) + rec := httptest.NewRecorder() + + if err := h.Handle(rec, req); err != nil { + t.Fatalf("err: %v", err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status %d", rec.Code) + } + + var resp testEnvelope + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if resp.Version != "v1" { + t.Fatalf("version %s", resp.Version) + } + + req2 := httptest.NewRequest("GET", "/education", nil) + req2.Header.Set("If-None-Match", "\"v1\"") + rec2 := httptest.NewRecorder() + if err := h.Handle(rec2, req2); err != nil { + t.Fatalf("err: %v", err) + } + if rec2.Code != http.StatusNotModified { + t.Fatalf("status %d", rec2.Code) + } + + badF, _ := os.CreateTemp("", "bad.json") + badF.WriteString("{") + badF.Close() + defer os.Remove(badF.Name()) + + bad := MakeEducationHandler(badF.Name()) + rec3 := httptest.NewRecorder() + req3 := httptest.NewRequest("GET", "/education", nil) + if bad.Handle(rec3, req3) == nil { + t.Fatalf("expected error") + } } func TestEducationHandlerHandle_Payload(t *testing.T) { diff --git a/handler/experience_test.go b/handler/experience_test.go index ebb82e47..d1f8159a 100644 --- a/handler/experience_test.go +++ b/handler/experience_test.go @@ -6,17 +6,52 @@ import ( "net/http/httptest" "os" "testing" - - pkghttp "github.com/oullin/pkg/http" ) func TestExperienceHandlerHandle(t *testing.T) { - runFileHandlerTest(t, "/experience", []map[string]string{{"uuid": "1"}}, func(p string) interface { - Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError - } { - h := MakeExperienceHandler(p) - return h - }) + file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + defer os.Remove(file) + + h := MakeExperienceHandler(file) + req := httptest.NewRequest("GET", "/experience", nil) + rec := httptest.NewRecorder() + + if err := h.Handle(rec, req); err != nil { + t.Fatalf("err: %v", err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status %d", rec.Code) + } + + var resp testEnvelope + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if resp.Version != "v1" { + t.Fatalf("version %s", resp.Version) + } + + req2 := httptest.NewRequest("GET", "/experience", nil) + req2.Header.Set("If-None-Match", "\"v1\"") + rec2 := httptest.NewRecorder() + if err := h.Handle(rec2, req2); err != nil { + t.Fatalf("err: %v", err) + } + if rec2.Code != http.StatusNotModified { + t.Fatalf("status %d", rec2.Code) + } + + badF, _ := os.CreateTemp("", "bad.json") + badF.WriteString("{") + badF.Close() + defer os.Remove(badF.Name()) + + bad := MakeExperienceHandler(badF.Name()) + rec3 := httptest.NewRecorder() + req3 := httptest.NewRequest("GET", "/experience", nil) + if bad.Handle(rec3, req3) == nil { + t.Fatalf("expected error") + } } func TestExperienceHandlerHandle_Payload(t *testing.T) { diff --git a/handler/file_handler_test.go b/handler/file_handler_test.go deleted file mode 100644 index 35a9976b..00000000 --- a/handler/file_handler_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package handler - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "reflect" - "testing" - - pkghttp "github.com/oullin/pkg/http" -) - -type testEnvelope struct { - Version string `json:"version"` - Data interface{} `json:"data"` -} - -func writeJSON(t *testing.T, v interface{}) string { - f, err := os.CreateTemp("", "data.json") - if err != nil { - t.Fatalf("tmp: %v", err) - } - enc := json.NewEncoder(f) - if err := enc.Encode(v); err != nil { - t.Fatalf("encode: %v", err) - } - f.Close() - return f.Name() -} - -func runFileHandlerTest(t *testing.T, path string, data interface{}, makeFn func(string) interface { - Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError -}) { - file := writeJSON(t, testEnvelope{Version: "v1", Data: data}) - defer os.Remove(file) - h := makeFn(file) - - req := httptest.NewRequest("GET", path, nil) - rec := httptest.NewRecorder() - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - if rec.Code != http.StatusOK { - t.Fatalf("status %d", rec.Code) - } - - var resp testEnvelope - if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - if resp.Version != "v1" { - t.Fatalf("version %s", resp.Version) - } - expectedBytes, err := json.Marshal(data) - if err != nil { - t.Fatalf("marshal expected: %v", err) - } - var expectedVal interface{} - if err := json.Unmarshal(expectedBytes, &expectedVal); err != nil { - t.Fatalf("unmarshal expected: %v", err) - } - gotBytes, err := json.Marshal(resp.Data) - if err != nil { - t.Fatalf("marshal got: %v", err) - } - var gotVal interface{} - if err := json.Unmarshal(gotBytes, &gotVal); err != nil { - t.Fatalf("unmarshal got: %v", err) - } - if !subset(expectedVal, gotVal) { - t.Fatalf("payload %v does not contain %v", gotVal, expectedVal) - } - - req2 := httptest.NewRequest("GET", path, nil) - req2.Header.Set("If-None-Match", "\"v1\"") - rec2 := httptest.NewRecorder() - if err := h.Handle(rec2, req2); err != nil { - t.Fatalf("err: %v", err) - } - if rec2.Code != http.StatusNotModified { - t.Fatalf("status %d", rec2.Code) - } - - badF, _ := os.CreateTemp("", "bad.json") - badF.WriteString("{") - badF.Close() - defer os.Remove(badF.Name()) - bad := makeFn(badF.Name()) - rec3 := httptest.NewRecorder() - req3 := httptest.NewRequest("GET", path, nil) - if bad.Handle(rec3, req3) == nil { - t.Fatalf("expected error") - } -} - -func subset(expected, got interface{}) bool { - switch e := expected.(type) { - case map[string]interface{}: - g, ok := got.(map[string]interface{}) - if !ok { - return false - } - for k, v := range e { - if !subset(v, g[k]) { - return false - } - } - return true - case []interface{}: - g, ok := got.([]interface{}) - if !ok || len(e) != len(g) { - return false - } - for i := range e { - if !subset(e[i], g[i]) { - return false - } - } - return true - default: - return reflect.DeepEqual(expected, got) - } -} diff --git a/handler/profile_test.go b/handler/profile_test.go index 4243473d..5d9cbb72 100644 --- a/handler/profile_test.go +++ b/handler/profile_test.go @@ -6,17 +6,52 @@ import ( "net/http/httptest" "os" "testing" - - pkghttp "github.com/oullin/pkg/http" ) func TestProfileHandlerHandle(t *testing.T) { - runFileHandlerTest(t, "/profile", map[string]string{"nickname": "nick"}, func(p string) interface { - Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError - } { - h := MakeProfileHandler(p) - return h - }) + file := writeJSON(t, testEnvelope{Version: "v1", Data: map[string]string{"nickname": "nick"}}) + defer os.Remove(file) + + h := MakeProfileHandler(file) + req := httptest.NewRequest("GET", "/profile", nil) + rec := httptest.NewRecorder() + + if err := h.Handle(rec, req); err != nil { + t.Fatalf("err: %v", err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status %d", rec.Code) + } + + var resp testEnvelope + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if resp.Version != "v1" { + t.Fatalf("version %s", resp.Version) + } + + req2 := httptest.NewRequest("GET", "/profile", nil) + req2.Header.Set("If-None-Match", "\"v1\"") + rec2 := httptest.NewRecorder() + if err := h.Handle(rec2, req2); err != nil { + t.Fatalf("err: %v", err) + } + if rec2.Code != http.StatusNotModified { + t.Fatalf("status %d", rec2.Code) + } + + badF, _ := os.CreateTemp("", "bad.json") + badF.WriteString("{") + badF.Close() + defer os.Remove(badF.Name()) + + bad := MakeProfileHandler(badF.Name()) + rec3 := httptest.NewRecorder() + req3 := httptest.NewRequest("GET", "/profile", nil) + if bad.Handle(rec3, req3) == nil { + t.Fatalf("expected error") + } } func TestProfileHandlerHandle_Payload(t *testing.T) { diff --git a/handler/projects_test.go b/handler/projects_test.go index 12d66fa1..589bfece 100644 --- a/handler/projects_test.go +++ b/handler/projects_test.go @@ -6,17 +6,52 @@ import ( "net/http/httptest" "os" "testing" - - pkghttp "github.com/oullin/pkg/http" ) func TestProjectsHandlerHandle(t *testing.T) { - runFileHandlerTest(t, "/projects", []map[string]string{{"uuid": "1"}}, func(p string) interface { - Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError - } { - h := MakeProjectsHandler(p) - return h - }) + file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + defer os.Remove(file) + + h := MakeProjectsHandler(file) + req := httptest.NewRequest("GET", "/projects", nil) + rec := httptest.NewRecorder() + + if err := h.Handle(rec, req); err != nil { + t.Fatalf("err: %v", err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status %d", rec.Code) + } + + var resp testEnvelope + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if resp.Version != "v1" { + t.Fatalf("version %s", resp.Version) + } + + req2 := httptest.NewRequest("GET", "/projects", nil) + req2.Header.Set("If-None-Match", "\"v1\"") + rec2 := httptest.NewRecorder() + if err := h.Handle(rec2, req2); err != nil { + t.Fatalf("err: %v", err) + } + if rec2.Code != http.StatusNotModified { + t.Fatalf("status %d", rec2.Code) + } + + badF, _ := os.CreateTemp("", "bad.json") + badF.WriteString("{") + badF.Close() + defer os.Remove(badF.Name()) + + bad := MakeProjectsHandler(badF.Name()) + rec3 := httptest.NewRecorder() + req3 := httptest.NewRequest("GET", "/projects", nil) + if bad.Handle(rec3, req3) == nil { + t.Fatalf("expected error") + } } func TestProjectsHandlerHandle_Payload(t *testing.T) { diff --git a/handler/recommendations_test.go b/handler/recommendations_test.go index be41fda5..f26bd628 100644 --- a/handler/recommendations_test.go +++ b/handler/recommendations_test.go @@ -1,17 +1,55 @@ package handler import ( + "encoding/json" "net/http" + "net/http/httptest" + "os" "testing" - - pkghttp "github.com/oullin/pkg/http" ) func TestRecommendationsHandlerHandle(t *testing.T) { - runFileHandlerTest(t, "/recommendations", []map[string]string{{"uuid": "1"}}, func(p string) interface { - Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError - } { - h := MakeRecommendationsHandler(p) - return h - }) + file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + defer os.Remove(file) + + h := MakeRecommendationsHandler(file) + req := httptest.NewRequest("GET", "/recommendations", nil) + rec := httptest.NewRecorder() + + if err := h.Handle(rec, req); err != nil { + t.Fatalf("err: %v", err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status %d", rec.Code) + } + + var resp testEnvelope + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if resp.Version != "v1" { + t.Fatalf("version %s", resp.Version) + } + + req2 := httptest.NewRequest("GET", "/recommendations", nil) + req2.Header.Set("If-None-Match", "\"v1\"") + rec2 := httptest.NewRecorder() + if err := h.Handle(rec2, req2); err != nil { + t.Fatalf("err: %v", err) + } + if rec2.Code != http.StatusNotModified { + t.Fatalf("status %d", rec2.Code) + } + + badF, _ := os.CreateTemp("", "bad.json") + badF.WriteString("{") + badF.Close() + defer os.Remove(badF.Name()) + + bad := MakeRecommendationsHandler(badF.Name()) + rec3 := httptest.NewRecorder() + req3 := httptest.NewRequest("GET", "/recommendations", nil) + if bad.Handle(rec3, req3) == nil { + t.Fatalf("expected error") + } } diff --git a/handler/social_test.go b/handler/social_test.go index a3238cd0..12b51720 100644 --- a/handler/social_test.go +++ b/handler/social_test.go @@ -6,17 +6,52 @@ import ( "net/http/httptest" "os" "testing" - - pkghttp "github.com/oullin/pkg/http" ) func TestSocialHandlerHandle(t *testing.T) { - runFileHandlerTest(t, "/social", []map[string]string{{"uuid": "1"}}, func(p string) interface { - Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError - } { - h := MakeSocialHandler(p) - return h - }) + file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + defer os.Remove(file) + + h := MakeSocialHandler(file) + req := httptest.NewRequest("GET", "/social", nil) + rec := httptest.NewRecorder() + + if err := h.Handle(rec, req); err != nil { + t.Fatalf("err: %v", err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status %d", rec.Code) + } + + var resp testEnvelope + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if resp.Version != "v1" { + t.Fatalf("version %s", resp.Version) + } + + req2 := httptest.NewRequest("GET", "/social", nil) + req2.Header.Set("If-None-Match", "\"v1\"") + rec2 := httptest.NewRecorder() + if err := h.Handle(rec2, req2); err != nil { + t.Fatalf("err: %v", err) + } + if rec2.Code != http.StatusNotModified { + t.Fatalf("status %d", rec2.Code) + } + + badF, _ := os.CreateTemp("", "bad.json") + badF.WriteString("{") + badF.Close() + defer os.Remove(badF.Name()) + + bad := MakeSocialHandler(badF.Name()) + rec3 := httptest.NewRecorder() + req3 := httptest.NewRequest("GET", "/social", nil) + if bad.Handle(rec3, req3) == nil { + t.Fatalf("expected error") + } } func TestSocialHandlerHandle_Payload(t *testing.T) { diff --git a/handler/talks_test.go b/handler/talks_test.go index 29207575..35556262 100644 --- a/handler/talks_test.go +++ b/handler/talks_test.go @@ -6,17 +6,52 @@ import ( "net/http/httptest" "os" "testing" - - pkghttp "github.com/oullin/pkg/http" ) func TestTalksHandlerHandle(t *testing.T) { - runFileHandlerTest(t, "/talks", []map[string]string{{"uuid": "1"}}, func(p string) interface { - Handle(http.ResponseWriter, *http.Request) *pkghttp.ApiError - } { - h := MakeTalksHandler(p) - return h - }) + file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + defer os.Remove(file) + + h := MakeTalksHandler(file) + req := httptest.NewRequest("GET", "/talks", nil) + rec := httptest.NewRecorder() + + if err := h.Handle(rec, req); err != nil { + t.Fatalf("err: %v", err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status %d", rec.Code) + } + + var resp testEnvelope + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if resp.Version != "v1" { + t.Fatalf("version %s", resp.Version) + } + + req2 := httptest.NewRequest("GET", "/talks", nil) + req2.Header.Set("If-None-Match", "\"v1\"") + rec2 := httptest.NewRecorder() + if err := h.Handle(rec2, req2); err != nil { + t.Fatalf("err: %v", err) + } + if rec2.Code != http.StatusNotModified { + t.Fatalf("status %d", rec2.Code) + } + + badF, _ := os.CreateTemp("", "bad.json") + badF.WriteString("{") + badF.Close() + defer os.Remove(badF.Name()) + + bad := MakeTalksHandler(badF.Name()) + rec3 := httptest.NewRecorder() + req3 := httptest.NewRequest("GET", "/talks", nil) + if bad.Handle(rec3, req3) == nil { + t.Fatalf("expected error") + } } func TestTalksHandlerHandle_Payload(t *testing.T) { diff --git a/handler/test_helpers_test.go b/handler/test_helpers_test.go new file mode 100644 index 00000000..1ab1c3cc --- /dev/null +++ b/handler/test_helpers_test.go @@ -0,0 +1,25 @@ +package handler + +import ( + "encoding/json" + "os" + "testing" +) + +type testEnvelope struct { + Version string `json:"version"` + Data interface{} `json:"data"` +} + +func writeJSON(t *testing.T, v interface{}) string { + t.Helper() + f, err := os.CreateTemp("", "data.json") + if err != nil { + t.Fatalf("tmp: %v", err) + } + if err := json.NewEncoder(f).Encode(v); err != nil { + t.Fatalf("encode: %v", err) + } + f.Close() + return f.Name() +} From a5b183d82d8985e561c80783125496fec091ce88 Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 6 Aug 2025 11:49:37 +0800 Subject: [PATCH 11/16] test: add payload response coverage --- handler/payload/education_test.go | 17 +++++++++++++++++ handler/payload/experience_test.go | 17 +++++++++++++++++ handler/payload/profile_test.go | 17 +++++++++++++++++ handler/payload/projects_test.go | 17 +++++++++++++++++ handler/payload/recommendations_test.go | 17 +++++++++++++++++ handler/payload/social_test.go | 17 +++++++++++++++++ handler/payload/talks_test.go | 17 +++++++++++++++++ handler/payload/users_test.go | 17 +++++++++++++++++ 8 files changed, 136 insertions(+) create mode 100644 handler/payload/education_test.go create mode 100644 handler/payload/experience_test.go create mode 100644 handler/payload/profile_test.go create mode 100644 handler/payload/projects_test.go create mode 100644 handler/payload/recommendations_test.go create mode 100644 handler/payload/social_test.go create mode 100644 handler/payload/talks_test.go create mode 100644 handler/payload/users_test.go diff --git a/handler/payload/education_test.go b/handler/payload/education_test.go new file mode 100644 index 00000000..7fe134e7 --- /dev/null +++ b/handler/payload/education_test.go @@ -0,0 +1,17 @@ +package payload + +import ( + "encoding/json" + "testing" +) + +func TestEducationResponseJSON(t *testing.T) { + body := []byte(`{"version":"v1","data":[{"uuid":"u","icon":"i","school":"s","degree":"d","field":"f","description":"desc","graduated_at":"g","issuing_country":"c"}]}`) + var res EducationResponse + if err := json.Unmarshal(body, &res); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if res.Version != "v1" || len(res.Data) != 1 || res.Data[0].UUID != "u" { + t.Fatalf("unexpected response: %+v", res) + } +} diff --git a/handler/payload/experience_test.go b/handler/payload/experience_test.go new file mode 100644 index 00000000..4e1471e4 --- /dev/null +++ b/handler/payload/experience_test.go @@ -0,0 +1,17 @@ +package payload + +import ( + "encoding/json" + "testing" +) + +func TestExperienceResponseJSON(t *testing.T) { + body := []byte(`{"version":"v1","data":[{"uuid":"u","company":"c","employment_type":"e","location_type":"l","position":"p","start_date":"sd","end_date":"ed","summary":"s","country":"co","city":"ci","skills":"sk"}]}`) + var res ExperienceResponse + if err := json.Unmarshal(body, &res); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if res.Version != "v1" || len(res.Data) != 1 || res.Data[0].UUID != "u" { + t.Fatalf("unexpected response: %+v", res) + } +} diff --git a/handler/payload/profile_test.go b/handler/payload/profile_test.go new file mode 100644 index 00000000..e7d0d358 --- /dev/null +++ b/handler/payload/profile_test.go @@ -0,0 +1,17 @@ +package payload + +import ( + "encoding/json" + "testing" +) + +func TestProfileResponseJSON(t *testing.T) { + body := []byte(`{"version":"v1","data":{"nickname":"n","handle":"h","name":"nm","email":"e","profession":"p","skills":[{"uuid":"u","percentage":1,"item":"i","description":"d"}]}}`) + var res ProfileResponse + if err := json.Unmarshal(body, &res); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if res.Version != "v1" || res.Data.Handle != "h" || len(res.Data.Skills) != 1 || res.Data.Skills[0].Uuid != "u" { + t.Fatalf("unexpected response: %+v", res) + } +} diff --git a/handler/payload/projects_test.go b/handler/payload/projects_test.go new file mode 100644 index 00000000..f420d72d --- /dev/null +++ b/handler/payload/projects_test.go @@ -0,0 +1,17 @@ +package payload + +import ( + "encoding/json" + "testing" +) + +func TestProjectsResponseJSON(t *testing.T) { + body := []byte(`{"version":"v1","data":[{"uuid":"u","language":"l","title":"t","excerpt":"e","url":"u","icon":"i","is_open_source":true,"created_at":"c","updated_at":"up"}]}`) + var res ProjectsResponse + if err := json.Unmarshal(body, &res); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if res.Version != "v1" || len(res.Data) != 1 || !res.Data[0].IsOpenSource { + t.Fatalf("unexpected response: %+v", res) + } +} diff --git a/handler/payload/recommendations_test.go b/handler/payload/recommendations_test.go new file mode 100644 index 00000000..ae90e733 --- /dev/null +++ b/handler/payload/recommendations_test.go @@ -0,0 +1,17 @@ +package payload + +import ( + "encoding/json" + "testing" +) + +func TestRecommendationsResponseJSON(t *testing.T) { + body := []byte(`{"version":"v1","data":[{"uuid":"u","relation":"r","text":"t","created_at":"c","updated_at":"u","person":{"avatar":"a","full_name":"f","company":"co","designation":"d"}}]}`) + var res RecommendationsResponse + if err := json.Unmarshal(body, &res); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if res.Version != "v1" || len(res.Data) != 1 || res.Data[0].Person.FullName != "f" { + t.Fatalf("unexpected response: %+v", res) + } +} diff --git a/handler/payload/social_test.go b/handler/payload/social_test.go new file mode 100644 index 00000000..9926b7a3 --- /dev/null +++ b/handler/payload/social_test.go @@ -0,0 +1,17 @@ +package payload + +import ( + "encoding/json" + "testing" +) + +func TestSocialResponseJSON(t *testing.T) { + body := []byte(`{"version":"v1","data":[{"uuid":"u","handle":"h","url":"u","description":"d","name":"n"}]}`) + var res SocialResponse + if err := json.Unmarshal(body, &res); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if res.Version != "v1" || len(res.Data) != 1 || res.Data[0].Name != "n" { + t.Fatalf("unexpected response: %+v", res) + } +} diff --git a/handler/payload/talks_test.go b/handler/payload/talks_test.go new file mode 100644 index 00000000..450fb1a4 --- /dev/null +++ b/handler/payload/talks_test.go @@ -0,0 +1,17 @@ +package payload + +import ( + "encoding/json" + "testing" +) + +func TestTalksResponseJSON(t *testing.T) { + body := []byte(`{"version":"v1","data":[{"uuid":"u","title":"t","subject":"s","location":"l","url":"u","photo":"p","created_at":"c","updated_at":"up"}]}`) + var res TalksResponse + if err := json.Unmarshal(body, &res); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if res.Version != "v1" || len(res.Data) != 1 || res.Data[0].Title != "t" { + t.Fatalf("unexpected response: %+v", res) + } +} diff --git a/handler/payload/users_test.go b/handler/payload/users_test.go new file mode 100644 index 00000000..bcafb3f8 --- /dev/null +++ b/handler/payload/users_test.go @@ -0,0 +1,17 @@ +package payload + +import ( + "encoding/json" + "testing" +) + +func TestUserResponseJSON(t *testing.T) { + body := []byte(`{"uuid":"u","first_name":"f","last_name":"l","username":"un","display_name":"dn","bio":"b","picture_file_name":"p","profile_picture_url":"pu","is_admin":true}`) + var res UserResponse + if err := json.Unmarshal(body, &res); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if res.UUID != "u" || res.FirstName != "f" || !res.IsAdmin { + t.Fatalf("unexpected response: %+v", res) + } +} From 38a1635dbc2448ea1538faec589ccb2f950a8390 Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 6 Aug 2025 12:02:30 +0800 Subject: [PATCH 12/16] test: fix UUIDs in handler tests --- handler/categories_test.go | 7 ++++--- handler/posts_test.go | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/handler/categories_test.go b/handler/categories_test.go index 9e9b249a..07410958 100644 --- a/handler/categories_test.go +++ b/handler/categories_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/oullin/database" "github.com/oullin/database/repository" "github.com/oullin/database/repository/pagination" @@ -70,17 +71,17 @@ func makeCategoriesRepo(t *testing.T) *repository.Categories { t.Fatalf("migrate: %v", err) } - author := database.User{ID: 1, UUID: "u1", Username: "user", FirstName: "F", LastName: "L", Email: "u@example.com", PasswordHash: "x"} + author := database.User{ID: 1, UUID: uuid.NewString(), Username: "user", FirstName: "F", LastName: "L", Email: "u@example.com", PasswordHash: "x"} if err := conn.Sql().Create(&author).Error; err != nil { t.Fatalf("create user: %v", err) } published := time.Now() - post := database.Post{UUID: "p1", AuthorID: author.ID, Slug: "hello", Title: "Hello", Excerpt: "Ex", Content: "Body", PublishedAt: &published} + post := database.Post{UUID: uuid.NewString(), AuthorID: author.ID, Slug: "hello", Title: "Hello", Excerpt: "Ex", Content: "Body", PublishedAt: &published} if err := conn.Sql().Create(&post).Error; err != nil { t.Fatalf("create post: %v", err) } - cat := database.Category{UUID: "c1", Name: "Cat", Slug: "cat", Description: "desc"} + cat := database.Category{UUID: uuid.NewString(), Name: "Cat", Slug: "cat", Description: "desc"} if err := conn.Sql().Create(&cat).Error; err != nil { t.Fatalf("create category: %v", err) } diff --git a/handler/posts_test.go b/handler/posts_test.go index 55f1a989..d698c29d 100644 --- a/handler/posts_test.go +++ b/handler/posts_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/oullin/database" "github.com/oullin/database/repository" "github.com/oullin/database/repository/pagination" @@ -89,12 +90,12 @@ func makePostsRepo(t *testing.T) *repository.Posts { t.Fatalf("migrate: %v", err) } - author := database.User{ID: 1, UUID: "u1", Username: "user", FirstName: "F", LastName: "L", Email: "u@example.com", PasswordHash: "x"} + author := database.User{ID: 1, UUID: uuid.NewString(), Username: "user", FirstName: "F", LastName: "L", Email: "u@example.com", PasswordHash: "x"} if err := conn.Sql().Create(&author).Error; err != nil { t.Fatalf("create user: %v", err) } published := time.Now() - post := database.Post{UUID: "p1", AuthorID: author.ID, Slug: "hello", Title: "Hello", Excerpt: "Ex", Content: "Body", PublishedAt: &published} + post := database.Post{UUID: uuid.NewString(), AuthorID: author.ID, Slug: "hello", Title: "Hello", Excerpt: "Ex", Content: "Body", PublishedAt: &published} if err := conn.Sql().Create(&post).Error; err != nil { t.Fatalf("create post: %v", err) } From 8370b5a19f4e2e49727b233f9caafc6f0d3c3340 Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 6 Aug 2025 13:07:35 +0800 Subject: [PATCH 13/16] test: export test helpers --- handler/education_test.go | 10 ++++++---- handler/experience_test.go | 10 ++++++---- handler/profile_test.go | 10 ++++++---- handler/projects_test.go | 10 ++++++---- handler/recommendations_test.go | 6 ++++-- handler/social_test.go | 10 ++++++---- handler/talks_test.go | 10 ++++++---- handler/{test_helpers_test.go => tests/helpers.go} | 6 +++--- handler/tests/helpers_test.go | 14 ++++++++++++++ 9 files changed, 57 insertions(+), 29 deletions(-) rename handler/{test_helpers_test.go => tests/helpers.go} (78%) create mode 100644 handler/tests/helpers_test.go diff --git a/handler/education_test.go b/handler/education_test.go index 73bae288..62f59004 100644 --- a/handler/education_test.go +++ b/handler/education_test.go @@ -6,10 +6,12 @@ import ( "net/http/httptest" "os" "testing" + + tests "github.com/oullin/handler/tests" ) func TestEducationHandlerHandle(t *testing.T) { - file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeEducationHandler(file) @@ -23,7 +25,7 @@ func TestEducationHandlerHandle(t *testing.T) { t.Fatalf("status %d", rec.Code) } - var resp testEnvelope + var resp tests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } @@ -55,7 +57,7 @@ func TestEducationHandlerHandle(t *testing.T) { } func TestEducationHandlerHandle_Payload(t *testing.T) { - file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeEducationHandler(file) @@ -66,7 +68,7 @@ func TestEducationHandlerHandle_Payload(t *testing.T) { t.Fatalf("err: %v", err) } - var resp testEnvelope + var resp tests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } diff --git a/handler/experience_test.go b/handler/experience_test.go index d1f8159a..2ae0ea52 100644 --- a/handler/experience_test.go +++ b/handler/experience_test.go @@ -6,10 +6,12 @@ import ( "net/http/httptest" "os" "testing" + + tests "github.com/oullin/handler/tests" ) func TestExperienceHandlerHandle(t *testing.T) { - file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeExperienceHandler(file) @@ -23,7 +25,7 @@ func TestExperienceHandlerHandle(t *testing.T) { t.Fatalf("status %d", rec.Code) } - var resp testEnvelope + var resp tests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } @@ -55,7 +57,7 @@ func TestExperienceHandlerHandle(t *testing.T) { } func TestExperienceHandlerHandle_Payload(t *testing.T) { - file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeExperienceHandler(file) @@ -66,7 +68,7 @@ func TestExperienceHandlerHandle_Payload(t *testing.T) { t.Fatalf("err: %v", err) } - var resp testEnvelope + var resp tests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } diff --git a/handler/profile_test.go b/handler/profile_test.go index 5d9cbb72..ad37fe27 100644 --- a/handler/profile_test.go +++ b/handler/profile_test.go @@ -6,10 +6,12 @@ import ( "net/http/httptest" "os" "testing" + + tests "github.com/oullin/handler/tests" ) func TestProfileHandlerHandle(t *testing.T) { - file := writeJSON(t, testEnvelope{Version: "v1", Data: map[string]string{"nickname": "nick"}}) + file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: map[string]string{"nickname": "nick"}}) defer os.Remove(file) h := MakeProfileHandler(file) @@ -23,7 +25,7 @@ func TestProfileHandlerHandle(t *testing.T) { t.Fatalf("status %d", rec.Code) } - var resp testEnvelope + var resp tests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } @@ -55,7 +57,7 @@ func TestProfileHandlerHandle(t *testing.T) { } func TestProfileHandlerHandle_Payload(t *testing.T) { - file := writeJSON(t, testEnvelope{Version: "v1", Data: map[string]string{"nickname": "nick"}}) + file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: map[string]string{"nickname": "nick"}}) defer os.Remove(file) h := MakeProfileHandler(file) @@ -66,7 +68,7 @@ func TestProfileHandlerHandle_Payload(t *testing.T) { t.Fatalf("err: %v", err) } - var resp testEnvelope + var resp tests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } diff --git a/handler/projects_test.go b/handler/projects_test.go index 589bfece..ec21753b 100644 --- a/handler/projects_test.go +++ b/handler/projects_test.go @@ -6,10 +6,12 @@ import ( "net/http/httptest" "os" "testing" + + tests "github.com/oullin/handler/tests" ) func TestProjectsHandlerHandle(t *testing.T) { - file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeProjectsHandler(file) @@ -23,7 +25,7 @@ func TestProjectsHandlerHandle(t *testing.T) { t.Fatalf("status %d", rec.Code) } - var resp testEnvelope + var resp tests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } @@ -55,7 +57,7 @@ func TestProjectsHandlerHandle(t *testing.T) { } func TestProjectsHandlerHandle_Payload(t *testing.T) { - file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeProjectsHandler(file) @@ -66,7 +68,7 @@ func TestProjectsHandlerHandle_Payload(t *testing.T) { t.Fatalf("err: %v", err) } - var resp testEnvelope + var resp tests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } diff --git a/handler/recommendations_test.go b/handler/recommendations_test.go index f26bd628..f09a7a5b 100644 --- a/handler/recommendations_test.go +++ b/handler/recommendations_test.go @@ -6,10 +6,12 @@ import ( "net/http/httptest" "os" "testing" + + tests "github.com/oullin/handler/tests" ) func TestRecommendationsHandlerHandle(t *testing.T) { - file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeRecommendationsHandler(file) @@ -23,7 +25,7 @@ func TestRecommendationsHandlerHandle(t *testing.T) { t.Fatalf("status %d", rec.Code) } - var resp testEnvelope + var resp tests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } diff --git a/handler/social_test.go b/handler/social_test.go index 12b51720..3b88f639 100644 --- a/handler/social_test.go +++ b/handler/social_test.go @@ -6,10 +6,12 @@ import ( "net/http/httptest" "os" "testing" + + tests "github.com/oullin/handler/tests" ) func TestSocialHandlerHandle(t *testing.T) { - file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeSocialHandler(file) @@ -23,7 +25,7 @@ func TestSocialHandlerHandle(t *testing.T) { t.Fatalf("status %d", rec.Code) } - var resp testEnvelope + var resp tests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } @@ -55,7 +57,7 @@ func TestSocialHandlerHandle(t *testing.T) { } func TestSocialHandlerHandle_Payload(t *testing.T) { - file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeSocialHandler(file) @@ -66,7 +68,7 @@ func TestSocialHandlerHandle_Payload(t *testing.T) { t.Fatalf("err: %v", err) } - var resp testEnvelope + var resp tests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } diff --git a/handler/talks_test.go b/handler/talks_test.go index 35556262..9ec8b395 100644 --- a/handler/talks_test.go +++ b/handler/talks_test.go @@ -6,10 +6,12 @@ import ( "net/http/httptest" "os" "testing" + + tests "github.com/oullin/handler/tests" ) func TestTalksHandlerHandle(t *testing.T) { - file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeTalksHandler(file) @@ -23,7 +25,7 @@ func TestTalksHandlerHandle(t *testing.T) { t.Fatalf("status %d", rec.Code) } - var resp testEnvelope + var resp tests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } @@ -55,7 +57,7 @@ func TestTalksHandlerHandle(t *testing.T) { } func TestTalksHandlerHandle_Payload(t *testing.T) { - file := writeJSON(t, testEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeTalksHandler(file) @@ -66,7 +68,7 @@ func TestTalksHandlerHandle_Payload(t *testing.T) { t.Fatalf("err: %v", err) } - var resp testEnvelope + var resp tests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } diff --git a/handler/test_helpers_test.go b/handler/tests/helpers.go similarity index 78% rename from handler/test_helpers_test.go rename to handler/tests/helpers.go index 1ab1c3cc..f7290cd8 100644 --- a/handler/test_helpers_test.go +++ b/handler/tests/helpers.go @@ -1,4 +1,4 @@ -package handler +package tests import ( "encoding/json" @@ -6,12 +6,12 @@ import ( "testing" ) -type testEnvelope struct { +type TestEnvelope struct { Version string `json:"version"` Data interface{} `json:"data"` } -func writeJSON(t *testing.T, v interface{}) string { +func WriteJSON(t *testing.T, v interface{}) string { t.Helper() f, err := os.CreateTemp("", "data.json") if err != nil { diff --git a/handler/tests/helpers_test.go b/handler/tests/helpers_test.go new file mode 100644 index 00000000..674b8f3a --- /dev/null +++ b/handler/tests/helpers_test.go @@ -0,0 +1,14 @@ +package tests + +import ( + "os" + "testing" +) + +func TestWriteJSON(t *testing.T) { + file := WriteJSON(t, TestEnvelope{Version: "v1"}) + if file == "" { + t.Fatalf("expected file path") + } + os.Remove(file) +} From 31fdd3ab053e210df1d11c8111d4c271215c339a Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 6 Aug 2025 13:16:32 +0800 Subject: [PATCH 14/16] chore: rename handler test package --- handler/education_test.go | 10 +++++----- handler/experience_test.go | 10 +++++----- handler/profile_test.go | 10 +++++----- handler/projects_test.go | 10 +++++----- handler/recommendations_test.go | 6 +++--- handler/social_test.go | 10 +++++----- handler/talks_test.go | 10 +++++----- handler/tests/helpers.go | 2 +- handler/tests/helpers_test.go | 2 +- 9 files changed, 35 insertions(+), 35 deletions(-) diff --git a/handler/education_test.go b/handler/education_test.go index 62f59004..39490b32 100644 --- a/handler/education_test.go +++ b/handler/education_test.go @@ -7,11 +7,11 @@ import ( "os" "testing" - tests "github.com/oullin/handler/tests" + handlertests "github.com/oullin/handler/tests" ) func TestEducationHandlerHandle(t *testing.T) { - file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeEducationHandler(file) @@ -25,7 +25,7 @@ func TestEducationHandlerHandle(t *testing.T) { t.Fatalf("status %d", rec.Code) } - var resp tests.TestEnvelope + var resp handlertests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } @@ -57,7 +57,7 @@ func TestEducationHandlerHandle(t *testing.T) { } func TestEducationHandlerHandle_Payload(t *testing.T) { - file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeEducationHandler(file) @@ -68,7 +68,7 @@ func TestEducationHandlerHandle_Payload(t *testing.T) { t.Fatalf("err: %v", err) } - var resp tests.TestEnvelope + var resp handlertests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } diff --git a/handler/experience_test.go b/handler/experience_test.go index 2ae0ea52..012d5963 100644 --- a/handler/experience_test.go +++ b/handler/experience_test.go @@ -7,11 +7,11 @@ import ( "os" "testing" - tests "github.com/oullin/handler/tests" + handlertests "github.com/oullin/handler/tests" ) func TestExperienceHandlerHandle(t *testing.T) { - file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeExperienceHandler(file) @@ -25,7 +25,7 @@ func TestExperienceHandlerHandle(t *testing.T) { t.Fatalf("status %d", rec.Code) } - var resp tests.TestEnvelope + var resp handlertests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } @@ -57,7 +57,7 @@ func TestExperienceHandlerHandle(t *testing.T) { } func TestExperienceHandlerHandle_Payload(t *testing.T) { - file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeExperienceHandler(file) @@ -68,7 +68,7 @@ func TestExperienceHandlerHandle_Payload(t *testing.T) { t.Fatalf("err: %v", err) } - var resp tests.TestEnvelope + var resp handlertests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } diff --git a/handler/profile_test.go b/handler/profile_test.go index ad37fe27..b357f29c 100644 --- a/handler/profile_test.go +++ b/handler/profile_test.go @@ -7,11 +7,11 @@ import ( "os" "testing" - tests "github.com/oullin/handler/tests" + handlertests "github.com/oullin/handler/tests" ) func TestProfileHandlerHandle(t *testing.T) { - file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: map[string]string{"nickname": "nick"}}) + file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: map[string]string{"nickname": "nick"}}) defer os.Remove(file) h := MakeProfileHandler(file) @@ -25,7 +25,7 @@ func TestProfileHandlerHandle(t *testing.T) { t.Fatalf("status %d", rec.Code) } - var resp tests.TestEnvelope + var resp handlertests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } @@ -57,7 +57,7 @@ func TestProfileHandlerHandle(t *testing.T) { } func TestProfileHandlerHandle_Payload(t *testing.T) { - file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: map[string]string{"nickname": "nick"}}) + file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: map[string]string{"nickname": "nick"}}) defer os.Remove(file) h := MakeProfileHandler(file) @@ -68,7 +68,7 @@ func TestProfileHandlerHandle_Payload(t *testing.T) { t.Fatalf("err: %v", err) } - var resp tests.TestEnvelope + var resp handlertests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } diff --git a/handler/projects_test.go b/handler/projects_test.go index ec21753b..ac723b14 100644 --- a/handler/projects_test.go +++ b/handler/projects_test.go @@ -7,11 +7,11 @@ import ( "os" "testing" - tests "github.com/oullin/handler/tests" + handlertests "github.com/oullin/handler/tests" ) func TestProjectsHandlerHandle(t *testing.T) { - file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeProjectsHandler(file) @@ -25,7 +25,7 @@ func TestProjectsHandlerHandle(t *testing.T) { t.Fatalf("status %d", rec.Code) } - var resp tests.TestEnvelope + var resp handlertests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } @@ -57,7 +57,7 @@ func TestProjectsHandlerHandle(t *testing.T) { } func TestProjectsHandlerHandle_Payload(t *testing.T) { - file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeProjectsHandler(file) @@ -68,7 +68,7 @@ func TestProjectsHandlerHandle_Payload(t *testing.T) { t.Fatalf("err: %v", err) } - var resp tests.TestEnvelope + var resp handlertests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } diff --git a/handler/recommendations_test.go b/handler/recommendations_test.go index f09a7a5b..3ccf62c1 100644 --- a/handler/recommendations_test.go +++ b/handler/recommendations_test.go @@ -7,11 +7,11 @@ import ( "os" "testing" - tests "github.com/oullin/handler/tests" + handlertests "github.com/oullin/handler/tests" ) func TestRecommendationsHandlerHandle(t *testing.T) { - file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeRecommendationsHandler(file) @@ -25,7 +25,7 @@ func TestRecommendationsHandlerHandle(t *testing.T) { t.Fatalf("status %d", rec.Code) } - var resp tests.TestEnvelope + var resp handlertests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } diff --git a/handler/social_test.go b/handler/social_test.go index 3b88f639..fc62e0a2 100644 --- a/handler/social_test.go +++ b/handler/social_test.go @@ -7,11 +7,11 @@ import ( "os" "testing" - tests "github.com/oullin/handler/tests" + handlertests "github.com/oullin/handler/tests" ) func TestSocialHandlerHandle(t *testing.T) { - file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeSocialHandler(file) @@ -25,7 +25,7 @@ func TestSocialHandlerHandle(t *testing.T) { t.Fatalf("status %d", rec.Code) } - var resp tests.TestEnvelope + var resp handlertests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } @@ -57,7 +57,7 @@ func TestSocialHandlerHandle(t *testing.T) { } func TestSocialHandlerHandle_Payload(t *testing.T) { - file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeSocialHandler(file) @@ -68,7 +68,7 @@ func TestSocialHandlerHandle_Payload(t *testing.T) { t.Fatalf("err: %v", err) } - var resp tests.TestEnvelope + var resp handlertests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } diff --git a/handler/talks_test.go b/handler/talks_test.go index 9ec8b395..764bf6d8 100644 --- a/handler/talks_test.go +++ b/handler/talks_test.go @@ -7,11 +7,11 @@ import ( "os" "testing" - tests "github.com/oullin/handler/tests" + handlertests "github.com/oullin/handler/tests" ) func TestTalksHandlerHandle(t *testing.T) { - file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeTalksHandler(file) @@ -25,7 +25,7 @@ func TestTalksHandlerHandle(t *testing.T) { t.Fatalf("status %d", rec.Code) } - var resp tests.TestEnvelope + var resp handlertests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } @@ -57,7 +57,7 @@ func TestTalksHandlerHandle(t *testing.T) { } func TestTalksHandlerHandle_Payload(t *testing.T) { - file := tests.WriteJSON(t, tests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) + file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) defer os.Remove(file) h := MakeTalksHandler(file) @@ -68,7 +68,7 @@ func TestTalksHandlerHandle_Payload(t *testing.T) { t.Fatalf("err: %v", err) } - var resp tests.TestEnvelope + var resp handlertests.TestEnvelope if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } diff --git a/handler/tests/helpers.go b/handler/tests/helpers.go index f7290cd8..2f8d8482 100644 --- a/handler/tests/helpers.go +++ b/handler/tests/helpers.go @@ -1,4 +1,4 @@ -package tests +package handlertests import ( "encoding/json" diff --git a/handler/tests/helpers_test.go b/handler/tests/helpers_test.go index 674b8f3a..ce21dd2f 100644 --- a/handler/tests/helpers_test.go +++ b/handler/tests/helpers_test.go @@ -1,4 +1,4 @@ -package tests +package handlertests import ( "os" From 097210a41a309203935857e8504756d5c256bdab Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 6 Aug 2025 13:31:23 +0800 Subject: [PATCH 15/16] test: add shared helpers --- handler/categories_test.go | 73 ++------------------------- handler/education_test.go | 89 ++++----------------------------- handler/experience_test.go | 89 ++++----------------------------- handler/file_handler_test.go | 87 ++++++++++++++++++++++++++++++++ handler/posts_test.go | 81 +++++------------------------- handler/profile_test.go | 85 ++++--------------------------- handler/projects_test.go | 89 ++++----------------------------- handler/recommendations_test.go | 63 ++++------------------- handler/social_test.go | 89 ++++----------------------------- handler/talks_test.go | 89 ++++----------------------------- handler/tests/db.go | 75 +++++++++++++++++++++++++++ 11 files changed, 241 insertions(+), 668 deletions(-) create mode 100644 handler/file_handler_test.go create mode 100644 handler/tests/db.go diff --git a/handler/categories_test.go b/handler/categories_test.go index 07410958..210f033d 100644 --- a/handler/categories_test.go +++ b/handler/categories_test.go @@ -1,11 +1,9 @@ package handler import ( - "context" "encoding/json" "net/http" "net/http/httptest" - "os/exec" "testing" "time" @@ -13,90 +11,27 @@ import ( "github.com/oullin/database" "github.com/oullin/database/repository" "github.com/oullin/database/repository/pagination" - "github.com/oullin/env" "github.com/oullin/handler/payload" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/modules/postgres" + handlertests "github.com/oullin/handler/tests" ) -func makeCategoriesRepo(t *testing.T) *repository.Categories { - t.Helper() - - if _, err := exec.LookPath("docker"); err != nil { - t.Skip("docker not installed") - } - - ctx := context.Background() - pg, err := postgres.RunContainer(ctx, - testcontainers.WithImage("postgres:16-alpine"), - postgres.WithDatabase("testdb"), - postgres.WithUsername("test"), - postgres.WithPassword("secret"), - postgres.BasicWaitStrategies(), - ) - if err != nil { - t.Fatalf("container run err: %v", err) - } - t.Cleanup(func() { pg.Terminate(ctx) }) - - host, err := pg.Host(ctx) - if err != nil { - t.Fatalf("host err: %v", err) - } - port, err := pg.MappedPort(ctx, "5432/tcp") - if err != nil { - t.Fatalf("port err: %v", err) - } - - e := &env.Environment{ - DB: env.DBEnvironment{ - UserName: "test", - UserPassword: "secret", - DatabaseName: "testdb", - Port: port.Int(), - Host: host, - DriverName: database.DriverName, - SSLMode: "disable", - TimeZone: "UTC", - }, - } - - conn, err := database.MakeConnection(e) - if err != nil { - t.Fatalf("make connection: %v", err) - } - t.Cleanup(func() { conn.Close() }) - - if err := conn.Sql().AutoMigrate(&database.User{}, &database.Post{}, &database.Category{}, &database.PostCategory{}); err != nil { - t.Fatalf("migrate: %v", err) - } - - author := database.User{ID: 1, UUID: uuid.NewString(), Username: "user", FirstName: "F", LastName: "L", Email: "u@example.com", PasswordHash: "x"} - if err := conn.Sql().Create(&author).Error; err != nil { - t.Fatalf("create user: %v", err) - } +func TestCategoriesHandlerIndex_Success(t *testing.T) { + conn, author := handlertests.MakeTestDB(t) published := time.Now() post := database.Post{UUID: uuid.NewString(), AuthorID: author.ID, Slug: "hello", Title: "Hello", Excerpt: "Ex", Content: "Body", PublishedAt: &published} if err := conn.Sql().Create(&post).Error; err != nil { t.Fatalf("create post: %v", err) } - cat := database.Category{UUID: uuid.NewString(), Name: "Cat", Slug: "cat", Description: "desc"} if err := conn.Sql().Create(&cat).Error; err != nil { t.Fatalf("create category: %v", err) } - link := database.PostCategory{PostID: post.ID, CategoryID: cat.ID} if err := conn.Sql().Create(&link).Error; err != nil { t.Fatalf("create link: %v", err) } - return &repository.Categories{DB: conn} -} - -func TestCategoriesHandlerIndex_Success(t *testing.T) { - repo := makeCategoriesRepo(t) - h := MakeCategoriesHandler(repo) + h := MakeCategoriesHandler(&repository.Categories{DB: conn}) req := httptest.NewRequest("GET", "/categories", nil) rec := httptest.NewRecorder() diff --git a/handler/education_test.go b/handler/education_test.go index 39490b32..fa5e9f18 100644 --- a/handler/education_test.go +++ b/handler/education_test.go @@ -1,83 +1,12 @@ package handler -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "testing" - - handlertests "github.com/oullin/handler/tests" -) - -func TestEducationHandlerHandle(t *testing.T) { - file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) - defer os.Remove(file) - - h := MakeEducationHandler(file) - req := httptest.NewRequest("GET", "/education", nil) - rec := httptest.NewRecorder() - - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - if rec.Code != http.StatusOK { - t.Fatalf("status %d", rec.Code) - } - - var resp handlertests.TestEnvelope - if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - if resp.Version != "v1" { - t.Fatalf("version %s", resp.Version) - } - - req2 := httptest.NewRequest("GET", "/education", nil) - req2.Header.Set("If-None-Match", "\"v1\"") - rec2 := httptest.NewRecorder() - if err := h.Handle(rec2, req2); err != nil { - t.Fatalf("err: %v", err) - } - if rec2.Code != http.StatusNotModified { - t.Fatalf("status %d", rec2.Code) - } - - badF, _ := os.CreateTemp("", "bad.json") - badF.WriteString("{") - badF.Close() - defer os.Remove(badF.Name()) - - bad := MakeEducationHandler(badF.Name()) - rec3 := httptest.NewRecorder() - req3 := httptest.NewRequest("GET", "/education", nil) - if bad.Handle(rec3, req3) == nil { - t.Fatalf("expected error") - } -} - -func TestEducationHandlerHandle_Payload(t *testing.T) { - file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) - defer os.Remove(file) - - h := MakeEducationHandler(file) - req := httptest.NewRequest("GET", "/education", nil) - rec := httptest.NewRecorder() - - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - - var resp handlertests.TestEnvelope - if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - arr, ok := resp.Data.([]interface{}) - if !ok || len(arr) != 1 { - t.Fatalf("unexpected data: %+v", resp.Data) - } - m, ok := arr[0].(map[string]interface{}) - if !ok || m["uuid"] != "1" { - t.Fatalf("unexpected payload: %+v", resp.Data) - } +import "testing" + +func TestEducationHandler(t *testing.T) { + runFileHandlerTest(t, fileHandlerTestCase{ + make: func(f string) fileHandler { return MakeEducationHandler(f) }, + endpoint: "/education", + data: []map[string]string{{"uuid": "1"}}, + assert: assertArrayUUID1, + }) } diff --git a/handler/experience_test.go b/handler/experience_test.go index 012d5963..1effb6e7 100644 --- a/handler/experience_test.go +++ b/handler/experience_test.go @@ -1,83 +1,12 @@ package handler -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "testing" - - handlertests "github.com/oullin/handler/tests" -) - -func TestExperienceHandlerHandle(t *testing.T) { - file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) - defer os.Remove(file) - - h := MakeExperienceHandler(file) - req := httptest.NewRequest("GET", "/experience", nil) - rec := httptest.NewRecorder() - - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - if rec.Code != http.StatusOK { - t.Fatalf("status %d", rec.Code) - } - - var resp handlertests.TestEnvelope - if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - if resp.Version != "v1" { - t.Fatalf("version %s", resp.Version) - } - - req2 := httptest.NewRequest("GET", "/experience", nil) - req2.Header.Set("If-None-Match", "\"v1\"") - rec2 := httptest.NewRecorder() - if err := h.Handle(rec2, req2); err != nil { - t.Fatalf("err: %v", err) - } - if rec2.Code != http.StatusNotModified { - t.Fatalf("status %d", rec2.Code) - } - - badF, _ := os.CreateTemp("", "bad.json") - badF.WriteString("{") - badF.Close() - defer os.Remove(badF.Name()) - - bad := MakeExperienceHandler(badF.Name()) - rec3 := httptest.NewRecorder() - req3 := httptest.NewRequest("GET", "/experience", nil) - if bad.Handle(rec3, req3) == nil { - t.Fatalf("expected error") - } -} - -func TestExperienceHandlerHandle_Payload(t *testing.T) { - file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) - defer os.Remove(file) - - h := MakeExperienceHandler(file) - req := httptest.NewRequest("GET", "/experience", nil) - rec := httptest.NewRecorder() - - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - - var resp handlertests.TestEnvelope - if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - arr, ok := resp.Data.([]interface{}) - if !ok || len(arr) != 1 { - t.Fatalf("unexpected data: %+v", resp.Data) - } - m, ok := arr[0].(map[string]interface{}) - if !ok || m["uuid"] != "1" { - t.Fatalf("unexpected payload: %+v", resp.Data) - } +import "testing" + +func TestExperienceHandler(t *testing.T) { + runFileHandlerTest(t, fileHandlerTestCase{ + make: func(f string) fileHandler { return MakeExperienceHandler(f) }, + endpoint: "/experience", + data: []map[string]string{{"uuid": "1"}}, + assert: assertArrayUUID1, + }) } diff --git a/handler/file_handler_test.go b/handler/file_handler_test.go new file mode 100644 index 00000000..57ed7002 --- /dev/null +++ b/handler/file_handler_test.go @@ -0,0 +1,87 @@ +package handler + +import ( + "encoding/json" + baseHttp "net/http" + "net/http/httptest" + "os" + "testing" + + handlertests "github.com/oullin/handler/tests" + pkgHttp "github.com/oullin/pkg/http" +) + +type fileHandler interface { + Handle(baseHttp.ResponseWriter, *baseHttp.Request) *pkgHttp.ApiError +} + +type fileHandlerTestCase struct { + make func(string) fileHandler + endpoint string + data interface{} + assert func(*testing.T, any) +} + +func runFileHandlerTest(t *testing.T, tc fileHandlerTestCase) { + file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: tc.data}) + defer os.Remove(file) + + h := tc.make(file) + req := httptest.NewRequest("GET", tc.endpoint, nil) + rec := httptest.NewRecorder() + if err := h.Handle(rec, req); err != nil { + t.Fatalf("err: %v", err) + } + if rec.Code != baseHttp.StatusOK { + t.Fatalf("status %d", rec.Code) + } + + var resp handlertests.TestEnvelope + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if resp.Version != "v1" { + t.Fatalf("version %s", resp.Version) + } + tc.assert(t, resp.Data) + + req2 := httptest.NewRequest("GET", tc.endpoint, nil) + req2.Header.Set("If-None-Match", "\"v1\"") + rec2 := httptest.NewRecorder() + if err := h.Handle(rec2, req2); err != nil { + t.Fatalf("err: %v", err) + } + if rec2.Code != baseHttp.StatusNotModified { + t.Fatalf("status %d", rec2.Code) + } + + badF, _ := os.CreateTemp("", "bad.json") + badF.WriteString("{") + badF.Close() + defer os.Remove(badF.Name()) + + bad := tc.make(badF.Name()) + rec3 := httptest.NewRecorder() + req3 := httptest.NewRequest("GET", tc.endpoint, nil) + if bad.Handle(rec3, req3) == nil { + t.Fatalf("expected error") + } +} + +func assertArrayUUID1(t *testing.T, data any) { + arr, ok := data.([]interface{}) + if !ok || len(arr) != 1 { + t.Fatalf("unexpected data: %+v", data) + } + m, ok := arr[0].(map[string]interface{}) + if !ok || m["uuid"] != "1" { + t.Fatalf("unexpected payload: %+v", data) + } +} + +func assertNicknameNick(t *testing.T, data any) { + obj, ok := data.(map[string]interface{}) + if !ok || obj["nickname"] != "nick" { + t.Fatalf("unexpected payload: %+v", data) + } +} diff --git a/handler/posts_test.go b/handler/posts_test.go index d698c29d..1aca2e9e 100644 --- a/handler/posts_test.go +++ b/handler/posts_test.go @@ -2,11 +2,9 @@ package handler import ( "bytes" - "context" "encoding/json" "net/http" "net/http/httptest" - "os/exec" "testing" "time" @@ -14,10 +12,8 @@ import ( "github.com/oullin/database" "github.com/oullin/database/repository" "github.com/oullin/database/repository/pagination" - "github.com/oullin/env" "github.com/oullin/handler/payload" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/modules/postgres" + handlertests "github.com/oullin/handler/tests" ) func TestPostsHandlerIndex_ParseError(t *testing.T) { @@ -38,74 +34,15 @@ func TestPostsHandlerShow_MissingSlug(t *testing.T) { } } -func makePostsRepo(t *testing.T) *repository.Posts { - t.Helper() - - if _, err := exec.LookPath("docker"); err != nil { - t.Skip("docker not installed") - } - - ctx := context.Background() - pg, err := postgres.RunContainer(ctx, - testcontainers.WithImage("postgres:16-alpine"), - postgres.WithDatabase("testdb"), - postgres.WithUsername("test"), - postgres.WithPassword("secret"), - postgres.BasicWaitStrategies(), - ) - if err != nil { - t.Fatalf("container run err: %v", err) - } - t.Cleanup(func() { pg.Terminate(ctx) }) - - host, err := pg.Host(ctx) - if err != nil { - t.Fatalf("host err: %v", err) - } - port, err := pg.MappedPort(ctx, "5432/tcp") - if err != nil { - t.Fatalf("port err: %v", err) - } - - e := &env.Environment{ - DB: env.DBEnvironment{ - UserName: "test", - UserPassword: "secret", - DatabaseName: "testdb", - Port: port.Int(), - Host: host, - DriverName: database.DriverName, - SSLMode: "disable", - TimeZone: "UTC", - }, - } - - conn, err := database.MakeConnection(e) - if err != nil { - t.Fatalf("make connection: %v", err) - } - t.Cleanup(func() { conn.Close() }) - - if err := conn.Sql().AutoMigrate(&database.User{}, &database.Post{}, &database.Category{}, &database.Tag{}, &database.PostCategory{}, &database.PostTag{}); err != nil { - t.Fatalf("migrate: %v", err) - } - - author := database.User{ID: 1, UUID: uuid.NewString(), Username: "user", FirstName: "F", LastName: "L", Email: "u@example.com", PasswordHash: "x"} - if err := conn.Sql().Create(&author).Error; err != nil { - t.Fatalf("create user: %v", err) - } +func TestPostsHandlerIndex_Success(t *testing.T) { + conn, author := handlertests.MakeTestDB(t) published := time.Now() post := database.Post{UUID: uuid.NewString(), AuthorID: author.ID, Slug: "hello", Title: "Hello", Excerpt: "Ex", Content: "Body", PublishedAt: &published} if err := conn.Sql().Create(&post).Error; err != nil { t.Fatalf("create post: %v", err) } - return &repository.Posts{DB: conn} -} - -func TestPostsHandlerIndex_Success(t *testing.T) { - repo := makePostsRepo(t) - h := MakePostsHandler(repo) + h := MakePostsHandler(&repository.Posts{DB: conn}) req := httptest.NewRequest("POST", "/posts", bytes.NewReader([]byte("{}"))) rec := httptest.NewRecorder() @@ -127,8 +64,14 @@ func TestPostsHandlerIndex_Success(t *testing.T) { } func TestPostsHandlerShow_Success(t *testing.T) { - repo := makePostsRepo(t) - h := MakePostsHandler(repo) + conn, author := handlertests.MakeTestDB(t) + published := time.Now() + post := database.Post{UUID: uuid.NewString(), AuthorID: author.ID, Slug: "hello", Title: "Hello", Excerpt: "Ex", Content: "Body", PublishedAt: &published} + if err := conn.Sql().Create(&post).Error; err != nil { + t.Fatalf("create post: %v", err) + } + + h := MakePostsHandler(&repository.Posts{DB: conn}) req := httptest.NewRequest("GET", "/posts/hello", nil) req.SetPathValue("slug", "hello") diff --git a/handler/profile_test.go b/handler/profile_test.go index b357f29c..e1f7e059 100644 --- a/handler/profile_test.go +++ b/handler/profile_test.go @@ -1,79 +1,12 @@ package handler -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "testing" - - handlertests "github.com/oullin/handler/tests" -) - -func TestProfileHandlerHandle(t *testing.T) { - file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: map[string]string{"nickname": "nick"}}) - defer os.Remove(file) - - h := MakeProfileHandler(file) - req := httptest.NewRequest("GET", "/profile", nil) - rec := httptest.NewRecorder() - - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - if rec.Code != http.StatusOK { - t.Fatalf("status %d", rec.Code) - } - - var resp handlertests.TestEnvelope - if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - if resp.Version != "v1" { - t.Fatalf("version %s", resp.Version) - } - - req2 := httptest.NewRequest("GET", "/profile", nil) - req2.Header.Set("If-None-Match", "\"v1\"") - rec2 := httptest.NewRecorder() - if err := h.Handle(rec2, req2); err != nil { - t.Fatalf("err: %v", err) - } - if rec2.Code != http.StatusNotModified { - t.Fatalf("status %d", rec2.Code) - } - - badF, _ := os.CreateTemp("", "bad.json") - badF.WriteString("{") - badF.Close() - defer os.Remove(badF.Name()) - - bad := MakeProfileHandler(badF.Name()) - rec3 := httptest.NewRecorder() - req3 := httptest.NewRequest("GET", "/profile", nil) - if bad.Handle(rec3, req3) == nil { - t.Fatalf("expected error") - } -} - -func TestProfileHandlerHandle_Payload(t *testing.T) { - file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: map[string]string{"nickname": "nick"}}) - defer os.Remove(file) - - h := MakeProfileHandler(file) - req := httptest.NewRequest("GET", "/profile", nil) - rec := httptest.NewRecorder() - - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - - var resp handlertests.TestEnvelope - if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - obj, ok := resp.Data.(map[string]interface{}) - if !ok || obj["nickname"] != "nick" { - t.Fatalf("unexpected payload: %+v", resp.Data) - } +import "testing" + +func TestProfileHandler(t *testing.T) { + runFileHandlerTest(t, fileHandlerTestCase{ + make: func(f string) fileHandler { return MakeProfileHandler(f) }, + endpoint: "/profile", + data: map[string]string{"nickname": "nick"}, + assert: assertNicknameNick, + }) } diff --git a/handler/projects_test.go b/handler/projects_test.go index ac723b14..93897e9a 100644 --- a/handler/projects_test.go +++ b/handler/projects_test.go @@ -1,83 +1,12 @@ package handler -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "testing" - - handlertests "github.com/oullin/handler/tests" -) - -func TestProjectsHandlerHandle(t *testing.T) { - file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) - defer os.Remove(file) - - h := MakeProjectsHandler(file) - req := httptest.NewRequest("GET", "/projects", nil) - rec := httptest.NewRecorder() - - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - if rec.Code != http.StatusOK { - t.Fatalf("status %d", rec.Code) - } - - var resp handlertests.TestEnvelope - if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - if resp.Version != "v1" { - t.Fatalf("version %s", resp.Version) - } - - req2 := httptest.NewRequest("GET", "/projects", nil) - req2.Header.Set("If-None-Match", "\"v1\"") - rec2 := httptest.NewRecorder() - if err := h.Handle(rec2, req2); err != nil { - t.Fatalf("err: %v", err) - } - if rec2.Code != http.StatusNotModified { - t.Fatalf("status %d", rec2.Code) - } - - badF, _ := os.CreateTemp("", "bad.json") - badF.WriteString("{") - badF.Close() - defer os.Remove(badF.Name()) - - bad := MakeProjectsHandler(badF.Name()) - rec3 := httptest.NewRecorder() - req3 := httptest.NewRequest("GET", "/projects", nil) - if bad.Handle(rec3, req3) == nil { - t.Fatalf("expected error") - } -} - -func TestProjectsHandlerHandle_Payload(t *testing.T) { - file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) - defer os.Remove(file) - - h := MakeProjectsHandler(file) - req := httptest.NewRequest("GET", "/projects", nil) - rec := httptest.NewRecorder() - - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - - var resp handlertests.TestEnvelope - if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - arr, ok := resp.Data.([]interface{}) - if !ok || len(arr) != 1 { - t.Fatalf("unexpected data: %+v", resp.Data) - } - m, ok := arr[0].(map[string]interface{}) - if !ok || m["uuid"] != "1" { - t.Fatalf("unexpected payload: %+v", resp.Data) - } +import "testing" + +func TestProjectsHandler(t *testing.T) { + runFileHandlerTest(t, fileHandlerTestCase{ + make: func(f string) fileHandler { return MakeProjectsHandler(f) }, + endpoint: "/projects", + data: []map[string]string{{"uuid": "1"}}, + assert: assertArrayUUID1, + }) } diff --git a/handler/recommendations_test.go b/handler/recommendations_test.go index 3ccf62c1..7c04024d 100644 --- a/handler/recommendations_test.go +++ b/handler/recommendations_test.go @@ -1,57 +1,12 @@ package handler -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "testing" - - handlertests "github.com/oullin/handler/tests" -) - -func TestRecommendationsHandlerHandle(t *testing.T) { - file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) - defer os.Remove(file) - - h := MakeRecommendationsHandler(file) - req := httptest.NewRequest("GET", "/recommendations", nil) - rec := httptest.NewRecorder() - - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - if rec.Code != http.StatusOK { - t.Fatalf("status %d", rec.Code) - } - - var resp handlertests.TestEnvelope - if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - if resp.Version != "v1" { - t.Fatalf("version %s", resp.Version) - } - - req2 := httptest.NewRequest("GET", "/recommendations", nil) - req2.Header.Set("If-None-Match", "\"v1\"") - rec2 := httptest.NewRecorder() - if err := h.Handle(rec2, req2); err != nil { - t.Fatalf("err: %v", err) - } - if rec2.Code != http.StatusNotModified { - t.Fatalf("status %d", rec2.Code) - } - - badF, _ := os.CreateTemp("", "bad.json") - badF.WriteString("{") - badF.Close() - defer os.Remove(badF.Name()) - - bad := MakeRecommendationsHandler(badF.Name()) - rec3 := httptest.NewRecorder() - req3 := httptest.NewRequest("GET", "/recommendations", nil) - if bad.Handle(rec3, req3) == nil { - t.Fatalf("expected error") - } +import "testing" + +func TestRecommendationsHandler(t *testing.T) { + runFileHandlerTest(t, fileHandlerTestCase{ + make: func(f string) fileHandler { return MakeRecommendationsHandler(f) }, + endpoint: "/recommendations", + data: []map[string]string{{"uuid": "1"}}, + assert: assertArrayUUID1, + }) } diff --git a/handler/social_test.go b/handler/social_test.go index fc62e0a2..f875fd66 100644 --- a/handler/social_test.go +++ b/handler/social_test.go @@ -1,83 +1,12 @@ package handler -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "testing" - - handlertests "github.com/oullin/handler/tests" -) - -func TestSocialHandlerHandle(t *testing.T) { - file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) - defer os.Remove(file) - - h := MakeSocialHandler(file) - req := httptest.NewRequest("GET", "/social", nil) - rec := httptest.NewRecorder() - - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - if rec.Code != http.StatusOK { - t.Fatalf("status %d", rec.Code) - } - - var resp handlertests.TestEnvelope - if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - if resp.Version != "v1" { - t.Fatalf("version %s", resp.Version) - } - - req2 := httptest.NewRequest("GET", "/social", nil) - req2.Header.Set("If-None-Match", "\"v1\"") - rec2 := httptest.NewRecorder() - if err := h.Handle(rec2, req2); err != nil { - t.Fatalf("err: %v", err) - } - if rec2.Code != http.StatusNotModified { - t.Fatalf("status %d", rec2.Code) - } - - badF, _ := os.CreateTemp("", "bad.json") - badF.WriteString("{") - badF.Close() - defer os.Remove(badF.Name()) - - bad := MakeSocialHandler(badF.Name()) - rec3 := httptest.NewRecorder() - req3 := httptest.NewRequest("GET", "/social", nil) - if bad.Handle(rec3, req3) == nil { - t.Fatalf("expected error") - } -} - -func TestSocialHandlerHandle_Payload(t *testing.T) { - file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) - defer os.Remove(file) - - h := MakeSocialHandler(file) - req := httptest.NewRequest("GET", "/social", nil) - rec := httptest.NewRecorder() - - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - - var resp handlertests.TestEnvelope - if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - arr, ok := resp.Data.([]interface{}) - if !ok || len(arr) != 1 { - t.Fatalf("unexpected data: %+v", resp.Data) - } - m, ok := arr[0].(map[string]interface{}) - if !ok || m["uuid"] != "1" { - t.Fatalf("unexpected payload: %+v", resp.Data) - } +import "testing" + +func TestSocialHandler(t *testing.T) { + runFileHandlerTest(t, fileHandlerTestCase{ + make: func(f string) fileHandler { return MakeSocialHandler(f) }, + endpoint: "/social", + data: []map[string]string{{"uuid": "1"}}, + assert: assertArrayUUID1, + }) } diff --git a/handler/talks_test.go b/handler/talks_test.go index 764bf6d8..9c19e716 100644 --- a/handler/talks_test.go +++ b/handler/talks_test.go @@ -1,83 +1,12 @@ package handler -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "testing" - - handlertests "github.com/oullin/handler/tests" -) - -func TestTalksHandlerHandle(t *testing.T) { - file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) - defer os.Remove(file) - - h := MakeTalksHandler(file) - req := httptest.NewRequest("GET", "/talks", nil) - rec := httptest.NewRecorder() - - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - if rec.Code != http.StatusOK { - t.Fatalf("status %d", rec.Code) - } - - var resp handlertests.TestEnvelope - if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - if resp.Version != "v1" { - t.Fatalf("version %s", resp.Version) - } - - req2 := httptest.NewRequest("GET", "/talks", nil) - req2.Header.Set("If-None-Match", "\"v1\"") - rec2 := httptest.NewRecorder() - if err := h.Handle(rec2, req2); err != nil { - t.Fatalf("err: %v", err) - } - if rec2.Code != http.StatusNotModified { - t.Fatalf("status %d", rec2.Code) - } - - badF, _ := os.CreateTemp("", "bad.json") - badF.WriteString("{") - badF.Close() - defer os.Remove(badF.Name()) - - bad := MakeTalksHandler(badF.Name()) - rec3 := httptest.NewRecorder() - req3 := httptest.NewRequest("GET", "/talks", nil) - if bad.Handle(rec3, req3) == nil { - t.Fatalf("expected error") - } -} - -func TestTalksHandlerHandle_Payload(t *testing.T) { - file := handlertests.WriteJSON(t, handlertests.TestEnvelope{Version: "v1", Data: []map[string]string{{"uuid": "1"}}}) - defer os.Remove(file) - - h := MakeTalksHandler(file) - req := httptest.NewRequest("GET", "/talks", nil) - rec := httptest.NewRecorder() - - if err := h.Handle(rec, req); err != nil { - t.Fatalf("err: %v", err) - } - - var resp handlertests.TestEnvelope - if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - arr, ok := resp.Data.([]interface{}) - if !ok || len(arr) != 1 { - t.Fatalf("unexpected data: %+v", resp.Data) - } - m, ok := arr[0].(map[string]interface{}) - if !ok || m["uuid"] != "1" { - t.Fatalf("unexpected payload: %+v", resp.Data) - } +import "testing" + +func TestTalksHandler(t *testing.T) { + runFileHandlerTest(t, fileHandlerTestCase{ + make: func(f string) fileHandler { return MakeTalksHandler(f) }, + endpoint: "/talks", + data: []map[string]string{{"uuid": "1"}}, + assert: assertArrayUUID1, + }) } diff --git a/handler/tests/db.go b/handler/tests/db.go new file mode 100644 index 00000000..aba0265a --- /dev/null +++ b/handler/tests/db.go @@ -0,0 +1,75 @@ +package handlertests + +import ( + "context" + "os/exec" + "testing" + + "github.com/google/uuid" + "github.com/oullin/database" + "github.com/oullin/env" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" +) + +// MakeTestDB starts a PostgreSQL test container, runs migrations, and seeds a default user. +// It returns the database connection and the created user. +func MakeTestDB(t *testing.T) (*database.Connection, database.User) { + t.Helper() + + if _, err := exec.LookPath("docker"); err != nil { + t.Skip("docker not installed") + } + + ctx := context.Background() + pg, err := postgres.RunContainer(ctx, + testcontainers.WithImage("postgres:16-alpine"), + postgres.WithDatabase("testdb"), + postgres.WithUsername("test"), + postgres.WithPassword("secret"), + postgres.BasicWaitStrategies(), + ) + if err != nil { + t.Fatalf("container run err: %v", err) + } + t.Cleanup(func() { pg.Terminate(ctx) }) + + host, err := pg.Host(ctx) + if err != nil { + t.Fatalf("host err: %v", err) + } + port, err := pg.MappedPort(ctx, "5432/tcp") + if err != nil { + t.Fatalf("port err: %v", err) + } + + e := &env.Environment{ + DB: env.DBEnvironment{ + UserName: "test", + UserPassword: "secret", + DatabaseName: "testdb", + Port: port.Int(), + Host: host, + DriverName: database.DriverName, + SSLMode: "disable", + TimeZone: "UTC", + }, + } + + conn, err := database.MakeConnection(e) + if err != nil { + t.Fatalf("make connection: %v", err) + } + t.Cleanup(func() { conn.Close() }) + + if err := conn.Sql().AutoMigrate(&database.User{}, &database.Post{}, &database.Category{}, &database.Tag{}, &database.PostCategory{}, &database.PostTag{}); err != nil { + t.Fatalf("migrate: %v", err) + } + + author := database.User{ID: 1, UUID: uuid.NewString(), Username: "user", FirstName: "F", LastName: "L", Email: "u@example.com", PasswordHash: "x"} + if err := conn.Sql().Create(&author).Error; err != nil { + t.Fatalf("create user: %v", err) + } + + return conn, author +} From 59b278341cd0b8186684f0bcc8e1aa91f2f3d041 Mon Sep 17 00:00:00 2001 From: Gus Date: Wed, 6 Aug 2025 13:50:18 +0800 Subject: [PATCH 16/16] test: let db assign user id --- handler/tests/db.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handler/tests/db.go b/handler/tests/db.go index aba0265a..74d6d47f 100644 --- a/handler/tests/db.go +++ b/handler/tests/db.go @@ -66,7 +66,7 @@ func MakeTestDB(t *testing.T) (*database.Connection, database.User) { t.Fatalf("migrate: %v", err) } - author := database.User{ID: 1, UUID: uuid.NewString(), Username: "user", FirstName: "F", LastName: "L", Email: "u@example.com", PasswordHash: "x"} + author := database.User{UUID: uuid.NewString(), Username: "user", FirstName: "F", LastName: "L", Email: "u@example.com", PasswordHash: "x"} if err := conn.Sql().Create(&author).Error; err != nil { t.Fatalf("create user: %v", err) }