Dima před 2 roky
rodič
revize
028cafab37

+ 2 - 2
internal/mapper/tag.go

@@ -5,7 +5,7 @@ import (
 	"github.com/dmitriygnatenko/internal/models"
 )
 
-func ConvertTagModelsToDTO(m []models.Tag) ([]dto.TagDTO, error) {
+func ConvertTagModelsToDTO(m []models.Tag) []dto.TagDTO {
 	res := make([]dto.TagDTO, 0, len(m))
 
 	for i := range m {
@@ -16,7 +16,7 @@ func ConvertTagModelsToDTO(m []models.Tag) ([]dto.TagDTO, error) {
 		})
 	}
 
-	return res, nil
+	return res
 }
 
 func ConvertTagFormToModel(form models.TagForm) models.Tag {

+ 2 - 8
internal/services/handler/admin/article.go

@@ -51,10 +51,7 @@ func AddArticleHandler(sp interfaces.IServiceProvider) fiber.Handler {
 			return err
 		}
 
-		tagsDTO, err := mapper.ConvertTagModelsToDTO(tags)
-		if err != nil {
-			return err
-		}
+		tagsDTO := mapper.ConvertTagModelsToDTO(tags)
 
 		if ctx.Method() == fiber.MethodPost {
 			if err = ctx.BodyParser(&form); err != nil {
@@ -147,10 +144,7 @@ func EditArticleHandler(sp interfaces.IServiceProvider) fiber.Handler {
 			return err
 		}
 
-		tagsDTO, err := mapper.ConvertTagModelsToDTO(tags)
-		if err != nil {
-			return err
-		}
+		tagsDTO := mapper.ConvertTagModelsToDTO(tags)
 
 		var form *models.ArticleForm
 		if ctx.Method() == fiber.MethodGet {

+ 1 - 4
internal/services/handler/admin/tag.go

@@ -20,10 +20,7 @@ func TagHandler(sp interfaces.IServiceProvider) fiber.Handler {
 			return err
 		}
 
-		tagsDTO, err := mapper.ConvertTagModelsToDTO(tags)
-		if err != nil {
-			return err
-		}
+		tagsDTO := mapper.ConvertTagModelsToDTO(tags)
 
 		return ctx.Render("admin/tag", fiber.Map{
 			"tags":    tagsDTO,

+ 1 - 4
internal/services/handler/article.go

@@ -43,10 +43,7 @@ func ArticleHandler(sp interfaces.IServiceProvider) fiber.Handler {
 				return err
 			}
 
-			tagsDTO, err := mapper.ConvertTagModelsToDTO(tags)
-			if err != nil {
-				return err
-			}
+			tagsDTO := mapper.ConvertTagModelsToDTO(tags)
 
 			// Last articles
 			articles, err := sp.GetArticleRepository().GetAllPreview(ctx.Context())

+ 373 - 0
internal/services/handler/article_test.go

@@ -0,0 +1,373 @@
+package handler
+
+import (
+	"context"
+	"database/sql"
+	"errors"
+	"net/http/httptest"
+	"strconv"
+	"testing"
+
+	"github.com/brianvoe/gofakeit/v6"
+	"github.com/dmitriygnatenko/internal/interfaces"
+	"github.com/dmitriygnatenko/internal/models"
+	repositoryMocks "github.com/dmitriygnatenko/internal/repositories/mocks"
+	sp "github.com/dmitriygnatenko/internal/service_provider"
+	cacheMocks "github.com/dmitriygnatenko/internal/services/cache/mocks"
+	"github.com/dmitriygnatenko/internal/services/handler/test"
+	"github.com/gofiber/fiber/v2"
+	"github.com/gojuno/minimock/v3"
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_ArticleHandler(t *testing.T) {
+	type cacheMockFunc func(mc *minimock.Controller) interfaces.ICache
+	type tagMockFunc func(mc *minimock.Controller) interfaces.ITagRepository
+	type articleMockFunc func(mc *minimock.Controller) interfaces.IArticleRepository
+
+	type req struct {
+		method string
+		route  string
+	}
+
+	var (
+		mc          = minimock.NewController(t)
+		articleID   = gofakeit.Number(1, 100)
+		date        = gofakeit.Date()
+		publishTime = date.Format("2006-01-02 15:04:05")
+		internalErr = errors.New(gofakeit.Phrase())
+
+		article = models.Article{
+			ID:          articleID,
+			URL:         gofakeit.URL(),
+			Title:       gofakeit.Phrase(),
+			Text:        gofakeit.Phrase(),
+			PublishTime: publishTime,
+			PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
+			Image:       sql.NullString{Valid: true, String: gofakeit.URL()},
+			IsActive:    true,
+		}
+
+		notActiveArticle = models.Article{
+			ID:          articleID,
+			URL:         gofakeit.URL(),
+			Title:       gofakeit.Phrase(),
+			Text:        gofakeit.Phrase(),
+			PublishTime: publishTime,
+			PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
+			Image:       sql.NullString{Valid: true, String: gofakeit.URL()},
+		}
+
+		tags = []models.Tag{
+			{
+				ID:  gofakeit.Number(1, 100),
+				Tag: gofakeit.Word(),
+				URL: gofakeit.Word(),
+			},
+			{
+				ID:  gofakeit.Number(1, 100),
+				Tag: gofakeit.Word(),
+				URL: gofakeit.Word(),
+			},
+		}
+
+		previewArticles = []models.ArticlePreview{
+			{
+				ID:          gofakeit.Number(1, 100),
+				URL:         gofakeit.URL(),
+				Title:       gofakeit.Phrase(),
+				PublishTime: publishTime,
+				PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
+				Image:       sql.NullString{Valid: true, String: gofakeit.URL()},
+			},
+			{
+				ID:          gofakeit.Number(1, 100),
+				URL:         gofakeit.URL(),
+				Title:       gofakeit.Phrase(),
+				PublishTime: publishTime,
+				PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
+				Image:       sql.NullString{Valid: true, String: gofakeit.URL()},
+			},
+			{
+				ID:          gofakeit.Number(1, 100),
+				URL:         gofakeit.URL(),
+				Title:       gofakeit.Phrase(),
+				PublishTime: publishTime,
+				PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
+				Image:       sql.NullString{Valid: true, String: gofakeit.URL()},
+			},
+			{
+				ID:          gofakeit.Number(1, 100),
+				URL:         gofakeit.URL(),
+				Title:       gofakeit.Phrase(),
+				PublishTime: publishTime,
+				PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
+				Image:       sql.NullString{Valid: true, String: gofakeit.URL()},
+			},
+		}
+	)
+
+	tests := []struct {
+		name        string
+		req         req
+		res         int
+		err         error
+		cacheMock   cacheMockFunc
+		tagMock     tagMockFunc
+		articleMock articleMockFunc
+	}{
+		{
+			name: "positive case",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/article/" + strconv.Itoa(articleID),
+			},
+			res: fiber.StatusOK,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+				mock.SetMock.Return()
+
+				return mock
+			},
+			tagMock: func(mc *minimock.Controller) interfaces.ITagRepository {
+				mock := repositoryMocks.NewITagRepositoryMock(mc)
+				mock.GetAllUsedMock.Return(tags, nil)
+
+				return mock
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				mock := repositoryMocks.NewIArticleRepositoryMock(mc)
+
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+					assert.Equal(mc, strconv.Itoa(articleID), url)
+				}).Return(&article, nil)
+
+				mock.GetAllPreviewMock.Return(previewArticles, nil)
+
+				return mock
+			},
+		},
+		{
+			name: "negative case - article not found",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/article/" + strconv.Itoa(articleID),
+			},
+			res: fiber.StatusNotFound,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+
+				return mock
+			},
+			tagMock: func(mc *minimock.Controller) interfaces.ITagRepository {
+				mock := repositoryMocks.NewITagRepositoryMock(mc)
+
+				return mock
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				mock := repositoryMocks.NewIArticleRepositoryMock(mc)
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+					assert.Equal(mc, strconv.Itoa(articleID), url)
+				}).Return(nil, sql.ErrNoRows)
+
+				return mock
+			},
+		},
+		{
+			name: "negative case - article repository error",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/article/" + strconv.Itoa(articleID),
+			},
+			res: fiber.StatusInternalServerError,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+
+				return mock
+			},
+			tagMock: func(mc *minimock.Controller) interfaces.ITagRepository {
+				mock := repositoryMocks.NewITagRepositoryMock(mc)
+
+				return mock
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				mock := repositoryMocks.NewIArticleRepositoryMock(mc)
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+					assert.Equal(mc, strconv.Itoa(articleID), url)
+				}).Return(nil, internalErr)
+
+				return mock
+			},
+		},
+		{
+			name: "negative case - article not active",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/article/" + strconv.Itoa(articleID),
+			},
+			res: fiber.StatusNotFound,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+
+				return mock
+			},
+			tagMock: func(mc *minimock.Controller) interfaces.ITagRepository {
+				mock := repositoryMocks.NewITagRepositoryMock(mc)
+
+				return mock
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				mock := repositoryMocks.NewIArticleRepositoryMock(mc)
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+					assert.Equal(mc, strconv.Itoa(articleID), url)
+				}).Return(&notActiveArticle, nil)
+
+				return mock
+			},
+		},
+		{
+			name: "negative case - article mapper error",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/article/" + strconv.Itoa(articleID),
+			},
+			res: fiber.StatusInternalServerError,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+
+				return mock
+			},
+			tagMock: func(mc *minimock.Controller) interfaces.ITagRepository {
+				mock := repositoryMocks.NewITagRepositoryMock(mc)
+
+				return mock
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				mock := repositoryMocks.NewIArticleRepositoryMock(mc)
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+					assert.Equal(mc, strconv.Itoa(articleID), url)
+				}).Return(&models.Article{IsActive: true}, nil)
+
+				return mock
+			},
+		},
+		{
+			name: "negative case - tags repository error",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/article/" + strconv.Itoa(articleID),
+			},
+			res: fiber.StatusInternalServerError,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+
+				return mock
+			},
+			tagMock: func(mc *minimock.Controller) interfaces.ITagRepository {
+				mock := repositoryMocks.NewITagRepositoryMock(mc)
+				mock.GetAllUsedMock.Return(nil, internalErr)
+
+				return mock
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				mock := repositoryMocks.NewIArticleRepositoryMock(mc)
+
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+					assert.Equal(mc, strconv.Itoa(articleID), url)
+				}).Return(&article, nil)
+
+				return mock
+			},
+		},
+		{
+			name: "negative case - articles repository error",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/article/" + strconv.Itoa(articleID),
+			},
+			res: fiber.StatusInternalServerError,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+
+				return mock
+			},
+			tagMock: func(mc *minimock.Controller) interfaces.ITagRepository {
+				mock := repositoryMocks.NewITagRepositoryMock(mc)
+				mock.GetAllUsedMock.Return(tags, nil)
+
+				return mock
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				mock := repositoryMocks.NewIArticleRepositoryMock(mc)
+
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+					assert.Equal(mc, strconv.Itoa(articleID), url)
+				}).Return(&article, nil)
+
+				mock.GetAllPreviewMock.Return(nil, internalErr)
+
+				return mock
+			},
+		},
+		{
+			name: "negative case - articles mapper error",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/article/" + strconv.Itoa(articleID),
+			},
+			res: fiber.StatusInternalServerError,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+
+				return mock
+			},
+			tagMock: func(mc *minimock.Controller) interfaces.ITagRepository {
+				mock := repositoryMocks.NewITagRepositoryMock(mc)
+				mock.GetAllUsedMock.Return(tags, nil)
+
+				return mock
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				mock := repositoryMocks.NewIArticleRepositoryMock(mc)
+
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+					assert.Equal(mc, strconv.Itoa(articleID), url)
+				}).Return(&article, nil)
+
+				mock.GetAllPreviewMock.Return([]models.ArticlePreview{{}}, nil)
+
+				return mock
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fiberApp := fiber.New(test.GetFiberConfig())
+			fiberReq := httptest.NewRequest(tt.req.method, tt.req.route, nil)
+			serviceProvider := sp.InitMock(tt.cacheMock(mc), tt.tagMock(mc), tt.articleMock(mc))
+
+			fiberApp.Get("/article/:article", ArticleHandler(serviceProvider))
+
+			fiberRes, fiberErr := fiberApp.Test(fiberReq)
+			assert.Equal(t, tt.res, fiberRes.StatusCode)
+			assert.Equal(t, tt.err, fiberErr)
+		})
+	}
+}

+ 151 - 0
internal/services/handler/main_page_test.go

@@ -0,0 +1,151 @@
+package handler
+
+import (
+	"database/sql"
+	"errors"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/brianvoe/gofakeit/v6"
+	"github.com/dmitriygnatenko/internal/interfaces"
+	"github.com/dmitriygnatenko/internal/models"
+	repositoryMocks "github.com/dmitriygnatenko/internal/repositories/mocks"
+	sp "github.com/dmitriygnatenko/internal/service_provider"
+	cacheMocks "github.com/dmitriygnatenko/internal/services/cache/mocks"
+	"github.com/dmitriygnatenko/internal/services/handler/test"
+	"github.com/gofiber/fiber/v2"
+	"github.com/gojuno/minimock/v3"
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_MainPageHandler(t *testing.T) {
+	type cacheMockFunc func(mc *minimock.Controller) interfaces.ICache
+	type articleMockFunc func(mc *minimock.Controller) interfaces.IArticleRepository
+
+	type req struct {
+		method string
+		route  string
+	}
+
+	var (
+		mc          = minimock.NewController(t)
+		date        = gofakeit.Date()
+		publishTime = date.Format("2006-01-02 15:04:05")
+		internalErr = errors.New(gofakeit.Phrase())
+
+		previewArticles = []models.ArticlePreview{
+			{
+				ID:          gofakeit.Number(1, 100),
+				URL:         gofakeit.URL(),
+				Title:       gofakeit.Phrase(),
+				PublishTime: publishTime,
+				PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
+				Image:       sql.NullString{Valid: true, String: gofakeit.URL()},
+			},
+			{
+				ID:          gofakeit.Number(1, 100),
+				URL:         gofakeit.URL(),
+				Title:       gofakeit.Phrase(),
+				PublishTime: publishTime,
+				PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
+				Image:       sql.NullString{Valid: true, String: gofakeit.URL()},
+			},
+			{
+				ID:          gofakeit.Number(1, 100),
+				URL:         gofakeit.URL(),
+				Title:       gofakeit.Phrase(),
+				PublishTime: publishTime,
+				PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
+				Image:       sql.NullString{Valid: true, String: gofakeit.URL()},
+			},
+		}
+	)
+
+	tests := []struct {
+		name        string
+		req         req
+		res         int
+		err         error
+		cacheMock   cacheMockFunc
+		articleMock articleMockFunc
+	}{
+		{
+			name: "positive case",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/",
+			},
+			res: fiber.StatusOK,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+				mock.SetMock.Return()
+
+				return mock
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				mock := repositoryMocks.NewIArticleRepositoryMock(mc)
+				mock.GetAllPreviewMock.Return(previewArticles, nil)
+
+				return mock
+			},
+		},
+		{
+			name: "negative case - article repository error",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/",
+			},
+			res: fiber.StatusInternalServerError,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+
+				return mock
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				mock := repositoryMocks.NewIArticleRepositoryMock(mc)
+				mock.GetAllPreviewMock.Return(nil, internalErr)
+
+				return mock
+			},
+		},
+		{
+			name: "negative case - articles mapper error",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/",
+			},
+			res: fiber.StatusInternalServerError,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+
+				return mock
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				mock := repositoryMocks.NewIArticleRepositoryMock(mc)
+				mock.GetAllPreviewMock.Return([]models.ArticlePreview{{}}, nil)
+
+				return mock
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fiberApp := fiber.New(test.GetFiberConfig())
+			fiberReq := httptest.NewRequest(tt.req.method, tt.req.route, nil)
+			serviceProvider := sp.InitMock(tt.cacheMock(mc), tt.articleMock(mc))
+
+			fiberApp.Get("/", MainPageHandler(serviceProvider))
+
+			fiberRes, fiberErr := fiberApp.Test(fiberReq)
+			assert.Equal(t, tt.res, fiberRes.StatusCode)
+			assert.Equal(t, tt.err, fiberErr)
+		})
+	}
+}

+ 228 - 0
internal/services/handler/tag_test.go

@@ -0,0 +1,228 @@
+package handler
+
+import (
+	"context"
+	"database/sql"
+	"errors"
+	"net/http/httptest"
+	"strconv"
+	"testing"
+
+	"github.com/brianvoe/gofakeit/v6"
+	"github.com/dmitriygnatenko/internal/interfaces"
+	"github.com/dmitriygnatenko/internal/models"
+	repositoryMocks "github.com/dmitriygnatenko/internal/repositories/mocks"
+	sp "github.com/dmitriygnatenko/internal/service_provider"
+	cacheMocks "github.com/dmitriygnatenko/internal/services/cache/mocks"
+	"github.com/dmitriygnatenko/internal/services/handler/test"
+	"github.com/gofiber/fiber/v2"
+	"github.com/gojuno/minimock/v3"
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_TagHandler(t *testing.T) {
+	type cacheMockFunc func(mc *minimock.Controller) interfaces.ICache
+	type tagMockFunc func(mc *minimock.Controller) interfaces.ITagRepository
+	type articleMockFunc func(mc *minimock.Controller) interfaces.IArticleRepository
+
+	type req struct {
+		method string
+		route  string
+	}
+
+	var (
+		mc          = minimock.NewController(t)
+		tagID       = gofakeit.Number(1, 100)
+		date        = gofakeit.Date()
+		publishTime = date.Format("2006-01-02 15:04:05")
+		internalErr = errors.New(gofakeit.Phrase())
+
+		tag = models.Tag{
+			ID: tagID,
+		}
+
+		article = models.ArticlePreview{
+			ID:          gofakeit.Number(1, 100),
+			URL:         gofakeit.URL(),
+			Title:       gofakeit.Phrase(),
+			PublishTime: publishTime,
+			PreviewText: sql.NullString{Valid: true, String: gofakeit.Phrase()},
+			Image:       sql.NullString{Valid: true, String: gofakeit.URL()},
+		}
+	)
+
+	tests := []struct {
+		name        string
+		req         req
+		res         int
+		err         error
+		cacheMock   cacheMockFunc
+		tagMock     tagMockFunc
+		articleMock articleMockFunc
+	}{
+		{
+			name: "positive case",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/tag/" + strconv.Itoa(tagID),
+			},
+			res: fiber.StatusOK,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+				mock.SetMock.Return()
+
+				return mock
+			},
+			tagMock: func(mc *minimock.Controller) interfaces.ITagRepository {
+				mock := repositoryMocks.NewITagRepositoryMock(mc)
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+					assert.Equal(mc, strconv.Itoa(tagID), url)
+				}).Return(&tag, nil)
+
+				return mock
+
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				mock := repositoryMocks.NewIArticleRepositoryMock(mc)
+				mock.GetPreviewByTagIDMock.Inspect(func(ctx context.Context, id int) {
+					assert.Equal(mc, tagID, id)
+				}).Return([]models.ArticlePreview{article}, nil)
+
+				return mock
+			},
+		},
+		{
+			name: "negative case - tag not found",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/tag/" + strconv.Itoa(tagID),
+			},
+			res: fiber.StatusNotFound,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+
+				return mock
+			},
+			tagMock: func(mc *minimock.Controller) interfaces.ITagRepository {
+				mock := repositoryMocks.NewITagRepositoryMock(mc)
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+					assert.Equal(mc, strconv.Itoa(tagID), url)
+				}).Return(nil, sql.ErrNoRows)
+
+				return mock
+
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				return repositoryMocks.NewIArticleRepositoryMock(mc)
+			},
+		},
+		{
+			name: "negative case - tag repository error",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/tag/" + strconv.Itoa(tagID),
+			},
+			res: fiber.StatusInternalServerError,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+
+				return mock
+			},
+			tagMock: func(mc *minimock.Controller) interfaces.ITagRepository {
+				mock := repositoryMocks.NewITagRepositoryMock(mc)
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+					assert.Equal(mc, strconv.Itoa(tagID), url)
+				}).Return(nil, internalErr)
+
+				return mock
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				return repositoryMocks.NewIArticleRepositoryMock(mc)
+			},
+		},
+		{
+			name: "negative case - article repository error",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/tag/" + strconv.Itoa(tagID),
+			},
+			res: fiber.StatusInternalServerError,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+
+				return mock
+			},
+			tagMock: func(mc *minimock.Controller) interfaces.ITagRepository {
+				mock := repositoryMocks.NewITagRepositoryMock(mc)
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+					assert.Equal(mc, strconv.Itoa(tagID), url)
+				}).Return(&tag, nil)
+
+				return mock
+
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				mock := repositoryMocks.NewIArticleRepositoryMock(mc)
+				mock.GetPreviewByTagIDMock.Inspect(func(ctx context.Context, id int) {
+					assert.Equal(mc, tagID, id)
+				}).Return(nil, internalErr)
+
+				return mock
+			},
+		},
+		{
+			name: "negative case - article mapper error",
+			req: req{
+				method: fiber.MethodGet,
+				route:  "/tag/" + strconv.Itoa(tagID),
+			},
+			res: fiber.StatusInternalServerError,
+			err: nil,
+			cacheMock: func(mc *minimock.Controller) interfaces.ICache {
+				mock := cacheMocks.NewICacheMock(mc)
+				mock.GetMock.Return(nil, false)
+
+				return mock
+			},
+			tagMock: func(mc *minimock.Controller) interfaces.ITagRepository {
+				mock := repositoryMocks.NewITagRepositoryMock(mc)
+				mock.GetByURLMock.Inspect(func(ctx context.Context, url string) {
+					assert.Equal(mc, strconv.Itoa(tagID), url)
+				}).Return(&tag, nil)
+
+				return mock
+
+			},
+			articleMock: func(mc *minimock.Controller) interfaces.IArticleRepository {
+				mock := repositoryMocks.NewIArticleRepositoryMock(mc)
+				mock.GetPreviewByTagIDMock.Inspect(func(ctx context.Context, id int) {
+					assert.Equal(mc, tagID, id)
+				}).Return([]models.ArticlePreview{{}}, nil)
+
+				return mock
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fiberApp := fiber.New(test.GetFiberConfig())
+			fiberReq := httptest.NewRequest(tt.req.method, tt.req.route, nil)
+			serviceProvider := sp.InitMock(tt.cacheMock(mc), tt.tagMock(mc), tt.articleMock(mc))
+
+			fiberApp.Get("/tag/:tag", TagHandler(serviceProvider))
+
+			fiberRes, fiberErr := fiberApp.Test(fiberReq)
+			assert.Equal(t, tt.res, fiberRes.StatusCode)
+			assert.Equal(t, tt.err, fiberErr)
+		})
+	}
+}

+ 45 - 0
internal/services/handler/test/helper.go

@@ -0,0 +1,45 @@
+package test
+
+import (
+	"html/template"
+	"time"
+
+	"github.com/gofiber/fiber/v2"
+	"github.com/gofiber/template/html"
+)
+
+const (
+	testVersion = "1"
+	testGA      = false
+)
+
+func GetFiberConfig() fiber.Config {
+	engine := html.New("./../../templates", ".html")
+
+	// nolint:gocritic
+	engine.AddFunc("now", func() time.Time {
+		return time.Now()
+	})
+
+	// nolint:gosec
+	engine.AddFunc("noescape", func(str string) template.HTML {
+		return template.HTML(str)
+	})
+
+	engine.AddFunc("gridsep", func(i, l int) bool {
+		i++
+		return i%3 == 0 && i != l
+	})
+
+	engine.AddFunc("version", func() string {
+		return testVersion
+	})
+
+	engine.AddFunc("ga", func() bool {
+		return testGA
+	})
+
+	return fiber.Config{
+		Views: engine,
+	}
+}