diff --git a/application/article/getArticle/request.go b/application/article/getArticle/request.go new file mode 100644 index 00000000..97c0d43b --- /dev/null +++ b/application/article/getArticle/request.go @@ -0,0 +1,28 @@ +package getarticle + +import ( + "github.com/gofrs/uuid/v5" + "github.com/khanzadimahdi/testproject/domain" +) + +type Request struct { + CorrelationUUID string + LanguageCode string +} + +// Ensure Request implements domain.Validatable +var _ domain.Validatable = &Request{} + +func (r *Request) Validate() domain.ValidationErrors { + validationErrors := make(domain.ValidationErrors) + + if len(r.CorrelationUUID) == 0 { + validationErrors["correlation_uuid"] = "required_field" + } else { + if _, err := uuid.FromString(r.CorrelationUUID); err != nil { + validationErrors["correlation_uuid"] = "invalid_value" + } + } + + return validationErrors +} diff --git a/application/article/getArticle/response.go b/application/article/getArticle/response.go index 786f6666..6f968381 100644 --- a/application/article/getArticle/response.go +++ b/application/article/getArticle/response.go @@ -5,21 +5,26 @@ import ( "github.com/khanzadimahdi/testproject/application/element" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" ) type Response struct { - UUID string `json:"uuid"` - Cover string `json:"cover"` - Video string `json:"video"` - Title string `json:"title"` - Excerpt string `json:"excerpt"` - Body string `json:"body"` - PublishedAt string `json:"published_at"` - Author authorResponse `json:"author"` - Tags []string `json:"tags"` - ViewCount uint `json:"view_count"` - Elements []element.Response `json:"elements"` + ValidationErrors map[string]string `json:"validation_errors,omitempty"` + + CorrelationUUID string `json:"correlation_uuid"` + Cover string `json:"cover"` + Video string `json:"video"` + Title string `json:"title"` + Excerpt string `json:"excerpt"` + Body string `json:"body"` + PublishedAt string `json:"published_at"` + Author authorResponse `json:"author"` + Tags []string `json:"tags"` + ViewCount uint `json:"view_count"` + LanguageCode languageResponse `json:"language_code"` + AvailableLanguages []languageResponse `json:"available_languages"` + Elements []element.Response `json:"elements"` } type authorResponse struct { @@ -29,18 +34,31 @@ type authorResponse struct { Username string `json:"username"` } -func NewResponse(a article.Article, author user.User, elementsResponse []element.Response) *Response { +type languageResponse struct { + Code string `json:"code"` + Name string `json:"name"` +} + +func NewResponse(a article.Article, language language.Language, author user.User, availableLanguages []language.Language, elementsResponse []element.Response) *Response { tags := make([]string, len(a.Tags)) copy(tags, a.Tags) + languages := make([]languageResponse, len(availableLanguages)) + for i, l := range availableLanguages { + languages[i] = languageResponse{ + Code: l.Code, + Name: l.Name, + } + } + return &Response{ - UUID: a.UUID, - Cover: a.Cover, - Video: a.Video, - Title: a.Title, - Excerpt: a.Excerpt, - Body: a.Body, - PublishedAt: a.PublishedAt.Format(time.RFC3339), + CorrelationUUID: a.CorrelationUUID, + Cover: a.Cover, + Video: a.Video, + Title: a.Title, + Excerpt: a.Excerpt, + Body: a.Body, + PublishedAt: a.PublishedAt.Format(time.RFC3339), Author: authorResponse{ UUID: author.UUID, Name: author.Name, @@ -49,6 +67,11 @@ func NewResponse(a article.Article, author user.User, elementsResponse []element }, Tags: tags, ViewCount: a.ViewCount, - Elements: elementsResponse, + LanguageCode: languageResponse{ + Code: language.Code, + Name: language.Name, + }, + AvailableLanguages: languages, + Elements: elementsResponse, } } diff --git a/application/article/getArticle/usecase.go b/application/article/getArticle/usecase.go index 17bd13f9..06f2c2af 100644 --- a/application/article/getArticle/usecase.go +++ b/application/article/getArticle/usecase.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/khanzadimahdi/testproject/application/element" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" "github.com/khanzadimahdi/testproject/domain/user" @@ -13,23 +14,50 @@ import ( type UseCase struct { articleRepository article.Repository userRepository user.Repository + languageResolver resolver.Resolver elementRetriever *element.Retriever + validator domain.Validator } func NewUseCase( articleRepository article.Repository, userRepository user.Repository, + languageResolver resolver.Resolver, elementRetriever *element.Retriever, + validator domain.Validator, ) *UseCase { return &UseCase{ articleRepository: articleRepository, userRepository: userRepository, + languageResolver: languageResolver, elementRetriever: elementRetriever, + validator: validator, } } -func (uc *UseCase) Execute(UUID string) (*Response, error) { - a, err := uc.articleRepository.GetOnePublished(UUID) +func (uc *UseCase) Execute(request *Request) (*Response, error) { + if validationErrors := uc.validator.Validate(request); len(validationErrors) > 0 { + return &Response{ + ValidationErrors: validationErrors, + }, nil + } + + languageCode := request.LanguageCode + if len(languageCode) == 0 { + code, err := uc.languageResolver.DefaultCode() + if err != nil { + return nil, err + } + + languageCode = code + } + + l, err := uc.languageResolver.Resolve(languageCode) + if err != nil { + return nil, err + } + + a, err := uc.articleRepository.GetOnePublished(request.CorrelationUUID, languageCode) if err != nil { return nil, err } @@ -39,15 +67,20 @@ func (uc *UseCase) Execute(UUID string) (*Response, error) { return nil, err } - elementsResponse, err := uc.elementRetriever.RetrieveByVenues([]string{ - "articles/*", - fmt.Sprintf("articles/%s", UUID), - }) + availableLanguages, err := uc.articleRepository.GetPublishedLanguages(a.CorrelationUUID) + if err != nil { + return nil, err + } + + elementsResponse, err := uc.elementRetriever.RetrieveByVenues( + []string{"articles/*", fmt.Sprintf("articles/%s", a.CorrelationUUID)}, + languageCode, + ) if err != nil { return nil, err } defer uc.articleRepository.IncreaseView(a.UUID, 1) - return NewResponse(a, author, elementsResponse), nil + return NewResponse(a, l, author, availableLanguages, elementsResponse), nil } diff --git a/application/article/getArticle/usecase_test.go b/application/article/getArticle/usecase_test.go index 51100061..b7f5cb95 100644 --- a/application/article/getArticle/usecase_test.go +++ b/application/article/getArticle/usecase_test.go @@ -7,14 +7,17 @@ import ( "github.com/stretchr/testify/assert" "github.com/khanzadimahdi/testproject/application/element" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" domainElement "github.com/khanzadimahdi/testproject/domain/element" "github.com/khanzadimahdi/testproject/domain/element/component" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/elements" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/users" + "github.com/khanzadimahdi/testproject/infrastructure/validator" ) func TestUseCase_Execute(t *testing.T) { @@ -23,16 +26,21 @@ func TestUseCase_Execute(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + requestValidator validator.MockValidator mockComponent component.MockComponent - articleUUID string = "test-uuid" - authorUUID string = "author-uuid" - venues []string = []string{"articles/*", fmt.Sprintf("articles/%s", articleUUID)} - increaseView uint = 1 + correlationUUID string = "test-correlation-uuid" + articleUUID string = "test-uuid" + authorUUID string = "author-uuid" + venues []string = []string{"articles/*", fmt.Sprintf("articles/%s", correlationUUID)} + increaseView uint = 1 a = article.Article{ - UUID: articleUUID, - AuthorUUID: authorUUID, + UUID: articleUUID, + AuthorUUID: authorUUID, + LanguageCode: "EN", + CorrelationUUID: correlationUUID, } au = user.User{UUID: authorUUID, Name: "author-name", Avatar: "author-avatar"} va = []article.Article{ @@ -53,11 +61,21 @@ func TestUseCase_Execute(t *testing.T) { i[1].ContentUUID, i[2].ContentUUID, } + + request = Request{CorrelationUUID: correlationUUID} ) - articlesRepository.On("GetOnePublished", articleUUID).Once().Return(a, nil) + requestValidator.On("Validate", &request).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + + articlesRepository.On("GetOnePublished", correlationUUID, "EN").Once().Return(a, nil) + articlesRepository.On("GetPublishedLanguages", correlationUUID).Once().Return([]language.Language{{Code: "EN"}}, nil) articlesRepository.On("IncreaseView", articleUUID, increaseView).Once().Return(nil) - articlesRepository.On("GetByUUIDs", u).Once().Return(va, nil) + articlesRepository.On("GetByCorrelationUUIDs", u, "EN").Once().Return(va, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetOne", authorUUID).Once().Return(au, nil) @@ -76,7 +94,7 @@ func TestUseCase_Execute(t *testing.T) { defer elementsRepository.AssertExpectations(t) elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - response, err := NewUseCase(&articlesRepository, &userRepository, elementRetriever).Execute("test-uuid") + response, err := NewUseCase(&articlesRepository, &userRepository, &languageResolver, elementRetriever, &requestValidator).Execute(&request) assert.NotNil(t, response, "unexpected response") assert.NoError(t, err, "unexpected error") @@ -87,23 +105,33 @@ func TestUseCase_Execute(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + requestValidator validator.MockValidator - articleUUID string = "test-uuid" - expectedErr = domain.ErrNotExists + correlationUUID string = "test-correlation-uuid" + expectedErr = domain.ErrNotExists + + request = Request{CorrelationUUID: correlationUUID} ) - a := article.Article{} + requestValidator.On("Validate", &request).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) - articlesRepository.On("GetOnePublished", articleUUID).Once().Return(a, expectedErr) + articlesRepository.On("GetOnePublished", correlationUUID, "EN").Once().Return(article.Article{}, expectedErr) defer articlesRepository.AssertExpectations(t) elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - usecase := NewUseCase(&articlesRepository, &userRepository, elementRetriever) - response, err := usecase.Execute("test-uuid") + usecase := NewUseCase(&articlesRepository, &userRepository, &languageResolver, elementRetriever, &requestValidator) + response, err := usecase.Execute(&request) userRepository.AssertNotCalled(t, "GetOne") + articlesRepository.AssertNotCalled(t, "GetPublishedLanguages") elementsRepository.AssertNotCalled(t, "GetByVenues") - articlesRepository.AssertNotCalled(t, "GetByUUIDs") + articlesRepository.AssertNotCalled(t, "GetByCorrelationUUIDs") articlesRepository.AssertNotCalled(t, "IncreaseView") assert.Nil(t, response, "unexpected response") @@ -115,22 +143,37 @@ func TestUseCase_Execute(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + requestValidator validator.MockValidator mockComponent component.MockComponent - articleUUID string = "test-uuid" - authorUUID string = "missing-author-uuid" - venues []string = []string{"articles/*", fmt.Sprintf("articles/%s", articleUUID)} - increaseView uint = 1 + correlationUUID string = "test-correlation-uuid" + articleUUID string = "test-uuid" + authorUUID string = "missing-author-uuid" + venues []string = []string{"articles/*", fmt.Sprintf("articles/%s", correlationUUID)} + increaseView uint = 1 a = article.Article{ - UUID: articleUUID, - AuthorUUID: authorUUID, + UUID: articleUUID, + AuthorUUID: authorUUID, + LanguageCode: "EN", + CorrelationUUID: correlationUUID, } + + request = Request{CorrelationUUID: correlationUUID} ) - articlesRepository.On("GetOnePublished", articleUUID).Once().Return(a, nil) + requestValidator.On("Validate", &request).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + + articlesRepository.On("GetOnePublished", correlationUUID, "EN").Once().Return(a, nil) + articlesRepository.On("GetPublishedLanguages", correlationUUID).Once().Return([]language.Language{{Code: "EN"}}, nil) articlesRepository.On("IncreaseView", articleUUID, increaseView).Once().Return(nil) - articlesRepository.On("GetByUUIDs", []string{}).Once().Return(nil, nil) + articlesRepository.On("GetByCorrelationUUIDs", []string{}, "EN").Once().Return(nil, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetOne", authorUUID).Once().Return(user.User{}, domain.ErrNotExists) @@ -149,7 +192,7 @@ func TestUseCase_Execute(t *testing.T) { defer elementsRepository.AssertExpectations(t) elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - response, err := NewUseCase(&articlesRepository, &userRepository, elementRetriever).Execute("test-uuid") + response, err := NewUseCase(&articlesRepository, &userRepository, &languageResolver, elementRetriever, &requestValidator).Execute(&request) assert.NotNil(t, response, "unexpected response") assert.NoError(t, err, "unexpected error") @@ -160,19 +203,35 @@ func TestUseCase_Execute(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + requestValidator validator.MockValidator + + correlationUUID string = "test-correlation-uuid" + articleUUID string = "test-uuid" + authorUUID string = "author-uuid" + venues []string = []string{"articles/*", fmt.Sprintf("articles/%s", correlationUUID)} + expectedErr = domain.ErrNotExists - articleUUID string = "test-uuid" - authorUUID string = "author-uuid" - venues []string = []string{"articles/*", fmt.Sprintf("articles/%s", articleUUID)} - expectedErr = domain.ErrNotExists - a = article.Article{ - UUID: articleUUID, - AuthorUUID: authorUUID, + a = article.Article{ + UUID: articleUUID, + AuthorUUID: authorUUID, + LanguageCode: "EN", + CorrelationUUID: correlationUUID, } au = user.User{UUID: authorUUID} + + request = Request{CorrelationUUID: correlationUUID} ) - articlesRepository.On("GetOnePublished", articleUUID).Once().Return(a, nil) + requestValidator.On("Validate", &request).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + + articlesRepository.On("GetOnePublished", correlationUUID, "EN").Once().Return(a, nil) + articlesRepository.On("GetPublishedLanguages", correlationUUID).Once().Return([]language.Language{{Code: "EN"}}, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetOne", authorUUID).Once().Return(au, nil) @@ -182,10 +241,10 @@ func TestUseCase_Execute(t *testing.T) { defer elementsRepository.AssertExpectations(t) elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - usecase := NewUseCase(&articlesRepository, &userRepository, elementRetriever) - response, err := usecase.Execute("test-uuid") + usecase := NewUseCase(&articlesRepository, &userRepository, &languageResolver, elementRetriever, &requestValidator) + response, err := usecase.Execute(&request) - articlesRepository.AssertNotCalled(t, "GetByUUIDs") + articlesRepository.AssertNotCalled(t, "GetByCorrelationUUIDs") articlesRepository.AssertNotCalled(t, "IncreaseView") assert.Nil(t, response, "unexpected response") @@ -197,16 +256,21 @@ func TestUseCase_Execute(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + requestValidator validator.MockValidator mockComponent component.MockComponent - articleUUID string = "test-uuid" - authorUUID string = "author-uuid" - venues []string = []string{"articles/*", fmt.Sprintf("articles/%s", articleUUID)} - expectedErr = domain.ErrNotExists + correlationUUID string = "test-correlation-uuid" + articleUUID string = "test-uuid" + authorUUID string = "author-uuid" + venues []string = []string{"articles/*", fmt.Sprintf("articles/%s", correlationUUID)} + expectedErr = domain.ErrNotExists a = article.Article{ - UUID: articleUUID, - AuthorUUID: authorUUID, + UUID: articleUUID, + AuthorUUID: authorUUID, + LanguageCode: "EN", + CorrelationUUID: correlationUUID, } au = user.User{UUID: authorUUID} va = []article.Article{ @@ -223,10 +287,20 @@ func TestUseCase_Execute(t *testing.T) { i[1].ContentUUID, i[2].ContentUUID, } + + request = Request{CorrelationUUID: correlationUUID} ) - articlesRepository.On("GetOnePublished", articleUUID).Once().Return(a, nil) - articlesRepository.On("GetByUUIDs", u).Once().Return(nil, expectedErr) + requestValidator.On("Validate", &request).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + + articlesRepository.On("GetOnePublished", correlationUUID, "EN").Once().Return(a, nil) + articlesRepository.On("GetPublishedLanguages", correlationUUID).Once().Return([]language.Language{{Code: "EN"}}, nil) + articlesRepository.On("GetByCorrelationUUIDs", u, "EN").Once().Return(nil, expectedErr) defer articlesRepository.AssertExpectations(t) userRepository.On("GetOne", authorUUID).Once().Return(au, nil) @@ -243,7 +317,7 @@ func TestUseCase_Execute(t *testing.T) { defer elementsRepository.AssertExpectations(t) elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - response, err := NewUseCase(&articlesRepository, &userRepository, elementRetriever).Execute("test-uuid") + response, err := NewUseCase(&articlesRepository, &userRepository, &languageResolver, elementRetriever, &requestValidator).Execute(&request) articlesRepository.AssertNotCalled(t, "IncreaseView") @@ -251,22 +325,27 @@ func TestUseCase_Execute(t *testing.T) { assert.ErrorIs(t, err, expectedErr) }) - t.Run("error on increasing template count is not reflected on response", func(t *testing.T) { + t.Run("error on increasing view count is not reflected on response", func(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + requestValidator validator.MockValidator mockComponent component.MockComponent - articleUUID string = "test-uuid" - authorUUID string = "author-uuid" - venues []string = []string{"articles/*", fmt.Sprintf("articles/%s", articleUUID)} - increaseView uint = 1 - expectedErr = domain.ErrNotExists + correlationUUID string = "test-correlation-uuid" + articleUUID string = "test-uuid" + authorUUID string = "author-uuid" + venues []string = []string{"articles/*", fmt.Sprintf("articles/%s", correlationUUID)} + increaseView uint = 1 + expectedErr = domain.ErrNotExists a = article.Article{ - UUID: articleUUID, - AuthorUUID: authorUUID, + UUID: articleUUID, + AuthorUUID: authorUUID, + LanguageCode: "EN", + CorrelationUUID: correlationUUID, } au = user.User{UUID: authorUUID} va = []article.Article{ @@ -283,11 +362,21 @@ func TestUseCase_Execute(t *testing.T) { i[1].ContentUUID, i[2].ContentUUID, } + + request = Request{CorrelationUUID: correlationUUID} ) - articlesRepository.On("GetOnePublished", articleUUID).Once().Return(a, nil) + requestValidator.On("Validate", &request).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + + articlesRepository.On("GetOnePublished", correlationUUID, "EN").Once().Return(a, nil) + articlesRepository.On("GetPublishedLanguages", correlationUUID).Once().Return([]language.Language{{Code: "EN"}}, nil) articlesRepository.On("IncreaseView", articleUUID, increaseView).Once().Return(expectedErr) - articlesRepository.On("GetByUUIDs", u).Once().Return(va, nil) + articlesRepository.On("GetByCorrelationUUIDs", u, "EN").Once().Return(va, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetOne", authorUUID).Once().Return(au, nil) @@ -306,9 +395,62 @@ func TestUseCase_Execute(t *testing.T) { defer elementsRepository.AssertExpectations(t) elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - response, err := NewUseCase(&articlesRepository, &userRepository, elementRetriever).Execute("test-uuid") + response, err := NewUseCase(&articlesRepository, &userRepository, &languageResolver, elementRetriever, &requestValidator).Execute(&request) assert.NotNil(t, response, "unexpected response") assert.NoError(t, err, "unexpected error") }) + + t.Run("returns the article for the requested language", func(t *testing.T) { + var ( + articlesRepository articles.MockArticlesRepository + elementsRepository elements.MockElementsRepository + userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + requestValidator validator.MockValidator + + correlationUUID = "translation-uuid" + + faArticle = article.Article{ + UUID: "fa-uuid", + AuthorUUID: "author-fa", + LanguageCode: "FA", + CorrelationUUID: correlationUUID, + } + au = user.User{UUID: "author-fa", Name: "fa-author"} + + request = Request{CorrelationUUID: correlationUUID, LanguageCode: "FA"} + ) + + requestValidator.On("Validate", &request).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageResolver.On("Resolve", "FA").Once().Return(language.Language{Code: "FA"}, nil) + defer languageResolver.AssertExpectations(t) + + articlesRepository.On("GetOnePublished", correlationUUID, "FA").Once().Return(faArticle, nil) + articlesRepository.On("GetPublishedLanguages", correlationUUID).Once().Return([]language.Language{{Code: "EN"}, {Code: "FA"}}, nil) + articlesRepository.On("IncreaseView", "fa-uuid", uint(1)).Once().Return(nil) + articlesRepository.On("GetByCorrelationUUIDs", []string{}, "FA").Once().Return(nil, nil) + defer articlesRepository.AssertExpectations(t) + + userRepository.On("GetOne", "author-fa").Once().Return(au, nil) + userRepository.On("GetByUUIDs", []string{}).Once().Return([]user.User{}, nil) + defer userRepository.AssertExpectations(t) + + elementsRepository.On( + "GetByVenues", + []string{"articles/*", "articles/translation-uuid"}, + ).Once().Return([]domainElement.Element{}, nil) + defer elementsRepository.AssertExpectations(t) + + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) + response, err := NewUseCase(&articlesRepository, &userRepository, &languageResolver, elementRetriever, &requestValidator).Execute(&request) + + assert.NoError(t, err) + assert.NotNil(t, response) + assert.Equal(t, correlationUUID, response.CorrelationUUID) + assert.Equal(t, "FA", response.LanguageCode.Code) + assert.Len(t, response.AvailableLanguages, 2) + }) } diff --git a/application/article/getArticles/request.go b/application/article/getArticles/request.go index 5f25d371..d86156b8 100644 --- a/application/article/getArticles/request.go +++ b/application/article/getArticles/request.go @@ -1,5 +1,6 @@ package getarticles type Request struct { - Page uint + LanguageCode string + Page uint } diff --git a/application/article/getArticles/response.go b/application/article/getArticles/response.go index 1cc03da4..d4961417 100644 --- a/application/article/getArticles/response.go +++ b/application/article/getArticles/response.go @@ -4,22 +4,26 @@ import ( "time" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" ) type Response struct { - Items []articleResponse `json:"items"` + Items []articleResponse `json:"items"` + LanguageCode languageResponse `json:"language_code"` + Pagination paginationResponse `json:"pagination"` } type articleResponse struct { - UUID string `json:"uuid"` - Cover string `json:"cover"` - Video string `json:"video"` - Title string `json:"title"` - Excerpt string `json:"excerpt"` - PublishedAt string `json:"published_at"` - Author authorResponse `json:"author"` + UUID string `json:"uuid"` + Cover string `json:"cover"` + Video string `json:"video"` + Title string `json:"title"` + Excerpt string `json:"excerpt"` + PublishedAt string `json:"published_at"` + Author authorResponse `json:"author"` + AvailableLanguages []languageResponse `json:"available_languages"` } type authorResponse struct { @@ -29,12 +33,17 @@ type authorResponse struct { Username string `json:"username"` } +type languageResponse struct { + Code string `json:"code"` + Name string `json:"name"` +} + type paginationResponse struct { TotalPages uint `json:"total_pages"` CurrentPage uint `json:"current_page"` } -func NewResponse(a []article.Article, authors []user.User, totalPages, currentPage uint) *Response { +func NewResponse(a []article.Article, authors []user.User, articlesPublishedLanguages map[string][]language.Language, requestedLanguage language.Language, totalPages, currentPage uint) *Response { authorByUUID := make(map[string]user.User, len(authors)) for i := range authors { authorByUUID[authors[i].UUID] = authors[i] @@ -56,10 +65,23 @@ func NewResponse(a []article.Article, authors []user.User, totalPages, currentPa items[i].Author.Avatar = u.Avatar items[i].Author.Username = u.Username } + + if al, ok := articlesPublishedLanguages[a[i].UUID]; ok { + for l := range al { + items[i].AvailableLanguages = append(items[i].AvailableLanguages, languageResponse{ + Code: al[l].Code, + Name: al[l].Name, + }) + } + } } return &Response{ Items: items, + LanguageCode: languageResponse{ + Code: requestedLanguage.Code, + Name: requestedLanguage.Name, + }, Pagination: paginationResponse{ TotalPages: totalPages, CurrentPage: currentPage, diff --git a/application/article/getArticles/usecase.go b/application/article/getArticles/usecase.go index 11038f62..e3242413 100644 --- a/application/article/getArticles/usecase.go +++ b/application/article/getArticles/usecase.go @@ -1,7 +1,9 @@ package getarticles import ( + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" ) @@ -10,20 +12,38 @@ const limit = 10 type UseCase struct { articleRepository article.Repository userRepository user.Repository + languageResolver resolver.Resolver } func NewUseCase( articleRepository article.Repository, userRepository user.Repository, + languageResolver resolver.Resolver, ) *UseCase { return &UseCase{ articleRepository: articleRepository, userRepository: userRepository, + languageResolver: languageResolver, } } func (uc *UseCase) Execute(request *Request) (*Response, error) { - totalArticles, err := uc.articleRepository.CountPublished() + languageCode := request.LanguageCode + if len(languageCode) == 0 { + code, err := uc.languageResolver.DefaultCode() + if err != nil { + return nil, err + } + + languageCode = code + } + + l, err := uc.languageResolver.Resolve(languageCode) + if err != nil { + return nil, err + } + + totalArticles, err := uc.articleRepository.CountPublished(languageCode) if err != nil { return nil, err } @@ -44,7 +64,7 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { totalPages++ } - a, err := uc.articleRepository.GetAllPublished(offset, limit) + a, err := uc.articleRepository.GetAllPublished(languageCode, offset, limit) if err != nil { return nil, err } @@ -59,5 +79,14 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { return nil, err } - return NewResponse(a, authors, totalPages, currentPage), nil + publishedLanguages := make(map[string][]language.Language, len(a)) + for i := range a { + al, err := uc.articleRepository.GetPublishedLanguages(a[i].CorrelationUUID) + if err != nil { + return nil, err + } + publishedLanguages[a[i].UUID] = al + } + + return NewResponse(a, authors, publishedLanguages, l, totalPages, currentPage), nil } diff --git a/application/article/getArticles/usecase_test.go b/application/article/getArticles/usecase_test.go index 27ea2e90..96220521 100644 --- a/application/article/getArticles/usecase_test.go +++ b/application/article/getArticles/usecase_test.go @@ -6,7 +6,9 @@ import ( "github.com/stretchr/testify/assert" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/users" @@ -17,6 +19,7 @@ func TestUseCase_Execute(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver a = []article.Article{ {UUID: "test-article-1", AuthorUUID: "author-uuid-1"}, @@ -29,14 +32,20 @@ func TestUseCase_Execute(t *testing.T) { } ) - articlesRepository.On("CountPublished").Once().Return(uint(1), nil) - articlesRepository.On("GetAllPublished", uint(0), uint(10)).Once().Return(a, nil) + articlesRepository.On("CountPublished", "EN").Once().Return(uint(1), nil) + articlesRepository.On("GetAllPublished", "EN", uint(0), uint(10)).Once().Return(a, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetByUUIDs", []string{"author-uuid-1", "author-uuid-2", "author-uuid-1"}).Once().Return(u, nil) defer userRepository.AssertExpectations(t) - usecase := NewUseCase(&articlesRepository, &userRepository) + articlesRepository.On("GetPublishedLanguages", "").Return([]language.Language{}, nil) + + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + + usecase := NewUseCase(&articlesRepository, &userRepository, &languageResolver) request := Request{Page: 1} response, err := usecase.Execute(&request) @@ -48,13 +57,18 @@ func TestUseCase_Execute(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver expectedErr = errors.New("test error") ) - articlesRepository.On("CountPublished").Once().Return(uint(1), expectedErr) + articlesRepository.On("CountPublished", "EN").Once().Return(uint(1), expectedErr) defer articlesRepository.AssertExpectations(t) - usecase := NewUseCase(&articlesRepository, &userRepository) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + + usecase := NewUseCase(&articlesRepository, &userRepository, &languageResolver) request := Request{Page: 1} response, err := usecase.Execute(&request) @@ -69,14 +83,19 @@ func TestUseCase_Execute(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver expectedErr = errors.New("test error") ) - articlesRepository.On("CountPublished").Once().Return(uint(1), nil) - articlesRepository.On("GetAllPublished", uint(0), uint(10)).Once().Return(nil, expectedErr) + articlesRepository.On("CountPublished", "EN").Once().Return(uint(1), nil) + articlesRepository.On("GetAllPublished", "EN", uint(0), uint(10)).Once().Return(nil, expectedErr) defer articlesRepository.AssertExpectations(t) - usecase := NewUseCase(&articlesRepository, &userRepository) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + + usecase := NewUseCase(&articlesRepository, &userRepository, &languageResolver) request := Request{Page: 1} response, err := usecase.Execute(&request) @@ -90,6 +109,7 @@ func TestUseCase_Execute(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver expectedErr = errors.New("test error") a = []article.Article{ @@ -97,14 +117,18 @@ func TestUseCase_Execute(t *testing.T) { } ) - articlesRepository.On("CountPublished").Once().Return(uint(1), nil) - articlesRepository.On("GetAllPublished", uint(0), uint(10)).Once().Return(a, nil) + articlesRepository.On("CountPublished", "EN").Once().Return(uint(1), nil) + articlesRepository.On("GetAllPublished", "EN", uint(0), uint(10)).Once().Return(a, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetByUUIDs", []string{"author-uuid-1"}).Once().Return(nil, expectedErr) defer userRepository.AssertExpectations(t) - usecase := NewUseCase(&articlesRepository, &userRepository) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + + usecase := NewUseCase(&articlesRepository, &userRepository, &languageResolver) request := Request{Page: 1} response, err := usecase.Execute(&request) diff --git a/application/article/getArticlesByAuthor/request.go b/application/article/getArticlesByAuthor/request.go index e90ac9c0..dcc62ee9 100644 --- a/application/article/getArticlesByAuthor/request.go +++ b/application/article/getArticlesByAuthor/request.go @@ -8,9 +8,10 @@ import ( ) type Request struct { - AuthorUUID string - Username string - Page uint + AuthorUUID string + Username string + Page uint + LanguageCode string } var _ domain.Validatable = &Request{} @@ -24,7 +25,7 @@ func (r *Request) Validate() domain.ValidationErrors { if len(r.AuthorUUID) > 0 { if _, err := uuid.FromString(r.AuthorUUID); err != nil { - validationErrors["uuid"] = "invalid_value" + validationErrors["author_uuid"] = "invalid_value" } } diff --git a/application/article/getArticlesByAuthor/request_test.go b/application/article/getArticlesByAuthor/request_test.go index 45ad359c..c6d9b89a 100644 --- a/application/article/getArticlesByAuthor/request_test.go +++ b/application/article/getArticlesByAuthor/request_test.go @@ -72,7 +72,7 @@ func TestRequest_Validate(t *testing.T) { Page: 1, }, want: domain.ValidationErrors{ - "uuid": "invalid_value", + "author_uuid": "invalid_value", }, }, { @@ -83,8 +83,8 @@ func TestRequest_Validate(t *testing.T) { Page: 1, }, want: domain.ValidationErrors{ - "uuid": "invalid_value", - "username": "invalid_value", + "author_uuid": "invalid_value", + "username": "invalid_value", }, }, } diff --git a/application/article/getArticlesByAuthor/response.go b/application/article/getArticlesByAuthor/response.go index fc93c60f..6de10a80 100644 --- a/application/article/getArticlesByAuthor/response.go +++ b/application/article/getArticlesByAuthor/response.go @@ -5,24 +5,27 @@ import ( "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" ) type Response struct { ValidationErrors domain.ValidationErrors `json:"errors,omitempty"` - Author authorResponse `json:"author"` - Items []articleResponse `json:"items"` - Pagination paginationResponse `json:"pagination"` + Author authorResponse `json:"author"` + LanguageCode languageResponse `json:"language_code"` + Items []articleResponse `json:"items"` + Pagination paginationResponse `json:"pagination"` } type articleResponse struct { - UUID string `json:"uuid"` - Cover string `json:"cover"` - Video string `json:"video"` - Title string `json:"title"` - Excerpt string `json:"excerpt"` - PublishedAt string `json:"published_at"` + UUID string `json:"uuid"` + Cover string `json:"cover"` + Video string `json:"video"` + Title string `json:"title"` + Excerpt string `json:"excerpt"` + PublishedAt string `json:"published_at"` + AvailableLanguages []languageResponse `json:"available_languages"` } type authorResponse struct { @@ -33,12 +36,17 @@ type authorResponse struct { CreatedAt string `json:"created_at"` } +type languageResponse struct { + Code string `json:"code"` + Name string `json:"name"` +} + type paginationResponse struct { TotalPages uint `json:"total_pages"` CurrentPage uint `json:"current_page"` } -func NewResponse(author user.User, a []article.Article, totalPages, currentPage uint) *Response { +func NewResponse(author user.User, a []article.Article, articlesPublishedLanguages map[string][]language.Language, requestedLanguage language.Language, totalPages, currentPage uint) *Response { items := make([]articleResponse, len(a)) for i := range a { @@ -48,6 +56,15 @@ func NewResponse(author user.User, a []article.Article, totalPages, currentPage items[i].Title = a[i].Title items[i].Excerpt = a[i].Excerpt items[i].PublishedAt = a[i].PublishedAt.Format(time.RFC3339) + + if al, ok := articlesPublishedLanguages[a[i].UUID]; ok { + for l := range al { + items[i].AvailableLanguages = append(items[i].AvailableLanguages, languageResponse{ + Code: al[l].Code, + Name: al[l].Name, + }) + } + } } return &Response{ @@ -58,6 +75,10 @@ func NewResponse(author user.User, a []article.Article, totalPages, currentPage Username: author.Username, CreatedAt: author.CreatedAt.Format(time.RFC3339), }, + LanguageCode: languageResponse{ + Code: requestedLanguage.Code, + Name: requestedLanguage.Name, + }, Items: items, Pagination: paginationResponse{ TotalPages: totalPages, diff --git a/application/article/getArticlesByAuthor/usecase.go b/application/article/getArticlesByAuthor/usecase.go index 99d6888c..54d0dd7c 100644 --- a/application/article/getArticlesByAuthor/usecase.go +++ b/application/article/getArticlesByAuthor/usecase.go @@ -1,8 +1,10 @@ package getArticlesByAuthor import ( + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" ) @@ -11,17 +13,20 @@ const limit = 10 type UseCase struct { articleRepository article.Repository userRepository user.Repository + languageResolver resolver.Resolver validator domain.Validator } func NewUseCase( articleRepository article.Repository, userRepository user.Repository, + languageResolver resolver.Resolver, validator domain.Validator, ) *UseCase { return &UseCase{ articleRepository: articleRepository, userRepository: userRepository, + languageResolver: languageResolver, validator: validator, } } @@ -33,17 +38,35 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { }, nil } + languageCode := request.LanguageCode + if len(languageCode) == 0 { + code, err := uc.languageResolver.DefaultCode() + if err != nil { + return nil, err + } + + languageCode = code + } + + l, err := uc.languageResolver.Resolve(languageCode) + if err != nil { + return nil, err + } + author, err := uc.resolveAuthor(request) if err != nil { return nil, err } - totalArticles, err := uc.articleRepository.CountPublishedByAuthor(author.UUID) + totalArticles, err := uc.articleRepository.CountPublishedByAuthor(author.UUID, languageCode) if err != nil { return nil, err } - currentPage := currentPageOf(request) + currentPage := request.Page + if currentPage == 0 { + currentPage = 1 + } var offset uint = 0 if currentPage > 0 { @@ -56,12 +79,21 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { totalPages++ } - a, err := uc.articleRepository.GetPublishedByAuthor(author.UUID, offset, limit) + a, err := uc.articleRepository.GetPublishedByAuthor(author.UUID, languageCode, offset, limit) if err != nil { return nil, err } - return NewResponse(author, a, totalPages, currentPage), nil + publishedLanguages := make(map[string][]language.Language, len(a)) + for i := range a { + al, err := uc.articleRepository.GetPublishedLanguages(a[i].CorrelationUUID) + if err != nil { + return nil, err + } + publishedLanguages[a[i].UUID] = al + } + + return NewResponse(author, a, publishedLanguages, l, totalPages, currentPage), nil } func (uc *UseCase) resolveAuthor(request *Request) (user.User, error) { @@ -71,10 +103,3 @@ func (uc *UseCase) resolveAuthor(request *Request) (user.User, error) { return uc.userRepository.GetOneByIdentity(request.Username) } - -func currentPageOf(request *Request) uint { - if request.Page == 0 { - return 1 - } - return request.Page -} diff --git a/application/article/getArticlesByAuthor/usecase_test.go b/application/article/getArticlesByAuthor/usecase_test.go index 6cc3a916..dd593676 100644 --- a/application/article/getArticlesByAuthor/usecase_test.go +++ b/application/article/getArticlesByAuthor/usecase_test.go @@ -5,8 +5,10 @@ import ( "testing" "time" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/users" @@ -23,6 +25,7 @@ func TestUseCase_Execute(t *testing.T) { var ( articleRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator authorUUID = "author-uuid" @@ -45,14 +48,19 @@ func TestUseCase_Execute(t *testing.T) { requestValidator.On("Validate", &request).Once().Return(nil) defer requestValidator.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + userRepository.On("GetOneByIdentity", u.Username).Once().Return(u, nil) defer userRepository.AssertExpectations(t) - articleRepository.On("CountPublishedByAuthor", authorUUID).Once().Return(uint(len(a)), nil) - articleRepository.On("GetPublishedByAuthor", authorUUID, uint(0), uint(10)).Once().Return(a, nil) + articleRepository.On("CountPublishedByAuthor", authorUUID, "EN").Once().Return(uint(len(a)), nil) + articleRepository.On("GetPublishedByAuthor", authorUUID, "EN", uint(0), uint(10)).Once().Return(a, nil) + articleRepository.On("GetPublishedLanguages", "").Return([]language.Language{}, nil) defer articleRepository.AssertExpectations(t) - response, err := NewUseCase(&articleRepository, &userRepository, &requestValidator).Execute(&request) + response, err := NewUseCase(&articleRepository, &userRepository, &languageResolver, &requestValidator).Execute(&request) assert.NoError(t, err) assert.NotNil(t, response) @@ -70,6 +78,7 @@ func TestUseCase_Execute(t *testing.T) { var ( articleRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator authorUUID = "author-uuid" @@ -88,14 +97,19 @@ func TestUseCase_Execute(t *testing.T) { requestValidator.On("Validate", &request).Once().Return(nil) defer requestValidator.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + userRepository.On("GetOne", authorUUID).Once().Return(u, nil) defer userRepository.AssertExpectations(t) - articleRepository.On("CountPublishedByAuthor", authorUUID).Once().Return(uint(len(a)), nil) - articleRepository.On("GetPublishedByAuthor", authorUUID, uint(0), uint(10)).Once().Return(a, nil) + articleRepository.On("CountPublishedByAuthor", authorUUID, "EN").Once().Return(uint(len(a)), nil) + articleRepository.On("GetPublishedByAuthor", authorUUID, "EN", uint(0), uint(10)).Once().Return(a, nil) + articleRepository.On("GetPublishedLanguages", "").Return([]language.Language{}, nil) defer articleRepository.AssertExpectations(t) - response, err := NewUseCase(&articleRepository, &userRepository, &requestValidator).Execute(&request) + response, err := NewUseCase(&articleRepository, &userRepository, &languageResolver, &requestValidator).Execute(&request) userRepository.AssertNotCalled(t, "GetOneByIdentity") @@ -110,6 +124,7 @@ func TestUseCase_Execute(t *testing.T) { var ( articleRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator request = Request{Page: 1} @@ -123,7 +138,7 @@ func TestUseCase_Execute(t *testing.T) { requestValidator.On("Validate", &request).Once().Return(expectedResponse.ValidationErrors) defer requestValidator.AssertExpectations(t) - response, err := NewUseCase(&articleRepository, &userRepository, &requestValidator).Execute(&request) + response, err := NewUseCase(&articleRepository, &userRepository, &languageResolver, &requestValidator).Execute(&request) userRepository.AssertNotCalled(t, "GetOne") userRepository.AssertNotCalled(t, "GetOneByIdentity") @@ -140,6 +155,7 @@ func TestUseCase_Execute(t *testing.T) { var ( articleRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator request = Request{Username: "ghost", Page: 1} @@ -148,10 +164,14 @@ func TestUseCase_Execute(t *testing.T) { requestValidator.On("Validate", &request).Once().Return(nil) defer requestValidator.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + userRepository.On("GetOneByIdentity", request.Username).Once().Return(user.User{}, domain.ErrNotExists) defer userRepository.AssertExpectations(t) - response, err := NewUseCase(&articleRepository, &userRepository, &requestValidator).Execute(&request) + response, err := NewUseCase(&articleRepository, &userRepository, &languageResolver, &requestValidator).Execute(&request) articleRepository.AssertNotCalled(t, "CountPublishedByAuthor") articleRepository.AssertNotCalled(t, "GetPublishedByAuthor") @@ -166,6 +186,7 @@ func TestUseCase_Execute(t *testing.T) { var ( articleRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator expectedErr = errors.New("user repo failure") @@ -175,10 +196,14 @@ func TestUseCase_Execute(t *testing.T) { requestValidator.On("Validate", &request).Once().Return(nil) defer requestValidator.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + userRepository.On("GetOneByIdentity", request.Username).Once().Return(user.User{}, expectedErr) defer userRepository.AssertExpectations(t) - response, err := NewUseCase(&articleRepository, &userRepository, &requestValidator).Execute(&request) + response, err := NewUseCase(&articleRepository, &userRepository, &languageResolver, &requestValidator).Execute(&request) articleRepository.AssertNotCalled(t, "CountPublishedByAuthor") articleRepository.AssertNotCalled(t, "GetPublishedByAuthor") @@ -193,6 +218,7 @@ func TestUseCase_Execute(t *testing.T) { var ( articleRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator authorUUID = "author-uuid" @@ -204,13 +230,17 @@ func TestUseCase_Execute(t *testing.T) { requestValidator.On("Validate", &request).Once().Return(nil) defer requestValidator.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + userRepository.On("GetOneByIdentity", u.Username).Once().Return(u, nil) defer userRepository.AssertExpectations(t) - articleRepository.On("CountPublishedByAuthor", authorUUID).Once().Return(uint(0), expectedErr) + articleRepository.On("CountPublishedByAuthor", authorUUID, "EN").Once().Return(uint(0), expectedErr) defer articleRepository.AssertExpectations(t) - response, err := NewUseCase(&articleRepository, &userRepository, &requestValidator).Execute(&request) + response, err := NewUseCase(&articleRepository, &userRepository, &languageResolver, &requestValidator).Execute(&request) articleRepository.AssertNotCalled(t, "GetPublishedByAuthor") @@ -224,6 +254,7 @@ func TestUseCase_Execute(t *testing.T) { var ( articleRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator authorUUID = "author-uuid" @@ -235,14 +266,18 @@ func TestUseCase_Execute(t *testing.T) { requestValidator.On("Validate", &request).Once().Return(nil) defer requestValidator.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + userRepository.On("GetOneByIdentity", u.Username).Once().Return(u, nil) defer userRepository.AssertExpectations(t) - articleRepository.On("CountPublishedByAuthor", authorUUID).Once().Return(uint(5), nil) - articleRepository.On("GetPublishedByAuthor", authorUUID, uint(0), uint(10)).Once().Return(nil, expectedErr) + articleRepository.On("CountPublishedByAuthor", authorUUID, "EN").Once().Return(uint(5), nil) + articleRepository.On("GetPublishedByAuthor", authorUUID, "EN", uint(0), uint(10)).Once().Return(nil, expectedErr) defer articleRepository.AssertExpectations(t) - response, err := NewUseCase(&articleRepository, &userRepository, &requestValidator).Execute(&request) + response, err := NewUseCase(&articleRepository, &userRepository, &languageResolver, &requestValidator).Execute(&request) assert.ErrorIs(t, err, expectedErr) assert.Nil(t, response) diff --git a/application/article/getArticlesByHashtag/request.go b/application/article/getArticlesByHashtag/request.go index 348ab747..3cf592b5 100644 --- a/application/article/getArticlesByHashtag/request.go +++ b/application/article/getArticlesByHashtag/request.go @@ -5,8 +5,9 @@ import ( ) type Request struct { - Hashtag string - Page uint + Hashtag string + LanguageCode string + Page uint } var _ domain.Validatable = &Request{} diff --git a/application/article/getArticlesByHashtag/response.go b/application/article/getArticlesByHashtag/response.go index a606982c..18c3d0a6 100644 --- a/application/article/getArticlesByHashtag/response.go +++ b/application/article/getArticlesByHashtag/response.go @@ -5,24 +5,27 @@ import ( "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" ) type Response struct { ValidationErrors domain.ValidationErrors `json:"errors,omitempty"` - Items []articleResponse `json:"items"` - Pagination paginationResponse `json:"pagination"` + Items []articleResponse `json:"items"` + LanguageCode languageResponse `json:"language_code"` + Pagination paginationResponse `json:"pagination"` } type articleResponse struct { - UUID string `json:"uuid"` - Cover string `json:"cover"` - Video string `json:"video"` - Title string `json:"title"` - Excerpt string `json:"excerpt"` - PublishedAt string `json:"published_at"` - Author authorResponse `json:"author"` + UUID string `json:"uuid"` + Cover string `json:"cover"` + Video string `json:"video"` + Title string `json:"title"` + Excerpt string `json:"excerpt"` + PublishedAt string `json:"published_at"` + Author authorResponse `json:"author"` + AvailableLanguages []languageResponse `json:"available_languages"` } type authorResponse struct { @@ -32,12 +35,17 @@ type authorResponse struct { Username string `json:"username"` } +type languageResponse struct { + Code string `json:"code"` + Name string `json:"name"` +} + type paginationResponse struct { TotalPages uint `json:"total_pages"` CurrentPage uint `json:"current_page"` } -func NewResponse(a []article.Article, authors []user.User, totalPages, currentPage uint) *Response { +func NewResponse(a []article.Article, authors []user.User, articlesPublishedLanguages map[string][]language.Language, requestedLanguage language.Language, totalPages, currentPage uint) *Response { authorByUUID := make(map[string]user.User, len(authors)) for i := range authors { authorByUUID[authors[i].UUID] = authors[i] @@ -59,10 +67,23 @@ func NewResponse(a []article.Article, authors []user.User, totalPages, currentPa items[i].Author.Avatar = u.Avatar items[i].Author.Username = u.Username } + + if al, ok := articlesPublishedLanguages[a[i].UUID]; ok { + for l := range al { + items[i].AvailableLanguages = append(items[i].AvailableLanguages, languageResponse{ + Code: al[l].Code, + Name: al[l].Name, + }) + } + } } return &Response{ Items: items, + LanguageCode: languageResponse{ + Code: requestedLanguage.Code, + Name: requestedLanguage.Name, + }, Pagination: paginationResponse{ TotalPages: totalPages, CurrentPage: currentPage, diff --git a/application/article/getArticlesByHashtag/usecase.go b/application/article/getArticlesByHashtag/usecase.go index 0e852194..16f4c0c2 100644 --- a/application/article/getArticlesByHashtag/usecase.go +++ b/application/article/getArticlesByHashtag/usecase.go @@ -1,8 +1,10 @@ package getArticlesByHashtag import ( + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" ) @@ -11,17 +13,20 @@ const limit = 10 type UseCase struct { articleRepository article.Repository userRepository user.Repository + languageResolver resolver.Resolver validator domain.Validator } func NewUseCase( articleRepository article.Repository, userRepository user.Repository, + languageResolver resolver.Resolver, validator domain.Validator, ) *UseCase { return &UseCase{ articleRepository: articleRepository, userRepository: userRepository, + languageResolver: languageResolver, validator: validator, } } @@ -35,7 +40,22 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { hashtags := []string{request.Hashtag} - totalArticles, err := uc.articleRepository.CountPublishedByHashtags(hashtags) + languageCode := request.LanguageCode + if len(languageCode) == 0 { + code, err := uc.languageResolver.DefaultCode() + if err != nil { + return nil, err + } + + languageCode = code + } + + l, err := uc.languageResolver.Resolve(languageCode) + if err != nil { + return nil, err + } + + totalArticles, err := uc.articleRepository.CountPublishedByHashtags(hashtags, languageCode) if err != nil { return nil, err } @@ -56,7 +76,7 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { totalPages++ } - a, err := uc.articleRepository.GetPublishedByHashtags(hashtags, offset, limit) + a, err := uc.articleRepository.GetPublishedByHashtags(hashtags, languageCode, offset, limit) if err != nil { return nil, err } @@ -66,10 +86,19 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { userUUIDs[i] = a[i].AuthorUUID } - u, err := uc.userRepository.GetByUUIDs(userUUIDs) + authors, err := uc.userRepository.GetByUUIDs(userUUIDs) if err != nil { return nil, err } - return NewResponse(a, u, totalPages, currentPage), nil + publishedLanguages := make(map[string][]language.Language, len(a)) + for i := range a { + al, err := uc.articleRepository.GetPublishedLanguages(a[i].CorrelationUUID) + if err != nil { + return nil, err + } + publishedLanguages[a[i].UUID] = al + } + + return NewResponse(a, authors, publishedLanguages, l, totalPages, currentPage), nil } diff --git a/application/article/getArticlesByHashtag/usecase_test.go b/application/article/getArticlesByHashtag/usecase_test.go index fe3ac3f2..60115b25 100644 --- a/application/article/getArticlesByHashtag/usecase_test.go +++ b/application/article/getArticlesByHashtag/usecase_test.go @@ -4,8 +4,10 @@ import ( "errors" "testing" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/users" @@ -20,9 +22,10 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - repository articles.MockArticlesRepository - userRepository users.MockUsersRepository - validator validator.MockValidator + repository articles.MockArticlesRepository + userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + validator validator.MockValidator hashtag = "test-hashtag" a = []article.Article{ @@ -40,14 +43,20 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &request).Once().Return(nil) defer validator.AssertExpectations(t) - repository.On("CountPublishedByHashtags", []string{hashtag}).Once().Return(uint(len(a)), nil) - repository.On("GetPublishedByHashtags", []string{hashtag}, uint(0), uint(10)).Once().Return(a, nil) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + + repository.On("CountPublishedByHashtags", []string{hashtag}, "EN").Once().Return(uint(len(a)), nil) + repository.On("GetPublishedByHashtags", []string{hashtag}, "EN", uint(0), uint(10)).Once().Return(a, nil) defer repository.AssertExpectations(t) userRepository.On("GetByUUIDs", []string{"author-uuid-1", "author-uuid-2", "author-uuid-1"}).Once().Return(u, nil) defer userRepository.AssertExpectations(t) - usecase := NewUseCase(&repository, &userRepository, &validator) + repository.On("GetPublishedLanguages", "").Return([]language.Language{}, nil) + + usecase := NewUseCase(&repository, &userRepository, &languageResolver, &validator) response, err := usecase.Execute(&request) assert.NoError(t, err, "unexpected error") @@ -58,9 +67,10 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - repository articles.MockArticlesRepository - userRepository users.MockUsersRepository - validator validator.MockValidator + repository articles.MockArticlesRepository + userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + validator validator.MockValidator hashtag = "test-hashtag" request = Request{Page: 1, Hashtag: hashtag} @@ -75,7 +85,7 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &request).Once().Return(expectedResponse.ValidationErrors) defer validator.AssertExpectations(t) - usecase := NewUseCase(&repository, &userRepository, &validator) + usecase := NewUseCase(&repository, &userRepository, &languageResolver, &validator) response, err := usecase.Execute(&request) repository.AssertNotCalled(t, "CountPublishedByHashtags") @@ -90,9 +100,10 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - repository articles.MockArticlesRepository - userRepository users.MockUsersRepository - validator validator.MockValidator + repository articles.MockArticlesRepository + userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + validator validator.MockValidator hashtag = "test-hashtag" expectedErr = errors.New("test error") @@ -102,10 +113,14 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &request).Once().Return(nil) defer validator.AssertExpectations(t) - repository.On("CountPublishedByHashtags", []string{hashtag}).Once().Return(uint(0), expectedErr) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + + repository.On("CountPublishedByHashtags", []string{hashtag}, "EN").Once().Return(uint(0), expectedErr) defer repository.AssertExpectations(t) - usecase := NewUseCase(&repository, &userRepository, &validator) + usecase := NewUseCase(&repository, &userRepository, &languageResolver, &validator) response, err := usecase.Execute(&request) repository.AssertNotCalled(t, "GetPublishedByHashtags") @@ -119,9 +134,10 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - repository articles.MockArticlesRepository - userRepository users.MockUsersRepository - validator validator.MockValidator + repository articles.MockArticlesRepository + userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + validator validator.MockValidator hashtag = "test-hashtag" expectedErr = errors.New("test error") @@ -131,11 +147,15 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &request).Once().Return(nil) defer validator.AssertExpectations(t) - repository.On("CountPublishedByHashtags", []string{hashtag}).Once().Return(uint(5), nil) - repository.On("GetPublishedByHashtags", []string{hashtag}, uint(0), uint(10)).Once().Return(nil, expectedErr) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + + repository.On("CountPublishedByHashtags", []string{hashtag}, "EN").Once().Return(uint(5), nil) + repository.On("GetPublishedByHashtags", []string{hashtag}, "EN", uint(0), uint(10)).Once().Return(nil, expectedErr) defer repository.AssertExpectations(t) - usecase := NewUseCase(&repository, &userRepository, &validator) + usecase := NewUseCase(&repository, &userRepository, &languageResolver, &validator) response, err := usecase.Execute(&request) userRepository.AssertNotCalled(t, "GetByUUIDs") @@ -148,9 +168,10 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - repository articles.MockArticlesRepository - userRepository users.MockUsersRepository - validator validator.MockValidator + repository articles.MockArticlesRepository + userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + validator validator.MockValidator hashtag = "test-hashtag" expectedErr = errors.New("test error") @@ -163,14 +184,18 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &request).Once().Return(nil) defer validator.AssertExpectations(t) - repository.On("CountPublishedByHashtags", []string{hashtag}).Once().Return(uint(1), nil) - repository.On("GetPublishedByHashtags", []string{hashtag}, uint(0), uint(10)).Once().Return(a, nil) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + + repository.On("CountPublishedByHashtags", []string{hashtag}, "EN").Once().Return(uint(1), nil) + repository.On("GetPublishedByHashtags", []string{hashtag}, "EN", uint(0), uint(10)).Once().Return(a, nil) defer repository.AssertExpectations(t) userRepository.On("GetByUUIDs", []string{"author-uuid-1"}).Once().Return(nil, expectedErr) defer userRepository.AssertExpectations(t) - usecase := NewUseCase(&repository, &userRepository, &validator) + usecase := NewUseCase(&repository, &userRepository, &languageResolver, &validator) response, err := usecase.Execute(&request) assert.ErrorIs(t, err, expectedErr) diff --git a/application/auth/authentication.go b/application/auth/authentication.go index 7fe0c9d0..2df3796d 100644 --- a/application/auth/authentication.go +++ b/application/auth/authentication.go @@ -48,8 +48,8 @@ func NewTokenGenerator(jwt *jwt.JWT, roleRepository role.Repository) *AuthTokenG } } -func (t *AuthTokenGenerator) GenerateAccessToken(userUUID string) (string, error) { - roles, err := t.roleRepository.GetByUserUUID(userUUID) +func (t *AuthTokenGenerator) GenerateAccessToken(u *user.User) (string, error) { + roles, err := t.roleRepository.GetByUserUUID(u.UUID) if err != nil { return "", err } @@ -79,13 +79,14 @@ func (t *AuthTokenGenerator) GenerateAccessToken(userUUID string) (string, error } b := jwt.NewClaimsBuilder() - b.SetSubject(userUUID) + b.SetSubject(u.UUID) b.SetNotBefore(time.Now()) b.SetExpirationTime(time.Now().Add(AccessTokenExpirationTime)) b.SetIssuedAt(time.Now()) b.SetAudience([]string{AccessToken}) b.SetRoles(roleNames) b.SetPermissions(permissionNames) + b.SetLanguage(u.LanguageCode) return t.jwt.Generate(b.Build()) } diff --git a/application/auth/authentication_test.go b/application/auth/authentication_test.go index f797418c..5c8b924b 100644 --- a/application/auth/authentication_test.go +++ b/application/auth/authentication_test.go @@ -76,7 +76,9 @@ func TestGenerateAccessToken(t *testing.T) { authTokenGenerator := NewTokenGenerator(j, &roleRepository) - accessToken, err := authTokenGenerator.GenerateAccessToken(userUUID) + u := user.User{UUID: userUUID, LanguageCode: "EN"} + + accessToken, err := authTokenGenerator.GenerateAccessToken(&u) assert.NoError(t, err) assert.NotEmpty(t, accessToken) @@ -90,6 +92,30 @@ func TestGenerateAccessToken(t *testing.T) { assert.ElementsMatch(t, []string{"role-1", "role-2", "role-3"}, claimsMap["roles"]) assert.ElementsMatch(t, []string{"permission-1", "permission-2", "permission-5"}, claimsMap["permissions"]) + assert.Equal(t, u.LanguageCode, claimsMap["lang"]) + }) + + t.Run("generating access token with an empty language code sets an empty lang claim", func(t *testing.T) { + t.Parallel() + + var roleRepository roles.MockRolesRepository + + roleRepository.On("GetByUserUUID", userUUID).Once().Return(rl, nil) + defer roleRepository.AssertExpectations(t) + + authTokenGenerator := NewTokenGenerator(j, &roleRepository) + + accessToken, err := authTokenGenerator.GenerateAccessToken(&user.User{UUID: userUUID}) + assert.NoError(t, err) + assert.NotEmpty(t, accessToken) + + claims, err := j.Verify(accessToken) + assert.NoError(t, err) + + claimsMap, ok := claims.(jwtv5.MapClaims) + assert.True(t, ok) + + assert.Equal(t, "", claimsMap["lang"]) }) t.Run("generating access token fails", func(t *testing.T) { @@ -104,7 +130,7 @@ func TestGenerateAccessToken(t *testing.T) { authTokenGenerator := NewTokenGenerator(j, &roleRepository) - accessToken, err := authTokenGenerator.GenerateAccessToken(userUUID) + accessToken, err := authTokenGenerator.GenerateAccessToken(&user.User{UUID: userUUID}) assert.ErrorIs(t, err, expectedErr) assert.Empty(t, accessToken) }) diff --git a/application/auth/login/usecase.go b/application/auth/login/usecase.go index 789473c5..fc549663 100644 --- a/application/auth/login/usecase.go +++ b/application/auth/login/usecase.go @@ -58,7 +58,7 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { }, nil } - accessToken, err := uc.authTokenGenerator.GenerateAccessToken(u.UUID) + accessToken, err := uc.authTokenGenerator.GenerateAccessToken(&u) if err != nil { return nil, err } diff --git a/application/auth/login/usecase_test.go b/application/auth/login/usecase_test.go index f93c2828..27339850 100644 --- a/application/auth/login/usecase_test.go +++ b/application/auth/login/usecase_test.go @@ -5,6 +5,7 @@ import ( "reflect" "testing" + jwtv5 "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/assert" mock2 "github.com/stretchr/testify/mock" @@ -71,7 +72,8 @@ func TestUseCase_Execute(t *testing.T) { } u = user.User{ - UUID: request.Identity, + UUID: request.Identity, + LanguageCode: "EN", PasswordHash: password.Hash{ Value: []byte("hashed-value"), Salt: []byte("salt-value"), @@ -109,6 +111,10 @@ func TestUseCase_Execute(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "permission", audience[0]) + accessClaimsMap, ok := accessTokenClaims.(jwtv5.MapClaims) + assert.True(t, ok) + assert.Equal(t, u.LanguageCode, accessClaimsMap["lang"]) + refreshTokenClaims, err := j.Verify(response.RefreshToken) assert.NoError(t, err) assert.NotNil(t, accessTokenClaims) diff --git a/application/auth/refresh/usecase.go b/application/auth/refresh/usecase.go index 236b3245..551b323f 100644 --- a/application/auth/refresh/usecase.go +++ b/application/auth/refresh/usecase.go @@ -76,7 +76,7 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { return nil, err } - accessToken, err := uc.authTokenGenerator.GenerateAccessToken(u.UUID) + accessToken, err := uc.authTokenGenerator.GenerateAccessToken(&u) if err != nil { return nil, err } diff --git a/application/auth/verify/request.go b/application/auth/verify/request.go index 4752707e..e9a641f3 100644 --- a/application/auth/verify/request.go +++ b/application/auth/verify/request.go @@ -6,11 +6,12 @@ import ( ) type Request struct { - Token string `json:"token"` - Name string `json:"name"` - Username string `json:"username"` - Password string `json:"password"` - Repassword string `json:"repassword"` + Token string `json:"token"` + Name string `json:"name"` + Username string `json:"username"` + LanguageCode string `json:"language_code"` + Password string `json:"password"` + Repassword string `json:"repassword"` } var _ domain.Validatable = &Request{} @@ -32,6 +33,10 @@ func (r *Request) Validate() domain.ValidationErrors { validationErrors["username"] = "invalid_value" } + if len(r.LanguageCode) == 0 { + validationErrors["language_code"] = "required_field" + } + if len(r.Password) == 0 { validationErrors["password"] = "required_field" } diff --git a/application/auth/verify/request_test.go b/application/auth/verify/request_test.go index 9a80a974..3da33ed5 100644 --- a/application/auth/verify/request_test.go +++ b/application/auth/verify/request_test.go @@ -16,22 +16,24 @@ func TestRequest_Validate(t *testing.T) { { name: "valid request", request: Request{ - Token: "valid-token", - Name: "John Doe", - Username: "johndoe", - Password: "password123", - Repassword: "password123", + Token: "valid-token", + Name: "John Doe", + Username: "johndoe", + LanguageCode: "EN", + Password: "password123", + Repassword: "password123", }, want: domain.ValidationErrors{}, }, { name: "invalid request with empty token", request: Request{ - Token: "", - Name: "John Doe", - Username: "johndoe", - Password: "password123", - Repassword: "password123", + Token: "", + Name: "John Doe", + Username: "johndoe", + LanguageCode: "EN", + Password: "password123", + Repassword: "password123", }, want: domain.ValidationErrors{ "token": "required_field", @@ -40,11 +42,12 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with empty name", request: Request{ - Token: "valid-token", - Name: "", - Username: "johndoe", - Password: "password123", - Repassword: "password123", + Token: "valid-token", + Name: "", + Username: "johndoe", + LanguageCode: "EN", + Password: "password123", + Repassword: "password123", }, want: domain.ValidationErrors{ "name": "required_field", @@ -53,11 +56,12 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with empty username", request: Request{ - Token: "valid-token", - Name: "John Doe", - Username: "", - Password: "password123", - Repassword: "password123", + Token: "valid-token", + Name: "John Doe", + Username: "", + LanguageCode: "EN", + Password: "password123", + Repassword: "password123", }, want: domain.ValidationErrors{ "username": "required_field", @@ -66,11 +70,12 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with malformed username", request: Request{ - Token: "valid-token", - Name: "John Doe", - Username: "John Doe!", - Password: "password123", - Repassword: "password123", + Token: "valid-token", + Name: "John Doe", + Username: "John Doe!", + LanguageCode: "EN", + Password: "password123", + Repassword: "password123", }, want: domain.ValidationErrors{ "username": "invalid_value", @@ -79,24 +84,40 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with username having no alphanumerics", request: Request{ - Token: "valid-token", - Name: "John Doe", - Username: "...", - Password: "password123", - Repassword: "password123", + Token: "valid-token", + Name: "John Doe", + Username: "...", + LanguageCode: "EN", + Password: "password123", + Repassword: "password123", }, want: domain.ValidationErrors{ "username": "invalid_value", }, }, + { + name: "invalid request with empty language", + request: Request{ + Token: "valid-token", + Name: "John Doe", + Username: "johndoe", + LanguageCode: "", + Password: "password123", + Repassword: "password123", + }, + want: domain.ValidationErrors{ + "language_code": "required_field", + }, + }, { name: "invalid request with empty password", request: Request{ - Token: "valid-token", - Name: "John Doe", - Username: "johndoe", - Password: "", - Repassword: "password123", + Token: "valid-token", + Name: "John Doe", + Username: "johndoe", + LanguageCode: "EN", + Password: "", + Repassword: "password123", }, want: domain.ValidationErrors{ "password": "required_field", @@ -106,11 +127,12 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with empty repassword", request: Request{ - Token: "valid-token", - Name: "John Doe", - Username: "johndoe", - Password: "password123", - Repassword: "", + Token: "valid-token", + Name: "John Doe", + Username: "johndoe", + LanguageCode: "EN", + Password: "password123", + Repassword: "", }, want: domain.ValidationErrors{ "repassword": "repassword", @@ -119,11 +141,12 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with mismatched passwords", request: Request{ - Token: "valid-token", - Name: "John Doe", - Username: "johndoe", - Password: "password123", - Repassword: "password456", + Token: "valid-token", + Name: "John Doe", + Username: "johndoe", + LanguageCode: "EN", + Password: "password123", + Repassword: "password456", }, want: domain.ValidationErrors{ "repassword": "repassword", @@ -132,11 +155,12 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with different length passwords", request: Request{ - Token: "valid-token", - Name: "John Doe", - Username: "johndoe", - Password: "password123", - Repassword: "password1234", + Token: "valid-token", + Name: "John Doe", + Username: "johndoe", + LanguageCode: "EN", + Password: "password123", + Repassword: "password1234", }, want: domain.ValidationErrors{ "repassword": "repassword", @@ -145,18 +169,20 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with multiple errors", request: Request{ - Token: "", - Name: "", - Username: "", - Password: "", - Repassword: "", + Token: "", + Name: "", + Username: "", + LanguageCode: "", + Password: "", + Repassword: "", }, want: domain.ValidationErrors{ - "token": "required_field", - "name": "required_field", - "username": "required_field", - "password": "required_field", - "repassword": "repassword", + "token": "required_field", + "name": "required_field", + "username": "required_field", + "language_code": "required_field", + "password": "required_field", + "repassword": "repassword", }, }, } diff --git a/application/auth/verify/usecase.go b/application/auth/verify/usecase.go index 30c55346..6870c1f0 100644 --- a/application/auth/verify/usecase.go +++ b/application/auth/verify/usecase.go @@ -6,6 +6,7 @@ import ( "errors" "github.com/khanzadimahdi/testproject/application/auth" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/config" "github.com/khanzadimahdi/testproject/domain/password" @@ -19,6 +20,7 @@ type UseCase struct { userRepository user.Repository roleRepository role.Repository configRepository config.Repository + languageResolver resolver.Resolver hasher password.Hasher jwt *jwt.JWT translator translator.Translator @@ -29,6 +31,7 @@ func NewUseCase( userRepository user.Repository, roleRepository role.Repository, configRepository config.Repository, + languageResolver resolver.Resolver, hasher password.Hasher, JWT *jwt.JWT, translator translator.Translator, @@ -38,6 +41,7 @@ func NewUseCase( userRepository: userRepository, roleRepository: roleRepository, configRepository: configRepository, + languageResolver: languageResolver, hasher: hasher, jwt: JWT, translator: translator, @@ -108,10 +112,19 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { return nil, err } + if !uc.languageResolver.Verify(request.LanguageCode) { + return &Response{ + ValidationErrors: domain.ValidationErrors{ + "language_code": "invalid_value", + }, + }, nil + } + u := user.User{ - Name: request.Name, - Username: request.Username, - Email: identity, + Name: request.Name, + Username: request.Username, + Email: identity, + LanguageCode: request.LanguageCode, PasswordHash: password.Hash{ Value: uc.hasher.Hash([]byte(request.Password), salt), Salt: salt, diff --git a/application/auth/verify/usecase_test.go b/application/auth/verify/usecase_test.go index 4d86aa79..a3cb8443 100644 --- a/application/auth/verify/usecase_test.go +++ b/application/auth/verify/usecase_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/khanzadimahdi/testproject/application/auth" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/config" "github.com/khanzadimahdi/testproject/domain/role" @@ -43,6 +44,7 @@ func TestUseCase_Execute(t *testing.T) { userRepository users.MockUsersRepository roleRepository roles.MockRolesRepository configRepository configRepo.MockConfigRepository + languageResolver resolver.MockResolver hasher crypto.MockCrypto validator validator.MockValidator translator translator.TranslatorMock @@ -52,11 +54,12 @@ func TestUseCase_Execute(t *testing.T) { } r = Request{ - Token: generateToken(t, j, u, time.Now().Add(10*time.Second), auth.RegistrationToken), - Name: "test name", - Username: "test-user-name", - Password: "test-password", - Repassword: "test-password", + Token: generateToken(t, j, u, time.Now().Add(10*time.Second), auth.RegistrationToken), + Name: "test name", + Username: "test-user-name", + LanguageCode: "EN", + Password: "test-password", + Repassword: "test-password", } roles = []role.Role{ @@ -91,6 +94,9 @@ func TestUseCase_Execute(t *testing.T) { userRepository.On("Save", mock.Anything).Once().Return(u.UUID, nil) defer userRepository.AssertExpectations(t) + languageResolver.On("Verify", r.LanguageCode).Once().Return(true) + defer languageResolver.AssertExpectations(t) + hasher.On("Hash", []byte(r.Password), mock.AnythingOfType("[]uint8")).Once().Return([]byte("hashed-password"), nil) defer hasher.AssertExpectations(t) @@ -102,7 +108,7 @@ func TestUseCase_Execute(t *testing.T) { roleRepository.On("Save", &expectedRoles[1]).Once().Return(expectedRoles[1].UUID, nil) defer roleRepository.AssertExpectations(t) - response, err := NewUseCase(&userRepository, &roleRepository, &configRepository, &hasher, j, &translator, &validator).Execute(&r) + response, err := NewUseCase(&userRepository, &roleRepository, &configRepository, &languageResolver, &hasher, j, &translator, &validator).Execute(&r) translator.AssertNotCalled(t, "Translate") @@ -117,6 +123,7 @@ func TestUseCase_Execute(t *testing.T) { userRepository users.MockUsersRepository roleRepository roles.MockRolesRepository configRepository configRepo.MockConfigRepository + languageResolver resolver.MockResolver hasher crypto.MockCrypto validator validator.MockValidator translator translator.TranslatorMock @@ -137,7 +144,7 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(expectedResponse.ValidationErrors) defer validator.AssertExpectations(t) - response, err := NewUseCase(&userRepository, &roleRepository, &configRepository, &hasher, j, &translator, &validator).Execute(&r) + response, err := NewUseCase(&userRepository, &roleRepository, &configRepository, &languageResolver, &hasher, j, &translator, &validator).Execute(&r) translator.AssertNotCalled(t, "Translate") userRepository.AssertNotCalled(t, "GetOneByIdentity") @@ -159,6 +166,7 @@ func TestUseCase_Execute(t *testing.T) { userRepository users.MockUsersRepository roleRepository roles.MockRolesRepository configRepository configRepo.MockConfigRepository + languageResolver resolver.MockResolver hasher crypto.MockCrypto validator validator.MockValidator translator translator.TranslatorMock @@ -168,11 +176,12 @@ func TestUseCase_Execute(t *testing.T) { } r = Request{ - Token: generateToken(t, j, u, time.Now().Add(-10*time.Second), auth.RegistrationToken), - Name: "test name", - Username: "test-user-name", - Password: "test-password", - Repassword: "test-password", + Token: generateToken(t, j, u, time.Now().Add(-10*time.Second), auth.RegistrationToken), + Name: "test name", + Username: "test-user-name", + LanguageCode: "EN", + Password: "test-password", + Repassword: "test-password", } expectedResponse = Response{ @@ -185,7 +194,7 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(nil) defer validator.AssertExpectations(t) - response, err := NewUseCase(&userRepository, &roleRepository, &configRepository, &hasher, j, &translator, &validator).Execute(&r) + response, err := NewUseCase(&userRepository, &roleRepository, &configRepository, &languageResolver, &hasher, j, &translator, &validator).Execute(&r) translator.AssertNotCalled(t, "Translate") userRepository.AssertNotCalled(t, "GetOneByIdentity") @@ -207,6 +216,7 @@ func TestUseCase_Execute(t *testing.T) { userRepository users.MockUsersRepository roleRepository roles.MockRolesRepository configRepository configRepo.MockConfigRepository + languageResolver resolver.MockResolver hasher crypto.MockCrypto validator validator.MockValidator translator translator.TranslatorMock @@ -217,11 +227,12 @@ func TestUseCase_Execute(t *testing.T) { } r = Request{ - Token: generateToken(t, j, u, time.Now().Add(10*time.Second), auth.RegistrationToken), - Name: "test name", - Username: "test-user-name", - Password: "test-password", - Repassword: "test-password", + Token: generateToken(t, j, u, time.Now().Add(10*time.Second), auth.RegistrationToken), + Name: "test name", + Username: "test-user-name", + LanguageCode: "EN", + Password: "test-password", + Repassword: "test-password", } expectedResponse = Response{ @@ -244,7 +255,7 @@ func TestUseCase_Execute(t *testing.T) { userRepository.On("GetOneByIdentity", u.UUID).Once().Return(u, nil) defer userRepository.AssertExpectations(t) - response, err := NewUseCase(&userRepository, &roleRepository, &configRepository, &hasher, j, &translator, &validator).Execute(&r) + response, err := NewUseCase(&userRepository, &roleRepository, &configRepository, &languageResolver, &hasher, j, &translator, &validator).Execute(&r) userRepository.AssertNotCalled(t, "Save") hasher.AssertNotCalled(t, "Hash") @@ -264,6 +275,7 @@ func TestUseCase_Execute(t *testing.T) { userRepository users.MockUsersRepository roleRepository roles.MockRolesRepository configRepository configRepo.MockConfigRepository + languageResolver resolver.MockResolver hasher crypto.MockCrypto validator validator.MockValidator translator translator.TranslatorMock @@ -274,11 +286,12 @@ func TestUseCase_Execute(t *testing.T) { } r = Request{ - Token: generateToken(t, j, u, time.Now().Add(10*time.Second), auth.RegistrationToken), - Name: "test name", - Username: "test-user-name", - Password: "test-password", - Repassword: "test-password", + Token: generateToken(t, j, u, time.Now().Add(10*time.Second), auth.RegistrationToken), + Name: "test name", + Username: "test-user-name", + LanguageCode: "EN", + Password: "test-password", + Repassword: "test-password", } expectedResponse = Response{ @@ -302,7 +315,7 @@ func TestUseCase_Execute(t *testing.T) { userRepository.On("GetOneByIdentity", r.Username).Once().Return(u, nil) defer userRepository.AssertExpectations(t) - response, err := NewUseCase(&userRepository, &roleRepository, &configRepository, &hasher, j, &translator, &validator).Execute(&r) + response, err := NewUseCase(&userRepository, &roleRepository, &configRepository, &languageResolver, &hasher, j, &translator, &validator).Execute(&r) userRepository.AssertNotCalled(t, "Save") hasher.AssertNotCalled(t, "Hash") @@ -322,6 +335,7 @@ func TestUseCase_Execute(t *testing.T) { userRepository users.MockUsersRepository roleRepository roles.MockRolesRepository configRepository configRepo.MockConfigRepository + languageResolver resolver.MockResolver hasher crypto.MockCrypto validator validator.MockValidator translator translator.TranslatorMock @@ -332,11 +346,12 @@ func TestUseCase_Execute(t *testing.T) { } r = Request{ - Token: generateToken(t, j, u, time.Now().Add(10*time.Second), auth.RegistrationToken), - Name: "test name", - Username: "test-user-name", - Password: "test-password", - Repassword: "test-password", + Token: generateToken(t, j, u, time.Now().Add(10*time.Second), auth.RegistrationToken), + Name: "test name", + Username: "test-user-name", + LanguageCode: "EN", + Password: "test-password", + Repassword: "test-password", } expectedErr = errors.New("some error") @@ -350,10 +365,13 @@ func TestUseCase_Execute(t *testing.T) { userRepository.On("Save", mock.Anything).Once().Return("", expectedErr) defer userRepository.AssertExpectations(t) + languageResolver.On("Verify", r.LanguageCode).Once().Return(true) + defer languageResolver.AssertExpectations(t) + hasher.On("Hash", []byte(r.Password), mock.AnythingOfType("[]uint8")).Once().Return([]byte("hashed-password"), nil) defer hasher.AssertExpectations(t) - response, err := NewUseCase(&userRepository, &roleRepository, &configRepository, &hasher, j, &translator, &validator).Execute(&r) + response, err := NewUseCase(&userRepository, &roleRepository, &configRepository, &languageResolver, &hasher, j, &translator, &validator).Execute(&r) configRepository.AssertNotCalled(t, "GetLatestRevision") roleRepository.AssertNotCalled(t, "GetByUUIDs") diff --git a/application/dashboard/article/createArticle/request.go b/application/dashboard/article/createArticle/request.go index 4253c60e..82fb36ec 100644 --- a/application/dashboard/article/createArticle/request.go +++ b/application/dashboard/article/createArticle/request.go @@ -7,14 +7,16 @@ import ( ) type Request struct { - Cover string `json:"cover"` - Title string `json:"title"` - Video string `json:"video"` - Excerpt string `json:"excerpt"` - Body string `json:"body"` - PublishedAt time.Time `json:"published_at"` - AuthorUUID string `json:"-"` - Tags []string `json:"tags"` + Cover string `json:"cover"` + Title string `json:"title"` + Video string `json:"video"` + Excerpt string `json:"excerpt"` + Body string `json:"body"` + PublishedAt time.Time `json:"published_at"` + AuthorUUID string `json:"-"` + Tags []string `json:"tags"` + LanguageCode string `json:"language_code"` + CorrelationUUID string `json:"correlation_uuid"` } var _ domain.Validatable = &Request{} @@ -35,7 +37,11 @@ func (r *Request) Validate() domain.ValidationErrors { } if len(r.AuthorUUID) == 0 { - validationErrors["author"] = "required_field" + validationErrors["author_uuid"] = "required_field" + } + + if len(r.LanguageCode) == 0 { + validationErrors["language_code"] = "required_field" } return validationErrors diff --git a/application/dashboard/article/createArticle/request_test.go b/application/dashboard/article/createArticle/request_test.go index 839a75fc..2b1edecf 100644 --- a/application/dashboard/article/createArticle/request_test.go +++ b/application/dashboard/article/createArticle/request_test.go @@ -1,10 +1,11 @@ package createarticle import ( - "github.com/stretchr/testify/assert" "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/khanzadimahdi/testproject/domain" ) @@ -17,36 +18,39 @@ func TestRequest_Validate(t *testing.T) { { name: "valid request", request: Request{ - Cover: "cover.jpg", - Title: "Test Article", - Video: "video.mp4", - Excerpt: "This is an excerpt", - Body: "This is the body", - PublishedAt: time.Now(), - AuthorUUID: "author-uuid-123", - Tags: []string{"golang", "testing"}, + Cover: "cover.jpg", + Title: "Test Article", + Video: "video.mp4", + Excerpt: "This is an excerpt", + Body: "This is the body", + PublishedAt: time.Now(), + AuthorUUID: "author-uuid-123", + Tags: []string{"golang", "testing"}, + LanguageCode: "EN", }, want: domain.ValidationErrors{}, }, { name: "valid request with empty optional fields", request: Request{ - Title: "Test Article", - Excerpt: "This is an excerpt", - Body: "This is the body", - PublishedAt: time.Now(), - AuthorUUID: "author-uuid-123", + Title: "Test Article", + Excerpt: "This is an excerpt", + Body: "This is the body", + PublishedAt: time.Now(), + AuthorUUID: "author-uuid-123", + LanguageCode: "FA", }, want: domain.ValidationErrors{}, }, { name: "invalid request with empty title", request: Request{ - Title: "", - Excerpt: "This is an excerpt", - Body: "This is the body", - PublishedAt: time.Now(), - AuthorUUID: "author-uuid-123", + Title: "", + Excerpt: "This is an excerpt", + Body: "This is the body", + PublishedAt: time.Now(), + AuthorUUID: "author-uuid-123", + LanguageCode: "EN", }, want: domain.ValidationErrors{ "title": "required_field", @@ -55,11 +59,12 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with empty excerpt", request: Request{ - Title: "Test Article", - Excerpt: "", - Body: "This is the body", - PublishedAt: time.Now(), - AuthorUUID: "author-uuid-123", + Title: "Test Article", + Excerpt: "", + Body: "This is the body", + PublishedAt: time.Now(), + AuthorUUID: "author-uuid-123", + LanguageCode: "EN", }, want: domain.ValidationErrors{ "excerpt": "required_field", @@ -68,11 +73,12 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with empty body", request: Request{ - Title: "Test Article", - Excerpt: "This is an excerpt", - Body: "", - PublishedAt: time.Now(), - AuthorUUID: "author-uuid-123", + Title: "Test Article", + Excerpt: "This is an excerpt", + Body: "", + PublishedAt: time.Now(), + AuthorUUID: "author-uuid-123", + LanguageCode: "EN", }, want: domain.ValidationErrors{ "body": "required_field", @@ -81,30 +87,47 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with empty author uuid", request: Request{ - Title: "Test Article", - Excerpt: "This is an excerpt", - Body: "This is the body", - PublishedAt: time.Now(), - AuthorUUID: "", + Title: "Test Article", + Excerpt: "This is an excerpt", + Body: "This is the body", + PublishedAt: time.Now(), + AuthorUUID: "", + LanguageCode: "EN", + }, + want: domain.ValidationErrors{ + "author_uuid": "required_field", + }, + }, + { + name: "invalid request with empty language", + request: Request{ + Title: "Test Article", + Excerpt: "This is an excerpt", + Body: "This is the body", + PublishedAt: time.Now(), + AuthorUUID: "author-uuid-123", + LanguageCode: "", }, want: domain.ValidationErrors{ - "author": "required_field", + "language_code": "required_field", }, }, { name: "invalid request with multiple errors", request: Request{ - Title: "", - Excerpt: "", - Body: "", - PublishedAt: time.Now(), - AuthorUUID: "", + Title: "", + Excerpt: "", + Body: "", + PublishedAt: time.Now(), + AuthorUUID: "", + LanguageCode: "", }, want: domain.ValidationErrors{ - "title": "required_field", - "excerpt": "required_field", - "body": "required_field", - "author": "required_field", + "title": "required_field", + "excerpt": "required_field", + "body": "required_field", + "author_uuid": "required_field", + "language_code": "required_field", }, }, } diff --git a/application/dashboard/article/createArticle/usecase.go b/application/dashboard/article/createArticle/usecase.go index d3c688d9..cc9302d2 100644 --- a/application/dashboard/article/createArticle/usecase.go +++ b/application/dashboard/article/createArticle/usecase.go @@ -3,20 +3,24 @@ package createarticle import ( "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" ) type UseCase struct { - articleRepository article.Repository - validator domain.Validator + articleRepository article.Repository + languageRepository language.Repository + validator domain.Validator } func NewUseCase( articleRepository article.Repository, + languageRepository language.Repository, validator domain.Validator, ) *UseCase { return &UseCase{ - articleRepository: articleRepository, - validator: validator, + articleRepository: articleRepository, + languageRepository: languageRepository, + validator: validator, } } @@ -27,15 +31,40 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { }, nil } + if !uc.languageRepository.Exists(request.LanguageCode) { + return &Response{ + ValidationErrors: domain.ValidationErrors{ + "language_code": "invalid_value", + }, + }, nil + } + + if len(request.CorrelationUUID) > 0 { + exist, err := uc.articleRepository.CorrelationExist(request.CorrelationUUID) + if err != nil { + return nil, err + } + + if !exist { + return &Response{ + ValidationErrors: domain.ValidationErrors{ + "correlation_uuid": "invalid_value", + }, + }, nil + } + } + a := article.Article{ - Cover: request.Cover, - Video: request.Video, - Title: request.Title, - Excerpt: request.Excerpt, - Body: request.Body, - PublishedAt: request.PublishedAt, - AuthorUUID: request.AuthorUUID, - Tags: request.Tags, + Cover: request.Cover, + Video: request.Video, + Title: request.Title, + Excerpt: request.Excerpt, + Body: request.Body, + PublishedAt: request.PublishedAt, + AuthorUUID: request.AuthorUUID, + Tags: request.Tags, + LanguageCode: request.LanguageCode, + CorrelationUUID: request.CorrelationUUID, } uuid, err := uc.articleRepository.Save(&a) diff --git a/application/dashboard/article/createArticle/usecase_test.go b/application/dashboard/article/createArticle/usecase_test.go index aadf133b..b99855dd 100644 --- a/application/dashboard/article/createArticle/usecase_test.go +++ b/application/dashboard/article/createArticle/usecase_test.go @@ -9,6 +9,7 @@ import ( "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" "github.com/khanzadimahdi/testproject/infrastructure/validator" ) @@ -19,25 +20,28 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - articleRepository articles.MockArticlesRepository - validator validator.MockValidator + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + validator validator.MockValidator r = Request{ - Title: "test title", - Excerpt: "test excerpt", - Body: "test body", - AuthorUUID: "test-author-uuid", - Tags: []string{"tag1", "tag2"}, + Title: "test title", + Excerpt: "test excerpt", + Body: "test body", + AuthorUUID: "test-author-uuid", + Tags: []string{"tag1", "tag2"}, + LanguageCode: "EN", } a = article.Article{ - Cover: r.Cover, - Video: r.Video, - Title: r.Title, - Excerpt: r.Excerpt, - Body: r.Body, - PublishedAt: r.PublishedAt, - AuthorUUID: r.AuthorUUID, - Tags: r.Tags, + Cover: r.Cover, + Video: r.Video, + Title: r.Title, + Excerpt: r.Excerpt, + Body: r.Body, + PublishedAt: r.PublishedAt, + AuthorUUID: r.AuthorUUID, + Tags: r.Tags, + LanguageCode: r.LanguageCode, } u = "article-uuid" @@ -47,10 +51,13 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(nil) defer validator.AssertExpectations(t) + languageRepository.On("Exists", "EN").Once().Return(true) + defer languageRepository.AssertExpectations(t) + articleRepository.On("Save", &a).Once().Return(u, nil) defer articleRepository.AssertExpectations(t) - response, err := NewUseCase(&articleRepository, &validator).Execute(&r) + response, err := NewUseCase(&articleRepository, &languageRepository, &validator).Execute(&r) assert.NoError(t, err) assert.Equal(t, &expectedResponse, response) @@ -60,16 +67,18 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - articleRepository articles.MockArticlesRepository - validator validator.MockValidator + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + validator validator.MockValidator r = Request{} expectedResponse = Response{ ValidationErrors: domain.ValidationErrors{ - "title": "title is required", - "excerpt": "excerpt is required", - "body": "body is required", - "author": "author is required", + "title": "title is required", + "excerpt": "excerpt is required", + "body": "body is required", + "author": "author is required", + "language_code": "language is required", }, } ) @@ -77,37 +86,73 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(expectedResponse.ValidationErrors) defer validator.AssertExpectations(t) - response, err := NewUseCase(&articleRepository, &validator).Execute(&r) + response, err := NewUseCase(&articleRepository, &languageRepository, &validator).Execute(&r) articleRepository.AssertNotCalled(t, "Save") + languageRepository.AssertNotCalled(t, "Exists") assert.NoError(t, err) assert.Equal(t, &expectedResponse, response) }) + t.Run("language is invalid", func(t *testing.T) { + t.Parallel() + + var ( + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + validator validator.MockValidator + + r = Request{ + Title: "test title", + Excerpt: "test excerpt", + Body: "test body", + AuthorUUID: "test-author-uuid", + LanguageCode: "DE", + } + ) + + validator.On("Validate", &r).Once().Return(nil) + defer validator.AssertExpectations(t) + + languageRepository.On("Exists", "DE").Once().Return(false) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&articleRepository, &languageRepository, &validator).Execute(&r) + + articleRepository.AssertNotCalled(t, "Save") + + assert.NoError(t, err) + assert.NotNil(t, response) + assert.Equal(t, "invalid_value", response.ValidationErrors["language_code"]) + }) + t.Run("saving the article fails", func(t *testing.T) { t.Parallel() var ( - articleRepository articles.MockArticlesRepository - validator validator.MockValidator + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + validator validator.MockValidator r = Request{ - Title: "test title", - Excerpt: "test excerpt", - Body: "test body", - AuthorUUID: "test-author-uuid", - Tags: []string{"tag1", "tag2"}, + Title: "test title", + Excerpt: "test excerpt", + Body: "test body", + AuthorUUID: "test-author-uuid", + Tags: []string{"tag1", "tag2"}, + LanguageCode: "EN", } a = article.Article{ - Cover: r.Cover, - Video: r.Video, - Title: r.Title, - Excerpt: r.Excerpt, - Body: r.Body, - PublishedAt: r.PublishedAt, - AuthorUUID: r.AuthorUUID, - Tags: r.Tags, + Cover: r.Cover, + Video: r.Video, + Title: r.Title, + Excerpt: r.Excerpt, + Body: r.Body, + PublishedAt: r.PublishedAt, + AuthorUUID: r.AuthorUUID, + Tags: r.Tags, + LanguageCode: r.LanguageCode, } expectedErr = errors.New("error happened") @@ -116,10 +161,138 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(nil) defer validator.AssertExpectations(t) + languageRepository.On("Exists", "EN").Once().Return(true) + defer languageRepository.AssertExpectations(t) + articleRepository.On("Save", &a).Once().Return("", expectedErr) defer articleRepository.AssertExpectations(t) - response, err := NewUseCase(&articleRepository, &validator).Execute(&r) + response, err := NewUseCase(&articleRepository, &languageRepository, &validator).Execute(&r) + + assert.ErrorIs(t, err, expectedErr) + assert.Nil(t, response) + }) + + t.Run("creates a translation reusing an existing correlation uuid", func(t *testing.T) { + t.Parallel() + + var ( + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator + + correlationUUID = "existing-correlation-uuid" + + r = Request{ + Title: "translation title", + Excerpt: "translation excerpt", + Body: "translation body", + AuthorUUID: "author-uuid", + LanguageCode: "FA", + CorrelationUUID: correlationUUID, + } + a = article.Article{ + Cover: r.Cover, + Video: r.Video, + Title: r.Title, + Excerpt: r.Excerpt, + Body: r.Body, + PublishedAt: r.PublishedAt, + AuthorUUID: r.AuthorUUID, + Tags: r.Tags, + LanguageCode: r.LanguageCode, + CorrelationUUID: r.CorrelationUUID, + } + ) + + requestValidator.On("Validate", &r).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageRepository.On("Exists", "FA").Once().Return(true) + defer languageRepository.AssertExpectations(t) + + articleRepository.On("CorrelationExist", correlationUUID).Once().Return(true, nil) + articleRepository.On("Save", &a).Once().Return("new-uuid", nil) + defer articleRepository.AssertExpectations(t) + + response, err := NewUseCase(&articleRepository, &languageRepository, &requestValidator).Execute(&r) + + assert.NoError(t, err) + assert.NotNil(t, response) + assert.Equal(t, "new-uuid", response.UUID) + }) + + t.Run("correlation uuid does not exist", func(t *testing.T) { + t.Parallel() + + var ( + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator + + correlationUUID = "ghost-correlation-uuid" + + r = Request{ + Title: "translation title", + Excerpt: "translation excerpt", + Body: "translation body", + AuthorUUID: "author-uuid", + LanguageCode: "FA", + CorrelationUUID: correlationUUID, + } + ) + + requestValidator.On("Validate", &r).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageRepository.On("Exists", "FA").Once().Return(true) + defer languageRepository.AssertExpectations(t) + + articleRepository.On("CorrelationExist", correlationUUID).Once().Return(false, nil) + defer articleRepository.AssertExpectations(t) + + response, err := NewUseCase(&articleRepository, &languageRepository, &requestValidator).Execute(&r) + + articleRepository.AssertNotCalled(t, "Save") + + assert.NoError(t, err) + assert.NotNil(t, response) + assert.Equal(t, "invalid_value", response.ValidationErrors["correlation_uuid"]) + }) + + t.Run("checking correlation existence fails", func(t *testing.T) { + t.Parallel() + + var ( + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator + + correlationUUID = "existing-correlation-uuid" + expectedErr = errors.New("error happened") + + r = Request{ + Title: "translation title", + Excerpt: "translation excerpt", + Body: "translation body", + AuthorUUID: "author-uuid", + LanguageCode: "FA", + CorrelationUUID: correlationUUID, + } + ) + + requestValidator.On("Validate", &r).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageRepository.On("Exists", "FA").Once().Return(true) + defer languageRepository.AssertExpectations(t) + + articleRepository.On("CorrelationExist", correlationUUID).Once().Return(false, expectedErr) + defer articleRepository.AssertExpectations(t) + + response, err := NewUseCase(&articleRepository, &languageRepository, &requestValidator).Execute(&r) + + articleRepository.AssertNotCalled(t, "Save") assert.ErrorIs(t, err, expectedErr) assert.Nil(t, response) diff --git a/application/dashboard/article/getArticle/response.go b/application/dashboard/article/getArticle/response.go index 7f39d11d..429537bc 100644 --- a/application/dashboard/article/getArticle/response.go +++ b/application/dashboard/article/getArticle/response.go @@ -8,16 +8,18 @@ import ( ) type Response struct { - UUID string `json:"uuid"` - Cover string `json:"cover"` - Video string `json:"video"` - Title string `json:"title"` - Excerpt string `json:"excerpt"` - Body string `json:"body"` - PublishedAt string `json:"published_at"` - Author author `json:"author"` - Tags []string `json:"tags"` - ViewCount uint `json:"view_count"` + UUID string `json:"uuid"` + Cover string `json:"cover"` + Video string `json:"video"` + Title string `json:"title"` + Excerpt string `json:"excerpt"` + Body string `json:"body"` + PublishedAt string `json:"published_at"` + Author author `json:"author"` + Tags []string `json:"tags"` + ViewCount uint `json:"view_count"` + LanguageCode string `json:"language_code"` + CorrelationID string `json:"translation_correlation_id,omitempty"` } type author struct { @@ -45,7 +47,9 @@ func NewResponse(a article.Article, u user.User) *Response { Avatar: u.Avatar, Username: u.Username, }, - Tags: tags, - ViewCount: a.ViewCount, + Tags: tags, + ViewCount: a.ViewCount, + LanguageCode: a.LanguageCode, + CorrelationID: a.CorrelationUUID, } } diff --git a/application/dashboard/article/updateArticle/request.go b/application/dashboard/article/updateArticle/request.go index b5680923..680d8513 100644 --- a/application/dashboard/article/updateArticle/request.go +++ b/application/dashboard/article/updateArticle/request.go @@ -7,15 +7,17 @@ import ( ) type Request struct { - UUID string `json:"uuid"` - Cover string `json:"cover"` - Video string `json:"video"` - Title string `json:"title"` - Excerpt string `json:"excerpt"` - Body string `json:"body"` - PublishedAt time.Time `json:"published_at"` - AuthorUUID string `json:"-"` - Tags []string `json:"tags"` + UUID string `json:"uuid"` + Cover string `json:"cover"` + Video string `json:"video"` + Title string `json:"title"` + Excerpt string `json:"excerpt"` + Body string `json:"body"` + PublishedAt time.Time `json:"published_at"` + AuthorUUID string `json:"-"` + Tags []string `json:"tags"` + LanguageCode string `json:"language_code"` + CorrelationUUID string `json:"correlation_uuid"` } var _ domain.Validatable = &Request{} @@ -39,5 +41,9 @@ func (r *Request) Validate() domain.ValidationErrors { validationErrors["author"] = "required_field" } + if len(r.LanguageCode) == 0 { + validationErrors["language_code"] = "required_field" + } + return validationErrors } diff --git a/application/dashboard/article/updateArticle/request_test.go b/application/dashboard/article/updateArticle/request_test.go index 422d7862..c1a65339 100644 --- a/application/dashboard/article/updateArticle/request_test.go +++ b/application/dashboard/article/updateArticle/request_test.go @@ -17,39 +17,42 @@ func TestRequest_Validate(t *testing.T) { { name: "valid request", request: Request{ - UUID: "article-uuid-123", - Cover: "cover.jpg", - Title: "Test Article", - Video: "video.mp4", - Excerpt: "This is an excerpt", - Body: "This is the body", - PublishedAt: time.Now(), - AuthorUUID: "author-uuid-123", - Tags: []string{"golang", "testing"}, + UUID: "article-uuid-123", + Cover: "cover.jpg", + Title: "Test Article", + Video: "video.mp4", + Excerpt: "This is an excerpt", + Body: "This is the body", + PublishedAt: time.Now(), + AuthorUUID: "author-uuid-123", + Tags: []string{"golang", "testing"}, + LanguageCode: "EN", }, want: domain.ValidationErrors{}, }, { name: "valid request with empty optional fields", request: Request{ - UUID: "article-uuid-123", - Title: "Test Article", - Excerpt: "This is an excerpt", - Body: "This is the body", - PublishedAt: time.Now(), - AuthorUUID: "author-uuid-123", + UUID: "article-uuid-123", + Title: "Test Article", + Excerpt: "This is an excerpt", + Body: "This is the body", + PublishedAt: time.Now(), + AuthorUUID: "author-uuid-123", + LanguageCode: "FA", }, want: domain.ValidationErrors{}, }, { name: "invalid request with empty title", request: Request{ - UUID: "article-uuid-123", - Title: "", - Excerpt: "This is an excerpt", - Body: "This is the body", - PublishedAt: time.Now(), - AuthorUUID: "author-uuid-123", + UUID: "article-uuid-123", + Title: "", + Excerpt: "This is an excerpt", + Body: "This is the body", + PublishedAt: time.Now(), + AuthorUUID: "author-uuid-123", + LanguageCode: "EN", }, want: domain.ValidationErrors{ "title": "required_field", @@ -58,12 +61,13 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with empty excerpt", request: Request{ - UUID: "article-uuid-123", - Title: "Test Article", - Excerpt: "", - Body: "This is the body", - PublishedAt: time.Now(), - AuthorUUID: "author-uuid-123", + UUID: "article-uuid-123", + Title: "Test Article", + Excerpt: "", + Body: "This is the body", + PublishedAt: time.Now(), + AuthorUUID: "author-uuid-123", + LanguageCode: "EN", }, want: domain.ValidationErrors{ "excerpt": "required_field", @@ -72,12 +76,13 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with empty body", request: Request{ - UUID: "article-uuid-123", - Title: "Test Article", - Excerpt: "This is an excerpt", - Body: "", - PublishedAt: time.Now(), - AuthorUUID: "author-uuid-123", + UUID: "article-uuid-123", + Title: "Test Article", + Excerpt: "This is an excerpt", + Body: "", + PublishedAt: time.Now(), + AuthorUUID: "author-uuid-123", + LanguageCode: "EN", }, want: domain.ValidationErrors{ "body": "required_field", @@ -86,32 +91,50 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with empty author uuid", request: Request{ - UUID: "article-uuid-123", - Title: "Test Article", - Excerpt: "This is an excerpt", - Body: "This is the body", - PublishedAt: time.Now(), - AuthorUUID: "", + UUID: "article-uuid-123", + Title: "Test Article", + Excerpt: "This is an excerpt", + Body: "This is the body", + PublishedAt: time.Now(), + AuthorUUID: "", + LanguageCode: "EN", }, want: domain.ValidationErrors{ "author": "required_field", }, }, + { + name: "invalid request with empty language", + request: Request{ + UUID: "article-uuid-123", + Title: "Test Article", + Excerpt: "This is an excerpt", + Body: "This is the body", + PublishedAt: time.Now(), + AuthorUUID: "author-uuid-123", + LanguageCode: "", + }, + want: domain.ValidationErrors{ + "language_code": "required_field", + }, + }, { name: "invalid request with multiple errors", request: Request{ - UUID: "article-uuid-123", - Title: "", - Excerpt: "", - Body: "", - PublishedAt: time.Now(), - AuthorUUID: "", + UUID: "article-uuid-123", + Title: "", + Excerpt: "", + Body: "", + PublishedAt: time.Now(), + AuthorUUID: "", + LanguageCode: "", }, want: domain.ValidationErrors{ - "title": "required_field", - "excerpt": "required_field", - "body": "required_field", - "author": "required_field", + "title": "required_field", + "excerpt": "required_field", + "body": "required_field", + "author": "required_field", + "language_code": "required_field", }, }, } diff --git a/application/dashboard/article/updateArticle/usecase.go b/application/dashboard/article/updateArticle/usecase.go index a61a933c..2f86fa80 100644 --- a/application/dashboard/article/updateArticle/usecase.go +++ b/application/dashboard/article/updateArticle/usecase.go @@ -3,20 +3,24 @@ package updatearticle import ( "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" ) type UseCase struct { - articleRepository article.Repository - validator domain.Validator + articleRepository article.Repository + languageRepository language.Repository + validator domain.Validator } func NewUseCase( articleRepository article.Repository, + languageRepository language.Repository, validator domain.Validator, ) *UseCase { return &UseCase{ - articleRepository: articleRepository, - validator: validator, + articleRepository: articleRepository, + languageRepository: languageRepository, + validator: validator, } } @@ -27,16 +31,47 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { }, nil } + if !uc.languageRepository.Exists(request.LanguageCode) { + return &Response{ + ValidationErrors: domain.ValidationErrors{ + "language_code": "invalid_value", + }, + }, nil + } + + existing, err := uc.articleRepository.GetOne(request.UUID) + if err != nil { + return nil, err + } + + if len(request.CorrelationUUID) > 0 { + exist, err := uc.articleRepository.CorrelationExist(request.CorrelationUUID) + if err != nil { + return nil, err + } + + if !exist { + return &Response{ + ValidationErrors: domain.ValidationErrors{ + "correlation_uuid": "invalid_value", + }, + }, nil + } + } + a := article.Article{ - UUID: request.UUID, - Cover: request.Cover, - Video: request.Video, - Title: request.Title, - Excerpt: request.Excerpt, - Body: request.Body, - PublishedAt: request.PublishedAt, - AuthorUUID: request.AuthorUUID, - Tags: request.Tags, + UUID: request.UUID, + Cover: request.Cover, + Video: request.Video, + Title: request.Title, + Excerpt: request.Excerpt, + Body: request.Body, + PublishedAt: request.PublishedAt, + AuthorUUID: request.AuthorUUID, + Tags: request.Tags, + LanguageCode: request.LanguageCode, + CorrelationUUID: request.CorrelationUUID, + ViewCount: existing.ViewCount, } if _, err := uc.articleRepository.Save(&a); err != nil { diff --git a/application/dashboard/article/updateArticle/usecase_test.go b/application/dashboard/article/updateArticle/usecase_test.go index 747dfeac..ce85674b 100644 --- a/application/dashboard/article/updateArticle/usecase_test.go +++ b/application/dashboard/article/updateArticle/usecase_test.go @@ -9,6 +9,7 @@ import ( "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" "github.com/khanzadimahdi/testproject/infrastructure/validator" ) @@ -19,37 +20,51 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - articleRepository articles.MockArticlesRepository - validator validator.MockValidator + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + validator validator.MockValidator r = Request{ - UUID: "test-article-uuid", - Title: "test title", - Excerpt: "test excerpt", - Body: "test body", - AuthorUUID: "test-author-uuid", - Tags: []string{"tag1", "tag2"}, + UUID: "test-article-uuid", + Title: "test title", + Excerpt: "test excerpt", + Body: "test body", + AuthorUUID: "test-author-uuid", + Tags: []string{"tag1", "tag2"}, + LanguageCode: "EN", + } + existing = article.Article{ + UUID: r.UUID, + Title: "old title", + LanguageCode: "EN", + ViewCount: 7, } a = article.Article{ - UUID: r.UUID, - Cover: r.Cover, - Video: r.Video, - Title: r.Title, - Excerpt: r.Excerpt, - Body: r.Body, - PublishedAt: r.PublishedAt, - AuthorUUID: r.AuthorUUID, - Tags: r.Tags, + UUID: r.UUID, + Cover: r.Cover, + Video: r.Video, + Title: r.Title, + Excerpt: r.Excerpt, + Body: r.Body, + PublishedAt: r.PublishedAt, + AuthorUUID: r.AuthorUUID, + Tags: r.Tags, + LanguageCode: r.LanguageCode, + ViewCount: existing.ViewCount, } ) validator.On("Validate", &r).Once().Return(nil) defer validator.AssertExpectations(t) + languageRepository.On("Exists", "EN").Once().Return(true) + defer languageRepository.AssertExpectations(t) + + articleRepository.On("GetOne", r.UUID).Once().Return(existing, nil) articleRepository.On("Save", &a).Once().Return(a.UUID, nil) defer articleRepository.AssertExpectations(t) - response, err := NewUseCase(&articleRepository, &validator).Execute(&r) + response, err := NewUseCase(&articleRepository, &languageRepository, &validator).Execute(&r) assert.NoError(t, err) assert.Nil(t, response) @@ -59,16 +74,18 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - articleRepository articles.MockArticlesRepository - validator validator.MockValidator + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + validator validator.MockValidator r = Request{} expectedResponse = Response{ ValidationErrors: domain.ValidationErrors{ - "title": "title is required", - "excerpt": "excerpt is required", - "body": "body is required", - "author": "author is required", + "title": "title is required", + "excerpt": "excerpt is required", + "body": "body is required", + "author": "author is required", + "language_code": "language is required", }, } ) @@ -76,39 +93,77 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(expectedResponse.ValidationErrors) defer validator.AssertExpectations(t) - response, err := NewUseCase(&articleRepository, &validator).Execute(&r) + response, err := NewUseCase(&articleRepository, &languageRepository, &validator).Execute(&r) articleRepository.AssertNotCalled(t, "Save") + articleRepository.AssertNotCalled(t, "GetOne") assert.NoError(t, err) assert.Equal(t, &expectedResponse, response) }) + t.Run("invalid language fails", func(t *testing.T) { + t.Parallel() + + var ( + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + validator validator.MockValidator + + r = Request{ + UUID: "test-article-uuid", + Title: "test title", + Excerpt: "test excerpt", + Body: "test body", + AuthorUUID: "test-author-uuid", + LanguageCode: "DE", + } + ) + + validator.On("Validate", &r).Once().Return(nil) + defer validator.AssertExpectations(t) + + languageRepository.On("Exists", "DE").Once().Return(false) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&articleRepository, &languageRepository, &validator).Execute(&r) + + articleRepository.AssertNotCalled(t, "Save") + + assert.NoError(t, err) + assert.NotNil(t, response) + assert.Equal(t, "invalid_value", response.ValidationErrors["language_code"]) + }) + t.Run("updating an article fails", func(t *testing.T) { t.Parallel() var ( - articleRepository articles.MockArticlesRepository - validator validator.MockValidator + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + validator validator.MockValidator r = Request{ - UUID: "test-article-uuid", - Title: "test title", - Excerpt: "test excerpt", - Body: "test body", - AuthorUUID: "test-author-uuid", - Tags: []string{"tag1", "tag2"}, + UUID: "test-article-uuid", + Title: "test title", + Excerpt: "test excerpt", + Body: "test body", + AuthorUUID: "test-author-uuid", + Tags: []string{"tag1", "tag2"}, + LanguageCode: "EN", } - a = article.Article{ - UUID: r.UUID, - Cover: r.Cover, - Video: r.Video, - Title: r.Title, - Excerpt: r.Excerpt, - Body: r.Body, - PublishedAt: r.PublishedAt, - AuthorUUID: r.AuthorUUID, - Tags: r.Tags, + existing = article.Article{UUID: r.UUID, LanguageCode: "EN"} + a = article.Article{ + UUID: r.UUID, + Cover: r.Cover, + Video: r.Video, + Title: r.Title, + Excerpt: r.Excerpt, + Body: r.Body, + PublishedAt: r.PublishedAt, + AuthorUUID: r.AuthorUUID, + Tags: r.Tags, + LanguageCode: r.LanguageCode, } expectedErr = errors.New("error happened") @@ -117,10 +172,14 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(nil) defer validator.AssertExpectations(t) + languageRepository.On("Exists", "EN").Once().Return(true) + defer languageRepository.AssertExpectations(t) + + articleRepository.On("GetOne", r.UUID).Once().Return(existing, nil) articleRepository.On("Save", &a).Once().Return("", expectedErr) defer articleRepository.AssertExpectations(t) - response, err := NewUseCase(&articleRepository, &validator).Execute(&r) + response, err := NewUseCase(&articleRepository, &languageRepository, &validator).Execute(&r) assert.ErrorIs(t, err, expectedErr) assert.Nil(t, response) diff --git a/application/dashboard/config/getConfig/response.go b/application/dashboard/config/getConfig/response.go index 149f914c..c4399450 100644 --- a/application/dashboard/config/getConfig/response.go +++ b/application/dashboard/config/getConfig/response.go @@ -1,6 +1,7 @@ package getConfig type Response struct { - Revision uint `json:"revision"` - UserDefaultRoles []string `json:"user_default_roles"` + Revision uint `json:"revision"` + UserDefaultRoles []string `json:"user_default_roles"` + DefaultLanguageCode string `json:"default_language_code"` } diff --git a/application/dashboard/config/getConfig/usecase.go b/application/dashboard/config/getConfig/usecase.go index 2a9533da..8f60352f 100644 --- a/application/dashboard/config/getConfig/usecase.go +++ b/application/dashboard/config/getConfig/usecase.go @@ -24,8 +24,9 @@ func (uc *UseCase) Execute() (*Response, error) { } response := Response{ - Revision: c.Revision, - UserDefaultRoles: c.UserDefaultRoleUUIDs, + Revision: c.Revision, + UserDefaultRoles: c.UserDefaultRoleUUIDs, + DefaultLanguageCode: c.DefaultLanguageCode, } if response.UserDefaultRoles == nil { diff --git a/application/dashboard/config/updateConfig/request.go b/application/dashboard/config/updateConfig/request.go index 62f5b29b..662ef5f8 100644 --- a/application/dashboard/config/updateConfig/request.go +++ b/application/dashboard/config/updateConfig/request.go @@ -3,7 +3,8 @@ package updateConfig import "github.com/khanzadimahdi/testproject/domain" type Request struct { - UserDefaultRoles []string `json:"user_default_roles"` + UserDefaultRoles []string `json:"user_default_roles"` + DefaultLanguageCode string `json:"default_language_code"` } var _ domain.Validatable = &Request{} @@ -15,5 +16,9 @@ func (r *Request) Validate() domain.ValidationErrors { validationErrors["user_default_roles"] = "required_field" } + if len(r.DefaultLanguageCode) == 0 { + validationErrors["default_language_code"] = "required_field" + } + return validationErrors } diff --git a/application/dashboard/config/updateConfig/request_test.go b/application/dashboard/config/updateConfig/request_test.go index 4b569eb7..83e78eaf 100644 --- a/application/dashboard/config/updateConfig/request_test.go +++ b/application/dashboard/config/updateConfig/request_test.go @@ -16,21 +16,24 @@ func TestRequest_Validate(t *testing.T) { { name: "valid request", request: Request{ - UserDefaultRoles: []string{"user", "editor"}, + UserDefaultRoles: []string{"user", "editor"}, + DefaultLanguageCode: "EN", }, want: domain.ValidationErrors{}, }, { name: "valid request with single role", request: Request{ - UserDefaultRoles: []string{"user"}, + UserDefaultRoles: []string{"user"}, + DefaultLanguageCode: "FA", }, want: domain.ValidationErrors{}, }, { name: "invalid request with empty user default roles", request: Request{ - UserDefaultRoles: []string{}, + UserDefaultRoles: []string{}, + DefaultLanguageCode: "EN", }, want: domain.ValidationErrors{ "user_default_roles": "required_field", @@ -39,12 +42,23 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with nil user default roles", request: Request{ - UserDefaultRoles: nil, + UserDefaultRoles: nil, + DefaultLanguageCode: "EN", }, want: domain.ValidationErrors{ "user_default_roles": "required_field", }, }, + { + name: "invalid request with empty default language", + request: Request{ + UserDefaultRoles: []string{"user"}, + DefaultLanguageCode: "", + }, + want: domain.ValidationErrors{ + "default_language_code": "required_field", + }, + }, } for _, tt := range tests { diff --git a/application/dashboard/config/updateConfig/usecase.go b/application/dashboard/config/updateConfig/usecase.go index 31ef97ea..3fab1c52 100644 --- a/application/dashboard/config/updateConfig/usecase.go +++ b/application/dashboard/config/updateConfig/usecase.go @@ -5,20 +5,24 @@ import ( "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/config" + "github.com/khanzadimahdi/testproject/domain/language" ) type UseCase struct { - configRepository config.Repository - validator domain.Validator + configRepository config.Repository + languageRepository language.Repository + validator domain.Validator } func NewUseCase( configRepository config.Repository, + languageRepository language.Repository, validator domain.Validator, ) *UseCase { return &UseCase{ - configRepository: configRepository, - validator: validator, + configRepository: configRepository, + languageRepository: languageRepository, + validator: validator, } } @@ -29,12 +33,21 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { }, nil } + if !uc.languageRepository.Exists(request.DefaultLanguageCode) { + return &Response{ + ValidationErrors: domain.ValidationErrors{ + "default_language_code": "invalid_value", + }, + }, nil + } + c, err := uc.configRepository.GetLatestRevision() if err != nil && !errors.Is(err, domain.ErrNotExists) { return nil, err } c.UserDefaultRoleUUIDs = request.UserDefaultRoles + c.DefaultLanguageCode = request.DefaultLanguageCode _, err = uc.configRepository.Save(&c) diff --git a/application/dashboard/config/updateConfig/usecase_test.go b/application/dashboard/config/updateConfig/usecase_test.go index a35cdb4c..d3ececa4 100644 --- a/application/dashboard/config/updateConfig/usecase_test.go +++ b/application/dashboard/config/updateConfig/usecase_test.go @@ -9,6 +9,7 @@ import ( "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/config" configMocks "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/config" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" "github.com/khanzadimahdi/testproject/infrastructure/validator" ) @@ -19,11 +20,13 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - configRepository configMocks.MockConfigRepository - validator validator.MockValidator + configRepository configMocks.MockConfigRepository + languageRepository languages.MockLanguagesRepository + validator validator.MockValidator r = Request{ - UserDefaultRoles: []string{"role1", "role2"}, + UserDefaultRoles: []string{"role1", "role2"}, + DefaultLanguageCode: "EN", } loadedConfig = config.Config{ @@ -34,17 +37,21 @@ func TestUseCase_Execute(t *testing.T) { savedConfig = config.Config{ Revision: loadedConfig.Revision, UserDefaultRoleUUIDs: r.UserDefaultRoles, + DefaultLanguageCode: r.DefaultLanguageCode, } ) validator.On("Validate", &r).Once().Return(nil) defer validator.AssertExpectations(t) + languageRepository.On("Exists", r.DefaultLanguageCode).Once().Return(true) + defer languageRepository.AssertExpectations(t) + configRepository.On("GetLatestRevision").Once().Return(loadedConfig, nil) configRepository.On("Save", &savedConfig).Once().Return("new-revision-uuid", nil) defer configRepository.AssertExpectations(t) - response, err := NewUseCase(&configRepository, &validator).Execute(&r) + response, err := NewUseCase(&configRepository, &languageRepository, &validator).Execute(&r) assert.NoError(t, err) assert.Nil(t, response) }) @@ -53,13 +60,15 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - configRepository configMocks.MockConfigRepository - validator validator.MockValidator + configRepository configMocks.MockConfigRepository + languageRepository languages.MockLanguagesRepository + validator validator.MockValidator r = Request{} expectedResponse = Response{ ValidationErrors: domain.ValidationErrors{ - "user_default_roles": "user_default_roles is required", + "user_default_roles": "user_default_roles is required", + "default_language_code": "default_language_code is required", }, } ) @@ -67,7 +76,43 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(expectedResponse.ValidationErrors) defer validator.AssertExpectations(t) - response, err := NewUseCase(&configRepository, &validator).Execute(&r) + response, err := NewUseCase(&configRepository, &languageRepository, &validator).Execute(&r) + + languageRepository.AssertNotCalled(t, "Exists") + configRepository.AssertNotCalled(t, "GetLatestRevision") + configRepository.AssertNotCalled(t, "Save") + + assert.NoError(t, err) + assert.Equal(t, &expectedResponse, response) + }) + + t.Run("default language does not exist", func(t *testing.T) { + t.Parallel() + + var ( + configRepository configMocks.MockConfigRepository + languageRepository languages.MockLanguagesRepository + validator validator.MockValidator + + r = Request{ + UserDefaultRoles: []string{"role1", "role2"}, + DefaultLanguageCode: "DE", + } + + expectedResponse = Response{ + ValidationErrors: domain.ValidationErrors{ + "default_language_code": "invalid_value", + }, + } + ) + + validator.On("Validate", &r).Once().Return(nil) + defer validator.AssertExpectations(t) + + languageRepository.On("Exists", r.DefaultLanguageCode).Once().Return(false) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&configRepository, &languageRepository, &validator).Execute(&r) configRepository.AssertNotCalled(t, "GetLatestRevision") configRepository.AssertNotCalled(t, "Save") @@ -80,11 +125,13 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - configRepository configMocks.MockConfigRepository - validator validator.MockValidator + configRepository configMocks.MockConfigRepository + languageRepository languages.MockLanguagesRepository + validator validator.MockValidator r = Request{ - UserDefaultRoles: []string{"role1", "role2"}, + UserDefaultRoles: []string{"role1", "role2"}, + DefaultLanguageCode: "EN", } expectedErr = errors.New("error") @@ -93,10 +140,13 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(nil) defer validator.AssertExpectations(t) + languageRepository.On("Exists", r.DefaultLanguageCode).Once().Return(true) + defer languageRepository.AssertExpectations(t) + configRepository.On("GetLatestRevision").Once().Return(config.Config{}, expectedErr) defer configRepository.AssertExpectations(t) - response, err := NewUseCase(&configRepository, &validator).Execute(&r) + response, err := NewUseCase(&configRepository, &languageRepository, &validator).Execute(&r) configRepository.AssertNotCalled(t, "Save") @@ -108,11 +158,13 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - configRepository configMocks.MockConfigRepository - validator validator.MockValidator + configRepository configMocks.MockConfigRepository + languageRepository languages.MockLanguagesRepository + validator validator.MockValidator r = Request{ - UserDefaultRoles: []string{"role1", "role2"}, + UserDefaultRoles: []string{"role1", "role2"}, + DefaultLanguageCode: "EN", } loadedConfig = config.Config{ @@ -123,6 +175,7 @@ func TestUseCase_Execute(t *testing.T) { savedConfig = config.Config{ Revision: loadedConfig.Revision, UserDefaultRoleUUIDs: r.UserDefaultRoles, + DefaultLanguageCode: r.DefaultLanguageCode, } expectedErr = errors.New("error") @@ -131,11 +184,14 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(nil) defer validator.AssertExpectations(t) + languageRepository.On("Exists", r.DefaultLanguageCode).Once().Return(true) + defer languageRepository.AssertExpectations(t) + configRepository.On("GetLatestRevision").Once().Return(loadedConfig, nil) configRepository.On("Save", &savedConfig).Once().Return("", expectedErr) defer configRepository.AssertExpectations(t) - response, err := NewUseCase(&configRepository, &validator).Execute(&r) + response, err := NewUseCase(&configRepository, &languageRepository, &validator).Execute(&r) assert.ErrorIs(t, err, expectedErr) assert.Nil(t, response) diff --git a/application/dashboard/language/createLanguage/request.go b/application/dashboard/language/createLanguage/request.go new file mode 100644 index 00000000..d1612f9c --- /dev/null +++ b/application/dashboard/language/createLanguage/request.go @@ -0,0 +1,24 @@ +package createlanguage + +import "github.com/khanzadimahdi/testproject/domain" + +type Request struct { + Code string `json:"code"` + Name string `json:"name"` +} + +var _ domain.Validatable = &Request{} + +func (r *Request) Validate() domain.ValidationErrors { + validationErrors := make(domain.ValidationErrors) + + if len(r.Code) == 0 { + validationErrors["code"] = "required_field" + } + + if len(r.Name) == 0 { + validationErrors["name"] = "required_field" + } + + return validationErrors +} diff --git a/application/dashboard/language/createLanguage/response.go b/application/dashboard/language/createLanguage/response.go new file mode 100644 index 00000000..a774db65 --- /dev/null +++ b/application/dashboard/language/createLanguage/response.go @@ -0,0 +1,9 @@ +package createlanguage + +import "github.com/khanzadimahdi/testproject/domain" + +type Response struct { + ValidationErrors domain.ValidationErrors `json:"errors,omitempty"` + + Code string `json:"code,omitempty"` +} diff --git a/application/dashboard/language/createLanguage/usecase.go b/application/dashboard/language/createLanguage/usecase.go new file mode 100644 index 00000000..e0c0f867 --- /dev/null +++ b/application/dashboard/language/createLanguage/usecase.go @@ -0,0 +1,46 @@ +package createlanguage + +import ( + "github.com/khanzadimahdi/testproject/domain" + "github.com/khanzadimahdi/testproject/domain/language" +) + +type UseCase struct { + languageRepository language.Repository + validator domain.Validator +} + +func NewUseCase(languageRepository language.Repository, validator domain.Validator) *UseCase { + return &UseCase{ + languageRepository: languageRepository, + validator: validator, + } +} + +func (uc *UseCase) Execute(request *Request) (*Response, error) { + if validationErrors := uc.validator.Validate(request); len(validationErrors) > 0 { + return &Response{ + ValidationErrors: validationErrors, + }, nil + } + + if uc.languageRepository.Exists(request.Code) { + return &Response{ + ValidationErrors: domain.ValidationErrors{ + "code": "already_exists", + }, + }, nil + } + + l := language.Language{ + Code: request.Code, + Name: request.Name, + } + + code, err := uc.languageRepository.Save(&l) + if err != nil { + return nil, err + } + + return &Response{Code: code}, nil +} diff --git a/application/dashboard/language/createLanguage/usecase_test.go b/application/dashboard/language/createLanguage/usecase_test.go new file mode 100644 index 00000000..f8b8fade --- /dev/null +++ b/application/dashboard/language/createLanguage/usecase_test.go @@ -0,0 +1,116 @@ +package createlanguage + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/khanzadimahdi/testproject/domain" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" + "github.com/khanzadimahdi/testproject/infrastructure/validator" +) + +func TestUseCase_Execute(t *testing.T) { + t.Parallel() + + t.Run("creates a language", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator + + request = Request{Code: "DE", Name: "Deutsch"} + ) + + requestValidator.On("Validate", &request).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageRepository.On("Exists", request.Code).Once().Return(false) + languageRepository.On("Save", mock.AnythingOfType("*language.Language")).Once().Return(request.Code, nil) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository, &requestValidator).Execute(&request) + + assert.NoError(t, err) + assert.NotNil(t, response) + assert.Empty(t, response.ValidationErrors) + assert.Equal(t, request.Code, response.Code) + }) + + t.Run("validation fails", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator + + request = Request{} + validationErrors = domain.ValidationErrors{ + "code": "required_field", + "name": "required_field", + } + ) + + requestValidator.On("Validate", &request).Once().Return(validationErrors) + defer requestValidator.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository, &requestValidator).Execute(&request) + + languageRepository.AssertNotCalled(t, "Exists") + languageRepository.AssertNotCalled(t, "Save") + + assert.NoError(t, err) + assert.Equal(t, validationErrors, response.ValidationErrors) + }) + + t.Run("language already exists", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator + + request = Request{Code: "EN", Name: "English"} + ) + + requestValidator.On("Validate", &request).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageRepository.On("Exists", request.Code).Once().Return(true) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository, &requestValidator).Execute(&request) + + languageRepository.AssertNotCalled(t, "Save") + + assert.NoError(t, err) + assert.Equal(t, "already_exists", response.ValidationErrors["code"]) + }) + + t.Run("saving fails", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator + + request = Request{Code: "DE", Name: "Deutsch"} + expectedError = errors.New("saving failed") + ) + + requestValidator.On("Validate", &request).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageRepository.On("Exists", request.Code).Once().Return(false) + languageRepository.On("Save", mock.AnythingOfType("*language.Language")).Once().Return("", expectedError) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository, &requestValidator).Execute(&request) + + assert.Nil(t, response) + assert.ErrorIs(t, err, expectedError) + }) +} diff --git a/application/dashboard/language/deleteLanguage/request.go b/application/dashboard/language/deleteLanguage/request.go new file mode 100644 index 00000000..96c57904 --- /dev/null +++ b/application/dashboard/language/deleteLanguage/request.go @@ -0,0 +1,5 @@ +package deletelanguage + +type Request struct { + Code string `json:"code"` +} diff --git a/application/dashboard/language/deleteLanguage/usecase.go b/application/dashboard/language/deleteLanguage/usecase.go new file mode 100644 index 00000000..6a540c77 --- /dev/null +++ b/application/dashboard/language/deleteLanguage/usecase.go @@ -0,0 +1,17 @@ +package deletelanguage + +import "github.com/khanzadimahdi/testproject/domain/language" + +type UseCase struct { + languageRepository language.Repository +} + +func NewUseCase(languageRepository language.Repository) *UseCase { + return &UseCase{ + languageRepository: languageRepository, + } +} + +func (uc *UseCase) Execute(request *Request) error { + return uc.languageRepository.Delete(request.Code) +} diff --git a/application/dashboard/language/deleteLanguage/usecase_test.go b/application/dashboard/language/deleteLanguage/usecase_test.go new file mode 100644 index 00000000..e86b55f4 --- /dev/null +++ b/application/dashboard/language/deleteLanguage/usecase_test.go @@ -0,0 +1,49 @@ +package deletelanguage + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" +) + +func TestUseCase_Execute(t *testing.T) { + t.Parallel() + + t.Run("deletes a language", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + + r = Request{Code: "EN"} + ) + + languageRepository.On("Delete", r.Code).Once().Return(nil) + defer languageRepository.AssertExpectations(t) + + err := NewUseCase(&languageRepository).Execute(&r) + + assert.NoError(t, err) + }) + + t.Run("deleting the language fails", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + + r = Request{Code: "EN"} + expectedError = errors.New("language deletion failed") + ) + + languageRepository.On("Delete", r.Code).Once().Return(expectedError) + defer languageRepository.AssertExpectations(t) + + err := NewUseCase(&languageRepository).Execute(&r) + + assert.ErrorIs(t, err, expectedError) + }) +} diff --git a/application/dashboard/language/getLanguage/response.go b/application/dashboard/language/getLanguage/response.go new file mode 100644 index 00000000..3cd20400 --- /dev/null +++ b/application/dashboard/language/getLanguage/response.go @@ -0,0 +1,15 @@ +package getlanguage + +import "github.com/khanzadimahdi/testproject/domain/language" + +type Response struct { + Code string `json:"code"` + Name string `json:"name"` +} + +func NewResponse(l language.Language) *Response { + return &Response{ + Code: l.Code, + Name: l.Name, + } +} diff --git a/application/dashboard/language/getLanguage/usecase.go b/application/dashboard/language/getLanguage/usecase.go new file mode 100644 index 00000000..4e144d63 --- /dev/null +++ b/application/dashboard/language/getLanguage/usecase.go @@ -0,0 +1,22 @@ +package getlanguage + +import "github.com/khanzadimahdi/testproject/domain/language" + +type UseCase struct { + languageRepository language.Repository +} + +func NewUseCase(languageRepository language.Repository) *UseCase { + return &UseCase{ + languageRepository: languageRepository, + } +} + +func (uc *UseCase) Execute(code string) (*Response, error) { + l, err := uc.languageRepository.GetOne(code) + if err != nil { + return nil, err + } + + return NewResponse(l), nil +} diff --git a/application/dashboard/language/getLanguage/usecase_test.go b/application/dashboard/language/getLanguage/usecase_test.go new file mode 100644 index 00000000..cd418ac5 --- /dev/null +++ b/application/dashboard/language/getLanguage/usecase_test.go @@ -0,0 +1,51 @@ +package getlanguage + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/khanzadimahdi/testproject/domain" + "github.com/khanzadimahdi/testproject/domain/language" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" +) + +func TestUseCase_Execute(t *testing.T) { + t.Parallel() + + t.Run("returns a language", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + + l = language.Language{Code: "EN", Name: "English"} + + expectedResponse = Response{Code: l.Code, Name: l.Name} + ) + + languageRepository.On("GetOne", l.Code).Once().Return(l, nil) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository).Execute(l.Code) + + assert.NoError(t, err) + assert.Equal(t, &expectedResponse, response) + }) + + t.Run("language does not exist", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + ) + + languageRepository.On("GetOne", "DE").Once().Return(language.Language{}, domain.ErrNotExists) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository).Execute("DE") + + assert.Nil(t, response) + assert.ErrorIs(t, err, domain.ErrNotExists) + }) +} diff --git a/application/dashboard/language/getLanguages/request.go b/application/dashboard/language/getLanguages/request.go new file mode 100644 index 00000000..889d2dbd --- /dev/null +++ b/application/dashboard/language/getLanguages/request.go @@ -0,0 +1,5 @@ +package getlanguages + +type Request struct { + Page uint +} diff --git a/application/dashboard/language/getLanguages/response.go b/application/dashboard/language/getLanguages/response.go new file mode 100644 index 00000000..eec35777 --- /dev/null +++ b/application/dashboard/language/getLanguages/response.go @@ -0,0 +1,35 @@ +package getlanguages + +import "github.com/khanzadimahdi/testproject/domain/language" + +type languageResponse struct { + Code string `json:"code"` + Name string `json:"name"` +} + +type Response struct { + Items []languageResponse `json:"items"` + Pagination pagination `json:"pagination"` +} + +type pagination struct { + TotalPages uint `json:"total_pages"` + CurrentPage uint `json:"current_page"` +} + +func NewResponse(l []language.Language, totalPages, currentPage uint) *Response { + items := make([]languageResponse, len(l)) + + for i := range l { + items[i].Code = l[i].Code + items[i].Name = l[i].Name + } + + return &Response{ + Items: items, + Pagination: pagination{ + TotalPages: totalPages, + CurrentPage: currentPage, + }, + } +} diff --git a/application/dashboard/language/getLanguages/usecase.go b/application/dashboard/language/getLanguages/usecase.go new file mode 100644 index 00000000..c277c4a9 --- /dev/null +++ b/application/dashboard/language/getLanguages/usecase.go @@ -0,0 +1,45 @@ +package getlanguages + +import "github.com/khanzadimahdi/testproject/domain/language" + +const limit = 10 + +type UseCase struct { + languageRepository language.Repository +} + +func NewUseCase(languageRepository language.Repository) *UseCase { + return &UseCase{ + languageRepository: languageRepository, + } +} + +func (uc *UseCase) Execute(request *Request) (*Response, error) { + totalLanguages, err := uc.languageRepository.Count() + if err != nil { + return nil, err + } + + currentPage := request.Page + if currentPage == 0 { + currentPage = 1 + } + + var offset uint = 0 + if currentPage > 0 { + offset = (currentPage - 1) * limit + } + + totalPages := totalLanguages / limit + + if (totalPages * limit) != totalLanguages { + totalPages++ + } + + languages, err := uc.languageRepository.GetAll(offset, limit) + if err != nil { + return nil, err + } + + return NewResponse(languages, totalPages, currentPage), nil +} diff --git a/application/dashboard/language/getLanguages/usecase_test.go b/application/dashboard/language/getLanguages/usecase_test.go new file mode 100644 index 00000000..e3b3a0e4 --- /dev/null +++ b/application/dashboard/language/getLanguages/usecase_test.go @@ -0,0 +1,87 @@ +package getlanguages + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/khanzadimahdi/testproject/domain/language" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" +) + +func TestUseCase_Execute(t *testing.T) { + t.Parallel() + + t.Run("returns a paginated list of languages", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + + l = []language.Language{ + {Code: "EN", Name: "English"}, + {Code: "FA", Name: "فارسی"}, + } + + request = Request{Page: 1} + + expectedResponse = Response{ + Items: []languageResponse{ + {Code: l[0].Code, Name: l[0].Name}, + {Code: l[1].Code, Name: l[1].Name}, + }, + Pagination: pagination{TotalPages: 1, CurrentPage: 1}, + } + ) + + languageRepository.On("Count").Once().Return(uint(len(l)), nil) + languageRepository.On("GetAll", uint(0), uint(limit)).Once().Return(l, nil) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository).Execute(&request) + + assert.NoError(t, err) + assert.Equal(t, &expectedResponse, response) + }) + + t.Run("counting languages fails", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + + request = Request{Page: 1} + expectedError = errors.New("counting failed") + ) + + languageRepository.On("Count").Once().Return(uint(0), expectedError) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository).Execute(&request) + + languageRepository.AssertNotCalled(t, "GetAll") + assert.Nil(t, response) + assert.ErrorIs(t, err, expectedError) + }) + + t.Run("getting languages fails", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + + request = Request{Page: 1} + expectedError = errors.New("getting failed") + ) + + languageRepository.On("Count").Once().Return(uint(2), nil) + languageRepository.On("GetAll", uint(0), uint(limit)).Once().Return(nil, expectedError) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository).Execute(&request) + + assert.Nil(t, response) + assert.ErrorIs(t, err, expectedError) + }) +} diff --git a/application/dashboard/language/updateLanguage/request.go b/application/dashboard/language/updateLanguage/request.go new file mode 100644 index 00000000..6b070d69 --- /dev/null +++ b/application/dashboard/language/updateLanguage/request.go @@ -0,0 +1,24 @@ +package updatelanguage + +import "github.com/khanzadimahdi/testproject/domain" + +type Request struct { + Code string `json:"code"` + Name string `json:"name"` +} + +var _ domain.Validatable = &Request{} + +func (r *Request) Validate() domain.ValidationErrors { + validationErrors := make(domain.ValidationErrors) + + if len(r.Code) == 0 { + validationErrors["code"] = "required_field" + } + + if len(r.Name) == 0 { + validationErrors["name"] = "required_field" + } + + return validationErrors +} diff --git a/application/dashboard/language/updateLanguage/response.go b/application/dashboard/language/updateLanguage/response.go new file mode 100644 index 00000000..7d2b8b20 --- /dev/null +++ b/application/dashboard/language/updateLanguage/response.go @@ -0,0 +1,7 @@ +package updatelanguage + +import "github.com/khanzadimahdi/testproject/domain" + +type Response struct { + ValidationErrors domain.ValidationErrors `json:"errors,omitempty"` +} diff --git a/application/dashboard/language/updateLanguage/usecase.go b/application/dashboard/language/updateLanguage/usecase.go new file mode 100644 index 00000000..3a099bb6 --- /dev/null +++ b/application/dashboard/language/updateLanguage/usecase.go @@ -0,0 +1,41 @@ +package updatelanguage + +import ( + "github.com/khanzadimahdi/testproject/domain" + "github.com/khanzadimahdi/testproject/domain/language" +) + +type UseCase struct { + languageRepository language.Repository + validator domain.Validator +} + +func NewUseCase(languageRepository language.Repository, validator domain.Validator) *UseCase { + return &UseCase{ + languageRepository: languageRepository, + validator: validator, + } +} + +func (uc *UseCase) Execute(request *Request) (*Response, error) { + if validationErrors := uc.validator.Validate(request); len(validationErrors) > 0 { + return &Response{ + ValidationErrors: validationErrors, + }, nil + } + + if !uc.languageRepository.Exists(request.Code) { + return nil, domain.ErrNotExists + } + + l := language.Language{ + Code: request.Code, + Name: request.Name, + } + + if _, err := uc.languageRepository.Save(&l); err != nil { + return nil, err + } + + return nil, nil +} diff --git a/application/dashboard/language/updateLanguage/usecase_test.go b/application/dashboard/language/updateLanguage/usecase_test.go new file mode 100644 index 00000000..5fd0afe4 --- /dev/null +++ b/application/dashboard/language/updateLanguage/usecase_test.go @@ -0,0 +1,114 @@ +package updatelanguage + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/khanzadimahdi/testproject/domain" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" + "github.com/khanzadimahdi/testproject/infrastructure/validator" +) + +func TestUseCase_Execute(t *testing.T) { + t.Parallel() + + t.Run("updates a language", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator + + request = Request{Code: "EN", Name: "English (US)"} + ) + + requestValidator.On("Validate", &request).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageRepository.On("Exists", request.Code).Once().Return(true) + languageRepository.On("Save", mock.AnythingOfType("*language.Language")).Once().Return(request.Code, nil) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository, &requestValidator).Execute(&request) + + assert.NoError(t, err) + assert.Nil(t, response) + }) + + t.Run("validation fails", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator + + request = Request{} + validationErrors = domain.ValidationErrors{ + "code": "required_field", + "name": "required_field", + } + ) + + requestValidator.On("Validate", &request).Once().Return(validationErrors) + defer requestValidator.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository, &requestValidator).Execute(&request) + + languageRepository.AssertNotCalled(t, "Exists") + languageRepository.AssertNotCalled(t, "Save") + + assert.NoError(t, err) + assert.Equal(t, validationErrors, response.ValidationErrors) + }) + + t.Run("language does not exist", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator + + request = Request{Code: "DE", Name: "Deutsch"} + ) + + requestValidator.On("Validate", &request).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageRepository.On("Exists", request.Code).Once().Return(false) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository, &requestValidator).Execute(&request) + + languageRepository.AssertNotCalled(t, "Save") + + assert.Nil(t, response) + assert.ErrorIs(t, err, domain.ErrNotExists) + }) + + t.Run("saving fails", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator + + request = Request{Code: "EN", Name: "English"} + expectedError = errors.New("saving failed") + ) + + requestValidator.On("Validate", &request).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageRepository.On("Exists", request.Code).Once().Return(true) + languageRepository.On("Save", mock.AnythingOfType("*language.Language")).Once().Return("", expectedError) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository, &requestValidator).Execute(&request) + + assert.Nil(t, response) + assert.ErrorIs(t, err, expectedError) + }) +} diff --git a/application/dashboard/profile/getprofile/response.go b/application/dashboard/profile/getprofile/response.go index 1426741d..9dfa7eae 100644 --- a/application/dashboard/profile/getprofile/response.go +++ b/application/dashboard/profile/getprofile/response.go @@ -1,9 +1,10 @@ package getprofile type Response struct { - UUID string `json:"uuid,omitempty"` - Name string `json:"name,omitempty"` - Avatar string `json:"avatar,omitempty"` - Email string `json:"email,omitempty"` - Username string `json:"username,omitempty"` + UUID string `json:"uuid,omitempty"` + Name string `json:"name,omitempty"` + Avatar string `json:"avatar,omitempty"` + Email string `json:"email,omitempty"` + Username string `json:"username,omitempty"` + LanguageCode string `json:"language_code,omitempty"` } diff --git a/application/dashboard/profile/getprofile/usecase.go b/application/dashboard/profile/getprofile/usecase.go index 1aa6ffa5..c4b6afd5 100644 --- a/application/dashboard/profile/getprofile/usecase.go +++ b/application/dashboard/profile/getprofile/usecase.go @@ -21,10 +21,11 @@ func (uc *UseCase) Execute(UUID string) (*Response, error) { } return &Response{ - UUID: UUID, - Name: u.Name, - Avatar: u.Avatar, - Email: u.Email, - Username: u.Username, + UUID: UUID, + Name: u.Name, + Avatar: u.Avatar, + Email: u.Email, + Username: u.Username, + LanguageCode: u.LanguageCode, }, err } diff --git a/application/dashboard/profile/getprofile/usecase_test.go b/application/dashboard/profile/getprofile/usecase_test.go index bef9fd49..32c02bbc 100644 --- a/application/dashboard/profile/getprofile/usecase_test.go +++ b/application/dashboard/profile/getprofile/usecase_test.go @@ -22,19 +22,21 @@ func TestUseCase_Execute(t *testing.T) { userUUID = "user-uuid" u = user.User{ - UUID: userUUID, - Name: "test name", - Avatar: "test-avatar", - Email: "test@test.com", - Username: "test-username", + UUID: userUUID, + Name: "test name", + Avatar: "test-avatar", + Email: "test@test.com", + Username: "test-username", + LanguageCode: "EN", } expectedResponse = Response{ - UUID: u.UUID, - Name: u.Name, - Avatar: u.Avatar, - Email: u.Email, - Username: u.Username, + UUID: u.UUID, + Name: u.Name, + Avatar: u.Avatar, + Email: u.Email, + Username: u.Username, + LanguageCode: u.LanguageCode, } ) diff --git a/application/dashboard/profile/updateprofile/request.go b/application/dashboard/profile/updateprofile/request.go index 4356398d..681543da 100644 --- a/application/dashboard/profile/updateprofile/request.go +++ b/application/dashboard/profile/updateprofile/request.go @@ -6,11 +6,12 @@ import ( ) type Request struct { - UserUUID string `json:"-"` - Name string `json:"name"` - Avatar string `json:"avatar"` - Email string `json:"email"` - Username string `json:"username"` + UserUUID string `json:"-"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Email string `json:"email"` + Username string `json:"username"` + LanguageCode string `json:"language_code"` } var _ domain.Validatable = &Request{} @@ -36,5 +37,9 @@ func (r *Request) Validate() domain.ValidationErrors { validationErrors["username"] = "required_field" } + if len(r.LanguageCode) == 0 { + validationErrors["language_code"] = "required_field" + } + return validationErrors } diff --git a/application/dashboard/profile/updateprofile/request_test.go b/application/dashboard/profile/updateprofile/request_test.go index 6628ebf6..bb605d91 100644 --- a/application/dashboard/profile/updateprofile/request_test.go +++ b/application/dashboard/profile/updateprofile/request_test.go @@ -17,31 +17,34 @@ func TestRequest_Validate(t *testing.T) { { name: "valid request", request: Request{ - UserUUID: "user-uuid-123", - Name: "John Doe", - Avatar: "avatar.jpg", - Email: "user@example.com", - Username: "johndoe", + UserUUID: "user-uuid-123", + Name: "John Doe", + Avatar: "avatar.jpg", + Email: "user@example.com", + Username: "johndoe", + LanguageCode: "EN", }, wantErr: domain.ValidationErrors{}, }, { name: "valid request with empty optional fields", request: Request{ - UserUUID: "user-uuid-123", - Name: "John Doe", - Email: "user@example.com", - Username: "johndoe", + UserUUID: "user-uuid-123", + Name: "John Doe", + Email: "user@example.com", + Username: "johndoe", + LanguageCode: "EN", }, wantErr: domain.ValidationErrors{}, }, { name: "invalid request with empty user uuid", request: Request{ - UserUUID: "", - Name: "John Doe", - Email: "user@example.com", - Username: "johndoe", + UserUUID: "", + Name: "John Doe", + Email: "user@example.com", + Username: "johndoe", + LanguageCode: "EN", }, wantErr: domain.ValidationErrors{ "uuid": "required_field", @@ -50,10 +53,11 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with empty name", request: Request{ - UserUUID: "user-uuid-123", - Name: "", - Email: "user@example.com", - Username: "johndoe", + UserUUID: "user-uuid-123", + Name: "", + Email: "user@example.com", + Username: "johndoe", + LanguageCode: "EN", }, wantErr: domain.ValidationErrors{ "name": "required_field", @@ -62,10 +66,11 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with empty email", request: Request{ - UserUUID: "user-uuid-123", - Name: "John Doe", - Email: "", - Username: "johndoe", + UserUUID: "user-uuid-123", + Name: "John Doe", + Email: "", + Username: "johndoe", + LanguageCode: "EN", }, wantErr: domain.ValidationErrors{ "email": "required_field", @@ -74,10 +79,11 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with malformed email", request: Request{ - UserUUID: "user-uuid-123", - Name: "John Doe", - Email: "not-an-email", - Username: "johndoe", + UserUUID: "user-uuid-123", + Name: "John Doe", + Email: "not-an-email", + Username: "johndoe", + LanguageCode: "EN", }, wantErr: domain.ValidationErrors{ "email": "invalid_email", @@ -86,28 +92,44 @@ func TestRequest_Validate(t *testing.T) { { name: "invalid request with empty username", request: Request{ - UserUUID: "user-uuid-123", - Name: "John Doe", - Email: "user@example.com", - Username: "", + UserUUID: "user-uuid-123", + Name: "John Doe", + Email: "user@example.com", + Username: "", + LanguageCode: "EN", }, wantErr: domain.ValidationErrors{ "username": "required_field", }, }, + { + name: "invalid request with empty language", + request: Request{ + UserUUID: "user-uuid-123", + Name: "John Doe", + Email: "user@example.com", + Username: "johndoe", + LanguageCode: "", + }, + wantErr: domain.ValidationErrors{ + "language_code": "required_field", + }, + }, { name: "invalid request with multiple errors", request: Request{ - UserUUID: "", - Name: "", - Email: "", - Username: "", + UserUUID: "", + Name: "", + Email: "", + Username: "", + LanguageCode: "", }, wantErr: domain.ValidationErrors{ - "uuid": "required_field", - "name": "required_field", - "email": "required_field", - "username": "required_field", + "uuid": "required_field", + "name": "required_field", + "email": "required_field", + "username": "required_field", + "language_code": "required_field", }, }, } diff --git a/application/dashboard/profile/updateprofile/usecase.go b/application/dashboard/profile/updateprofile/usecase.go index 0a151807..d6028d50 100644 --- a/application/dashboard/profile/updateprofile/usecase.go +++ b/application/dashboard/profile/updateprofile/usecase.go @@ -3,26 +3,30 @@ package updateprofile import ( "errors" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/translator" "github.com/khanzadimahdi/testproject/domain/user" ) type UseCase struct { - userRepository user.Repository - validator domain.Validator - translator translator.Translator + userRepository user.Repository + languageResolver resolver.Resolver + validator domain.Validator + translator translator.Translator } func NewUseCase( userRepository user.Repository, + languageResolver resolver.Resolver, validator domain.Validator, translator translator.Translator, ) *UseCase { return &UseCase{ - userRepository: userRepository, - validator: validator, - translator: translator, + userRepository: userRepository, + languageResolver: languageResolver, + validator: validator, + translator: translator, } } @@ -57,6 +61,14 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { }, nil } + if !uc.languageResolver.Verify(request.LanguageCode) { + return &Response{ + ValidationErrors: domain.ValidationErrors{ + "language_code": "invalid_value", + }, + }, nil + } + u, err := uc.userRepository.GetOne(request.UserUUID) if err != nil { return nil, err @@ -66,6 +78,7 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { u.Avatar = request.Avatar u.Email = request.Email u.Username = request.Username + u.LanguageCode = request.LanguageCode _, err = uc.userRepository.Save(&u) diff --git a/application/dashboard/profile/updateprofile/usecase_test.go b/application/dashboard/profile/updateprofile/usecase_test.go index db433417..c0efc22e 100644 --- a/application/dashboard/profile/updateprofile/usecase_test.go +++ b/application/dashboard/profile/updateprofile/usecase_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" translatorContract "github.com/khanzadimahdi/testproject/domain/translator" "github.com/khanzadimahdi/testproject/domain/user" @@ -25,37 +26,43 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - userRepository users.MockUsersRepository - validator validator.MockValidator - translator translator.TranslatorMock + userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + validator validator.MockValidator + translator translator.TranslatorMock r = Request{ - UserUUID: "test-user-uuid", - Name: "John Doe", - Avatar: "test-avatar", - Email: "test@test.com", - Username: "john.doe", + UserUUID: "test-user-uuid", + Name: "John Doe", + Avatar: "test-avatar", + Email: "test@test.com", + Username: "john.doe", + LanguageCode: "EN", } u = user.User{ - UUID: r.UserUUID, - Name: r.Name, - Avatar: r.Avatar, - Email: r.Email, - Username: r.Username, + UUID: r.UserUUID, + Name: r.Name, + Avatar: r.Avatar, + Email: r.Email, + Username: r.Username, + LanguageCode: r.LanguageCode, } ) validator.On("Validate", &r).Once().Return(nil) defer validator.AssertExpectations(t) + languageResolver.On("Verify", r.LanguageCode).Once().Return(true) + defer languageResolver.AssertExpectations(t) + userRepository.On("GetOneByIdentity", r.Email).Once().Return(u, nil) userRepository.On("GetOneByIdentity", r.Username).Once().Return(u, nil) userRepository.On("GetOne", r.UserUUID).Once().Return(u, nil) userRepository.On("Save", &u).Once().Return(r.UserUUID, nil) defer userRepository.AssertExpectations(t) - response, err := NewUseCase(&userRepository, &validator, &translator).Execute(&r) + response, err := NewUseCase(&userRepository, &languageResolver, &validator, &translator).Execute(&r) translator.AssertNotCalled(t, "Translate") @@ -67,18 +74,20 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - userRepository users.MockUsersRepository - validator validator.MockValidator - translator translator.TranslatorMock + userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + validator validator.MockValidator + translator translator.TranslatorMock r = Request{} expectedResponse = Response{ ValidationErrors: domain.ValidationErrors{ - "uuid": "universal unique identifier (uuid) is required", - "name": "name is required", - "email": "email is required", - "username": "username is required", + "uuid": "universal unique identifier (uuid) is required", + "name": "name is required", + "email": "email is required", + "username": "username is required", + "language_code": "language is required", }, } ) @@ -86,9 +95,10 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(expectedResponse.ValidationErrors) defer validator.AssertExpectations(t) - response, err := NewUseCase(&userRepository, &validator, &translator).Execute(&r) + response, err := NewUseCase(&userRepository, &languageResolver, &validator, &translator).Execute(&r) translator.AssertNotCalled(t, "Translate") + languageResolver.AssertNotCalled(t, "Verify") userRepository.AssertNotCalled(t, "GetOneByIdentity") userRepository.AssertNotCalled(t, "GetOne") userRepository.AssertNotCalled(t, "Save") @@ -101,16 +111,18 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - userRepository users.MockUsersRepository - validator validator.MockValidator - translator translator.TranslatorMock + userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + validator validator.MockValidator + translator translator.TranslatorMock r = Request{ - UserUUID: "test-user-uuid", - Name: "John Doe", - Avatar: "test-avatar", - Email: "test@test.com", - Username: "john.doe", + UserUUID: "test-user-uuid", + Name: "John Doe", + Avatar: "test-avatar", + Email: "test@test.com", + Username: "john.doe", + LanguageCode: "EN", } u = user.User{ @@ -141,8 +153,9 @@ func TestUseCase_Execute(t *testing.T) { userRepository.On("GetOneByIdentity", r.Email).Once().Return(u, nil) defer userRepository.AssertExpectations(t) - response, err := NewUseCase(&userRepository, &validator, &translator).Execute(&r) + response, err := NewUseCase(&userRepository, &languageResolver, &validator, &translator).Execute(&r) + languageResolver.AssertNotCalled(t, "Verify") userRepository.AssertNotCalled(t, "GetOneByIdentity", r.Username) userRepository.AssertNotCalled(t, "GetOne") userRepository.AssertNotCalled(t, "Save") @@ -155,16 +168,18 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - userRepository users.MockUsersRepository - validator validator.MockValidator - translator translator.TranslatorMock + userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + validator validator.MockValidator + translator translator.TranslatorMock r = Request{ - UserUUID: "test-user-uuid", - Name: "John Doe", - Avatar: "test-avatar", - Email: "test@test.com", - Username: "john.doe", + UserUUID: "test-user-uuid", + Name: "John Doe", + Avatar: "test-avatar", + Email: "test@test.com", + Username: "john.doe", + LanguageCode: "EN", } u = user.User{ @@ -204,8 +219,62 @@ func TestUseCase_Execute(t *testing.T) { userRepository.On("GetOneByIdentity", r.Username).Once().Return(anotherUserWithSameUsername, nil) defer userRepository.AssertExpectations(t) - response, err := NewUseCase(&userRepository, &validator, &translator).Execute(&r) + response, err := NewUseCase(&userRepository, &languageResolver, &validator, &translator).Execute(&r) + languageResolver.AssertNotCalled(t, "Verify") + userRepository.AssertNotCalled(t, "GetOne") + userRepository.AssertNotCalled(t, "Save") + + assert.NoError(t, err) + assert.Equal(t, &expectedResponse, response) + }) + + t.Run("failure because the chosen language does not exist", func(t *testing.T) { + t.Parallel() + + var ( + userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + validator validator.MockValidator + translator translator.TranslatorMock + + r = Request{ + UserUUID: "test-user-uuid", + Name: "John Doe", + Avatar: "test-avatar", + Email: "test@test.com", + Username: "john.doe", + LanguageCode: "DE", + } + + u = user.User{ + UUID: r.UserUUID, + Name: r.Name, + Avatar: r.Avatar, + Email: r.Email, + Username: r.Username, + } + + expectedResponse = Response{ + ValidationErrors: domain.ValidationErrors{ + "language_code": "invalid_value", + }, + } + ) + + validator.On("Validate", &r).Once().Return(nil) + defer validator.AssertExpectations(t) + + languageResolver.On("Verify", r.LanguageCode).Once().Return(false) + defer languageResolver.AssertExpectations(t) + + userRepository.On("GetOneByIdentity", r.Email).Once().Return(u, nil) + userRepository.On("GetOneByIdentity", r.Username).Once().Return(u, nil) + defer userRepository.AssertExpectations(t) + + response, err := NewUseCase(&userRepository, &languageResolver, &validator, &translator).Execute(&r) + + translator.AssertNotCalled(t, "Translate") userRepository.AssertNotCalled(t, "GetOne") userRepository.AssertNotCalled(t, "Save") @@ -217,16 +286,18 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - userRepository users.MockUsersRepository - validator validator.MockValidator - translator translator.TranslatorMock + userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + validator validator.MockValidator + translator translator.TranslatorMock r = Request{ - UserUUID: "test-user-uuid", - Name: "John Doe", - Avatar: "test-avatar", - Email: "test@test.com", - Username: "john.doe", + UserUUID: "test-user-uuid", + Name: "John Doe", + Avatar: "test-avatar", + Email: "test@test.com", + Username: "john.doe", + LanguageCode: "EN", } u = user.User{ @@ -243,12 +314,15 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(nil) defer validator.AssertExpectations(t) + languageResolver.On("Verify", r.LanguageCode).Once().Return(true) + defer languageResolver.AssertExpectations(t) + userRepository.On("GetOneByIdentity", r.Email).Once().Return(u, nil) userRepository.On("GetOneByIdentity", r.Username).Once().Return(u, nil) userRepository.On("GetOne", r.UserUUID).Once().Return(user.User{}, expectedErr) defer userRepository.AssertExpectations(t) - response, err := NewUseCase(&userRepository, &validator, &translator).Execute(&r) + response, err := NewUseCase(&userRepository, &languageResolver, &validator, &translator).Execute(&r) translator.AssertNotCalled(t, "Translate") userRepository.AssertNotCalled(t, "Save") @@ -261,24 +335,27 @@ func TestUseCase_Execute(t *testing.T) { t.Parallel() var ( - userRepository users.MockUsersRepository - validator validator.MockValidator - translator translator.TranslatorMock + userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + validator validator.MockValidator + translator translator.TranslatorMock r = Request{ - UserUUID: "test-user-uuid", - Name: "John Doe", - Avatar: "test-avatar", - Email: "test@test.com", - Username: "john.doe", + UserUUID: "test-user-uuid", + Name: "John Doe", + Avatar: "test-avatar", + Email: "test@test.com", + Username: "john.doe", + LanguageCode: "EN", } u = user.User{ - UUID: r.UserUUID, - Name: r.Name, - Avatar: r.Avatar, - Email: r.Email, - Username: r.Username, + UUID: r.UserUUID, + Name: r.Name, + Avatar: r.Avatar, + Email: r.Email, + Username: r.Username, + LanguageCode: r.LanguageCode, } expectedErr = errors.New("save user info error") @@ -287,13 +364,16 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(nil) defer validator.AssertExpectations(t) + languageResolver.On("Verify", r.LanguageCode).Once().Return(true) + defer languageResolver.AssertExpectations(t) + userRepository.On("GetOneByIdentity", r.Email).Once().Return(u, nil) userRepository.On("GetOneByIdentity", r.Username).Once().Return(u, nil) userRepository.On("GetOne", r.UserUUID).Once().Return(u, nil) userRepository.On("Save", &u).Once().Return("", expectedErr) defer userRepository.AssertExpectations(t) - response, err := NewUseCase(&userRepository, &validator, &translator).Execute(&r) + response, err := NewUseCase(&userRepository, &languageResolver, &validator, &translator).Execute(&r) translator.AssertNotCalled(t, "Translate") diff --git a/application/dashboard/user/getUser/response.go b/application/dashboard/user/getUser/response.go index 0f45f881..ce15ca8f 100644 --- a/application/dashboard/user/getUser/response.go +++ b/application/dashboard/user/getUser/response.go @@ -1,9 +1,10 @@ package getuser type Response struct { - UUID string `json:"uuid,omitempty"` - Name string `json:"name,omitempty"` - Avatar string `json:"avatar,omitempty"` - Email string `json:"email,omitempty"` - Username string `json:"username,omitempty"` + UUID string `json:"uuid,omitempty"` + Name string `json:"name,omitempty"` + Avatar string `json:"avatar,omitempty"` + Email string `json:"email,omitempty"` + Username string `json:"username,omitempty"` + LanguageCode string `json:"language_code,omitempty"` } diff --git a/application/dashboard/user/getUser/useCase.go b/application/dashboard/user/getUser/useCase.go index 86fb6aca..815e9f2e 100644 --- a/application/dashboard/user/getUser/useCase.go +++ b/application/dashboard/user/getUser/useCase.go @@ -21,10 +21,11 @@ func (uc *UseCase) Execute(UUID string) (*Response, error) { } return &Response{ - UUID: UUID, - Name: u.Name, - Avatar: u.Avatar, - Email: u.Email, - Username: u.Username, + UUID: UUID, + Name: u.Name, + Avatar: u.Avatar, + Email: u.Email, + Username: u.Username, + LanguageCode: u.LanguageCode, }, err } diff --git a/application/element/element.go b/application/element/element.go index 104fc889..cd3e82d5 100644 --- a/application/element/element.go +++ b/application/element/element.go @@ -28,7 +28,7 @@ func NewRetriever( } // RetrieveByVenues retrieves elements by venues. -func (r *Retriever) RetrieveByVenues(venues []string) ([]Response, error) { +func (r *Retriever) RetrieveByVenues(venues []string, languageCode string) ([]Response, error) { elements, err := r.elementRepository.GetByVenues(venues) if err != nil { return nil, err @@ -44,7 +44,7 @@ func (r *Retriever) RetrieveByVenues(venues []string) ([]Response, error) { uuids[i] = items[i].ContentUUID } - articles, err := r.articleRepository.GetByUUIDs(uuids) + articles, err := r.articleRepository.GetByCorrelationUUIDs(uuids, languageCode) if err != nil { return nil, err } diff --git a/application/home/request.go b/application/home/request.go new file mode 100644 index 00000000..d611b978 --- /dev/null +++ b/application/home/request.go @@ -0,0 +1,5 @@ +package home + +type Request struct { + LanguageCode string +} diff --git a/application/home/response.go b/application/home/response.go index 88e9b632..844ace72 100644 --- a/application/home/response.go +++ b/application/home/response.go @@ -5,13 +5,15 @@ import ( "github.com/khanzadimahdi/testproject/application/element" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" ) type Response struct { - All []articleResponse `json:"all"` - Popular []articleResponse `json:"popular"` - Elements []element.Response `json:"elements"` + All []articleResponse `json:"all"` + Popular []articleResponse `json:"popular"` + Elements []element.Response `json:"elements"` + LanguageCode languageResponse `json:"language_code"` } type articleResponse struct { @@ -31,7 +33,12 @@ type author struct { Username string `json:"username"` } -func NewResponse(all, popular []article.Article, authors []user.User, elementsResponse []element.Response) *Response { +type languageResponse struct { + Code string `json:"code"` + Name string `json:"name"` +} + +func NewResponse(all, popular []article.Article, authors []user.User, requestedLanguage language.Language, elementsResponse []element.Response) *Response { authorByUUID := make(map[string]user.User, len(authors)) for i := range authors { authorByUUID[authors[i].UUID] = authors[i] @@ -41,6 +48,10 @@ func NewResponse(all, popular []article.Article, authors []user.User, elementsRe All: toArticleResponse(all, authorByUUID), Popular: toArticleResponse(popular, authorByUUID), Elements: elementsResponse, + LanguageCode: languageResponse{ + Code: requestedLanguage.Code, + Name: requestedLanguage.Name, + }, } } diff --git a/application/home/usecase.go b/application/home/usecase.go index 687daebb..f0c973bd 100644 --- a/application/home/usecase.go +++ b/application/home/usecase.go @@ -2,6 +2,7 @@ package home import ( "github.com/khanzadimahdi/testproject/application/element" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain/article" "github.com/khanzadimahdi/testproject/domain/user" ) @@ -10,27 +11,45 @@ type UseCase struct { articleRepository article.Repository userRepository user.Repository elementRetriever *element.Retriever + languageResolver resolver.Resolver } func NewUseCase( articleRepository article.Repository, userRepository user.Repository, elementRetriever *element.Retriever, + languageResolver resolver.Resolver, ) *UseCase { return &UseCase{ articleRepository: articleRepository, userRepository: userRepository, elementRetriever: elementRetriever, + languageResolver: languageResolver, } } -func (uc *UseCase) Execute() (*Response, error) { - popular, err := uc.articleRepository.GetMostViewed(4) +func (uc *UseCase) Execute(request *Request) (*Response, error) { + languageCode := request.LanguageCode + if len(languageCode) == 0 { + code, err := uc.languageResolver.DefaultCode() + if err != nil { + return nil, err + } + + languageCode = code + } + + l, err := uc.languageResolver.Resolve(languageCode) + if err != nil { + return nil, err + } + + popular, err := uc.articleRepository.GetMostViewed(languageCode, 4) if err != nil { return nil, err } - all, err := uc.articleRepository.GetAllPublished(0, 3) + all, err := uc.articleRepository.GetAllPublished(languageCode, 0, 3) if err != nil { return nil, err } @@ -48,10 +67,10 @@ func (uc *UseCase) Execute() (*Response, error) { return nil, err } - elementsResponse, err := uc.elementRetriever.RetrieveByVenues([]string{"home"}) + elementsResponse, err := uc.elementRetriever.RetrieveByVenues([]string{"home"}, languageCode) if err != nil { return nil, err } - return NewResponse(all, popular, u, elementsResponse), nil + return NewResponse(all, popular, u, l, elementsResponse), nil } diff --git a/application/home/usecase_test.go b/application/home/usecase_test.go index 3212f688..afc2dab3 100644 --- a/application/home/usecase_test.go +++ b/application/home/usecase_test.go @@ -7,9 +7,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/khanzadimahdi/testproject/application/element" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain/article" domainElement "github.com/khanzadimahdi/testproject/domain/element" "github.com/khanzadimahdi/testproject/domain/element/component" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/elements" @@ -26,6 +28,7 @@ func TestUseCase_Execute(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver mockComponent component.MockComponent a = []article.Article{ @@ -60,9 +63,9 @@ func TestUseCase_Execute(t *testing.T) { elementAuthorUUIDs = []string{"author-uuid-1", "author-uuid-2"} ) - articlesRepository.On("GetMostViewed", uint(4)).Once().Return(a, nil) - articlesRepository.On("GetAllPublished", uint(0), uint(3)).Once().Return(a, nil) - articlesRepository.On("GetByUUIDs", articleUUIDs).Once().Return(va, nil) + articlesRepository.On("GetMostViewed", "EN", uint(4)).Once().Return(a, nil) + articlesRepository.On("GetAllPublished", "EN", uint(0), uint(3)).Once().Return(a, nil) + articlesRepository.On("GetByCorrelationUUIDs", articleUUIDs, "EN").Once().Return(va, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetByUUIDs", homeAuthorUUIDs).Once().Return(u, nil) @@ -80,9 +83,13 @@ func TestUseCase_Execute(t *testing.T) { elementsRepository.On("GetByVenues", []string{"home"}).Once().Return(v, nil) defer elementsRepository.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - usecase := NewUseCase(&articlesRepository, &userRepository, elementRetriever) - response, err := usecase.Execute() + usecase := NewUseCase(&articlesRepository, &userRepository, elementRetriever, &languageResolver) + response, err := usecase.Execute(&Request{}) assert.NotNil(t, response, "unexpected response") assert.NoError(t, err) @@ -95,19 +102,24 @@ func TestUseCase_Execute(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver expectedErr = errors.New("some error") ) - articlesRepository.On("GetMostViewed", uint(4)).Once().Return(nil, expectedErr) + articlesRepository.On("GetMostViewed", "EN", uint(4)).Once().Return(nil, expectedErr) defer articlesRepository.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - usecase := NewUseCase(&articlesRepository, &userRepository, elementRetriever) - response, err := usecase.Execute() + usecase := NewUseCase(&articlesRepository, &userRepository, elementRetriever, &languageResolver) + response, err := usecase.Execute(&Request{}) articlesRepository.AssertNotCalled(t, "GetAllPublished") - articlesRepository.AssertNotCalled(t, "GetByUUIDs") + articlesRepository.AssertNotCalled(t, "GetByCorrelationUUIDs") userRepository.AssertNotCalled(t, "GetByUUIDs") elementsRepository.AssertNotCalled(t, "GetByVenues") @@ -122,6 +134,7 @@ func TestUseCase_Execute(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver a = []article.Article{ {UUID: "test-article-1"}, @@ -132,16 +145,20 @@ func TestUseCase_Execute(t *testing.T) { expectedErr = errors.New("some error") ) - articlesRepository.On("GetMostViewed", uint(4)).Once().Return(a, nil) - articlesRepository.On("GetAllPublished", uint(0), uint(3)).Return(nil, expectedErr) + articlesRepository.On("GetMostViewed", "EN", uint(4)).Once().Return(a, nil) + articlesRepository.On("GetAllPublished", "EN", uint(0), uint(3)).Return(nil, expectedErr) defer articlesRepository.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - usecase := NewUseCase(&articlesRepository, &userRepository, elementRetriever) - response, err := usecase.Execute() + usecase := NewUseCase(&articlesRepository, &userRepository, elementRetriever, &languageResolver) + response, err := usecase.Execute(&Request{}) elementsRepository.AssertNotCalled(t, "GetByVenues") - articlesRepository.AssertNotCalled(t, "GetByUUIDs") + articlesRepository.AssertNotCalled(t, "GetByCorrelationUUIDs") userRepository.AssertNotCalled(t, "GetByUUIDs") assert.Nil(t, response, "unexpected response") @@ -155,6 +172,7 @@ func TestUseCase_Execute(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver a = []article.Article{ {UUID: "test-article-1", AuthorUUID: "author-uuid-1"}, @@ -163,18 +181,22 @@ func TestUseCase_Execute(t *testing.T) { expectedErr = errors.New("some error") ) - articlesRepository.On("GetMostViewed", uint(4)).Once().Return(a, nil) - articlesRepository.On("GetAllPublished", uint(0), uint(3)).Once().Return(a, nil) + articlesRepository.On("GetMostViewed", "EN", uint(4)).Once().Return(a, nil) + articlesRepository.On("GetAllPublished", "EN", uint(0), uint(3)).Once().Return(a, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetByUUIDs", []string{"author-uuid-1", "author-uuid-1"}).Once().Return(nil, expectedErr) defer userRepository.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - usecase := NewUseCase(&articlesRepository, &userRepository, elementRetriever) - response, err := usecase.Execute() + usecase := NewUseCase(&articlesRepository, &userRepository, elementRetriever, &languageResolver) + response, err := usecase.Execute(&Request{}) - articlesRepository.AssertNotCalled(t, "GetByUUIDs") + articlesRepository.AssertNotCalled(t, "GetByCorrelationUUIDs") elementsRepository.AssertNotCalled(t, "GetByVenues") assert.Nil(t, response, "unexpected response") @@ -188,6 +210,7 @@ func TestUseCase_Execute(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver a = []article.Article{ {UUID: "test-article-1", AuthorUUID: "author-uuid-1"}, @@ -199,8 +222,8 @@ func TestUseCase_Execute(t *testing.T) { expectedErr = errors.New("some error") ) - articlesRepository.On("GetMostViewed", uint(4)).Once().Return(a, nil) - articlesRepository.On("GetAllPublished", uint(0), uint(3)).Return(a, nil) + articlesRepository.On("GetMostViewed", "EN", uint(4)).Once().Return(a, nil) + articlesRepository.On("GetAllPublished", "EN", uint(0), uint(3)).Return(a, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetByUUIDs", []string{"author-uuid-1", "author-uuid-1"}).Once().Return(u, nil) @@ -209,11 +232,15 @@ func TestUseCase_Execute(t *testing.T) { elementsRepository.On("GetByVenues", []string{"home"}).Once().Return(nil, expectedErr) defer elementsRepository.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - usecase := NewUseCase(&articlesRepository, &userRepository, elementRetriever) - response, err := usecase.Execute() + usecase := NewUseCase(&articlesRepository, &userRepository, elementRetriever, &languageResolver) + response, err := usecase.Execute(&Request{}) - articlesRepository.AssertNotCalled(t, "GetByUUIDs") + articlesRepository.AssertNotCalled(t, "GetByCorrelationUUIDs") assert.Nil(t, response, "unexpected response") assert.ErrorIs(t, err, expectedErr) @@ -226,6 +253,7 @@ func TestUseCase_Execute(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver mockComponent component.MockComponent a = []article.Article{ @@ -261,9 +289,9 @@ func TestUseCase_Execute(t *testing.T) { expectedErr = errors.New("some error") ) - articlesRepository.On("GetMostViewed", uint(4)).Once().Return(a, nil) - articlesRepository.On("GetAllPublished", uint(0), uint(3)).Once().Return(a, nil) - articlesRepository.On("GetByUUIDs", articleUUIDs).Once().Return(nil, expectedErr) + articlesRepository.On("GetMostViewed", "EN", uint(4)).Once().Return(a, nil) + articlesRepository.On("GetAllPublished", "EN", uint(0), uint(3)).Once().Return(a, nil) + articlesRepository.On("GetByCorrelationUUIDs", articleUUIDs, "EN").Once().Return(nil, expectedErr) defer articlesRepository.AssertExpectations(t) userRepository.On("GetByUUIDs", homeAuthorUUIDs).Once().Return(u, nil) @@ -279,9 +307,13 @@ func TestUseCase_Execute(t *testing.T) { elementsRepository.On("GetByVenues", []string{"home"}).Once().Return(v, nil) defer elementsRepository.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - usecase := NewUseCase(&articlesRepository, &userRepository, elementRetriever) - response, err := usecase.Execute() + usecase := NewUseCase(&articlesRepository, &userRepository, elementRetriever, &languageResolver) + response, err := usecase.Execute(&Request{}) assert.Nil(t, response, "unexpected response") assert.ErrorIs(t, err, expectedErr) diff --git a/application/language/getLanguages/response.go b/application/language/getLanguages/response.go new file mode 100644 index 00000000..aef6468d --- /dev/null +++ b/application/language/getLanguages/response.go @@ -0,0 +1,30 @@ +package getlanguages + +import "github.com/khanzadimahdi/testproject/domain/language" + +type Response struct { + Items []languageResponse `json:"items"` + DefaultLanguage languageResponse `json:"default_language"` +} + +type languageResponse struct { + Code string `json:"code"` + Name string `json:"name"` +} + +func NewResponse(l []language.Language, defaultLanguage language.Language) *Response { + items := make([]languageResponse, len(l)) + + for i := range l { + items[i].Code = l[i].Code + items[i].Name = l[i].Name + } + + return &Response{ + Items: items, + DefaultLanguage: languageResponse{ + Code: defaultLanguage.Code, + Name: defaultLanguage.Name, + }, + } +} diff --git a/application/language/getLanguages/usecase.go b/application/language/getLanguages/usecase.go new file mode 100644 index 00000000..b1602b1f --- /dev/null +++ b/application/language/getLanguages/usecase.go @@ -0,0 +1,42 @@ +package getlanguages + +import ( + "github.com/khanzadimahdi/testproject/application/language/resolver" + "github.com/khanzadimahdi/testproject/domain/language" +) + +type UseCase struct { + languageRepository language.Repository + languageResolver resolver.Resolver +} + +func NewUseCase(languageRepository language.Repository, languageResolver resolver.Resolver) *UseCase { + return &UseCase{ + languageRepository: languageRepository, + languageResolver: languageResolver, + } +} + +func (uc *UseCase) Execute() (*Response, error) { + total, err := uc.languageRepository.Count() + if err != nil { + return nil, err + } + + languages, err := uc.languageRepository.GetAll(0, total) + if err != nil { + return nil, err + } + + defaultCode, err := uc.languageResolver.DefaultCode() + if err != nil { + return nil, err + } + + defaultLanguage, err := uc.languageResolver.Resolve(defaultCode) + if err != nil { + return nil, err + } + + return NewResponse(languages, defaultLanguage), nil +} diff --git a/application/language/getLanguages/usecase_test.go b/application/language/getLanguages/usecase_test.go new file mode 100644 index 00000000..3c42ba6d --- /dev/null +++ b/application/language/getLanguages/usecase_test.go @@ -0,0 +1,153 @@ +package getlanguages + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/khanzadimahdi/testproject/application/language/resolver" + "github.com/khanzadimahdi/testproject/domain/language" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" +) + +func TestUseCase_Execute(t *testing.T) { + t.Parallel() + + t.Run("returns all languages", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + languageResolver resolver.MockResolver + + l = []language.Language{ + {Code: "EN", Name: "English"}, + {Code: "FA", Name: "فارسی"}, + } + + defaultLanguage = l[0] + + expectedResponse = Response{ + Items: []languageResponse{ + {Code: l[0].Code, Name: l[0].Name}, + {Code: l[1].Code, Name: l[1].Name}, + }, + DefaultLanguage: languageResponse{Code: defaultLanguage.Code, Name: defaultLanguage.Name}, + } + ) + + languageRepository.On("Count").Once().Return(uint(len(l)), nil) + languageRepository.On("GetAll", uint(0), uint(len(l))).Once().Return(l, nil) + defer languageRepository.AssertExpectations(t) + + languageResolver.On("DefaultCode").Once().Return(defaultLanguage.Code, nil) + languageResolver.On("Resolve", defaultLanguage.Code).Once().Return(defaultLanguage, nil) + defer languageResolver.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository, &languageResolver).Execute() + + assert.NoError(t, err) + assert.Equal(t, &expectedResponse, response) + }) + + t.Run("counting languages fails", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + languageResolver resolver.MockResolver + + expectedError = errors.New("counting failed") + ) + + languageRepository.On("Count").Once().Return(uint(0), expectedError) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository, &languageResolver).Execute() + + languageRepository.AssertNotCalled(t, "GetAll") + languageResolver.AssertNotCalled(t, "DefaultCode") + languageResolver.AssertNotCalled(t, "Resolve") + assert.Nil(t, response) + assert.ErrorIs(t, err, expectedError) + }) + + t.Run("getting languages fails", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + languageResolver resolver.MockResolver + + expectedError = errors.New("getting failed") + ) + + languageRepository.On("Count").Once().Return(uint(2), nil) + languageRepository.On("GetAll", uint(0), uint(2)).Once().Return(nil, expectedError) + defer languageRepository.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository, &languageResolver).Execute() + + languageResolver.AssertNotCalled(t, "DefaultCode") + languageResolver.AssertNotCalled(t, "Resolve") + assert.Nil(t, response) + assert.ErrorIs(t, err, expectedError) + }) + + t.Run("getting default language code fails", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + languageResolver resolver.MockResolver + + l = []language.Language{ + {Code: "EN", Name: "English"}, + } + + expectedError = errors.New("resolving default code failed") + ) + + languageRepository.On("Count").Once().Return(uint(len(l)), nil) + languageRepository.On("GetAll", uint(0), uint(len(l))).Once().Return(l, nil) + defer languageRepository.AssertExpectations(t) + + languageResolver.On("DefaultCode").Once().Return("", expectedError) + defer languageResolver.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository, &languageResolver).Execute() + + languageResolver.AssertNotCalled(t, "Resolve") + assert.Nil(t, response) + assert.ErrorIs(t, err, expectedError) + }) + + t.Run("resolving default language fails", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + languageResolver resolver.MockResolver + + l = []language.Language{ + {Code: "EN", Name: "English"}, + } + + expectedError = errors.New("resolving default language failed") + ) + + languageRepository.On("Count").Once().Return(uint(len(l)), nil) + languageRepository.On("GetAll", uint(0), uint(len(l))).Once().Return(l, nil) + defer languageRepository.AssertExpectations(t) + + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{}, expectedError) + defer languageResolver.AssertExpectations(t) + + response, err := NewUseCase(&languageRepository, &languageResolver).Execute() + + assert.Nil(t, response) + assert.ErrorIs(t, err, expectedError) + }) +} diff --git a/application/language/resolver/mock.go b/application/language/resolver/mock.go new file mode 100644 index 00000000..1112167c --- /dev/null +++ b/application/language/resolver/mock.go @@ -0,0 +1,31 @@ +package resolver + +import ( + "github.com/stretchr/testify/mock" + + "github.com/khanzadimahdi/testproject/domain/language" +) + +type MockResolver struct { + mock.Mock +} + +var _ Resolver = &MockResolver{} + +func (m *MockResolver) DefaultCode() (string, error) { + args := m.Called() + + return args.String(0), args.Error(1) +} + +func (m *MockResolver) Resolve(requestedCode string) (language.Language, error) { + args := m.Called(requestedCode) + + return args.Get(0).(language.Language), args.Error(1) +} + +func (m *MockResolver) Verify(requestedCode string) bool { + args := m.Called(requestedCode) + + return args.Bool(0) +} diff --git a/application/language/resolver/resolver.go b/application/language/resolver/resolver.go new file mode 100644 index 00000000..e85e3443 --- /dev/null +++ b/application/language/resolver/resolver.go @@ -0,0 +1,63 @@ +package resolver + +import ( + "github.com/khanzadimahdi/testproject/domain" + "github.com/khanzadimahdi/testproject/domain/config" + "github.com/khanzadimahdi/testproject/domain/language" +) + +// Resolver resolves a language code to a language. +// +// DefaultCode returns the site's default language code (from config). Callers +// that accept an empty code are responsible for substituting it with +// DefaultCode before calling Resolve, so the substitution stays explicit. +// +// Read (GET) requests use Resolve, which fetches a language without verifying +// it against the existing ones. Write (POST/PATCH/PUT) requests use Verify to +// make sure a language actually exists before persisting it. +type Resolver interface { + DefaultCode() (string, error) + Resolve(requestedCode string) (language.Language, error) + Verify(requestedCode string) bool +} + +type LanguageResolver struct { + languageRepository language.Repository + configRepository config.Repository +} + +var _ Resolver = &LanguageResolver{} + +func New(languageRepository language.Repository, configRepository config.Repository) *LanguageResolver { + return &LanguageResolver{ + languageRepository: languageRepository, + configRepository: configRepository, + } +} + +// DefaultCode returns the site's default language code loaded from config (DB), +func (r *LanguageResolver) DefaultCode() (string, error) { + c, err := r.configRepository.GetLatestRevision() + if err == nil && len(c.DefaultLanguageCode) > 0 { + return c.DefaultLanguageCode, nil + } + + // fallback to first language in the respository + // incase config is not set or default code is empty + languages, err := r.languageRepository.GetAll(0, 1) + if err == nil && len(languages) > 0 { + return languages[0].Code, nil + } + + return "", domain.ErrNotExists +} + +// Resolve fetches the language for the given code. +func (r *LanguageResolver) Resolve(requestedCode string) (language.Language, error) { + return r.languageRepository.GetOne(requestedCode) +} + +// Verify reports whether a requested language code is acceptable. +func (r *LanguageResolver) Verify(requestedCode string) bool { + return r.languageRepository.Exists(requestedCode) +} diff --git a/application/language/resolver/resolver_test.go b/application/language/resolver/resolver_test.go new file mode 100644 index 00000000..fa51da3c --- /dev/null +++ b/application/language/resolver/resolver_test.go @@ -0,0 +1,177 @@ +package resolver + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/khanzadimahdi/testproject/domain" + "github.com/khanzadimahdi/testproject/domain/config" + "github.com/khanzadimahdi/testproject/domain/language" + configMocks "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/config" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" +) + +func TestResolver_DefaultCode(t *testing.T) { + t.Parallel() + + t.Run("returns the configured default", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + configRepository configMocks.MockConfigRepository + ) + + configRepository.On("GetLatestRevision").Once().Return(config.Config{DefaultLanguageCode: "FA"}, nil) + defer configRepository.AssertExpectations(t) + + code, err := New(&languageRepository, &configRepository).DefaultCode() + + languageRepository.AssertNotCalled(t, "GetOne") + languageRepository.AssertNotCalled(t, "GetAll") + assert.NoError(t, err) + assert.Equal(t, "FA", code) + }) + + t.Run("falls back to the first language when no default is configured", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + configRepository configMocks.MockConfigRepository + ) + + configRepository.On("GetLatestRevision").Once().Return(config.Config{}, domain.ErrNotExists) + defer configRepository.AssertExpectations(t) + + languageRepository.On("GetAll", uint(0), uint(1)).Once().Return([]language.Language{ + {Code: "EN", Name: "English"}, + {Code: "FA", Name: "فارسی"}, + }, nil) + defer languageRepository.AssertExpectations(t) + + code, err := New(&languageRepository, &configRepository).DefaultCode() + + assert.NoError(t, err) + assert.Equal(t, "EN", code) + }) + + t.Run("errors when no default is configured and no languages exist", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + configRepository configMocks.MockConfigRepository + ) + + configRepository.On("GetLatestRevision").Once().Return(config.Config{}, domain.ErrNotExists) + defer configRepository.AssertExpectations(t) + + languageRepository.On("GetAll", uint(0), uint(1)).Once().Return([]language.Language{}, nil) + defer languageRepository.AssertExpectations(t) + + code, err := New(&languageRepository, &configRepository).DefaultCode() + + assert.ErrorIs(t, err, domain.ErrNotExists) + assert.Empty(t, code) + }) +} + +func TestResolver_Resolve(t *testing.T) { + t.Parallel() + + t.Run("fetches the requested language without substituting a default", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + configRepository configMocks.MockConfigRepository + ) + + languageRepository.On("GetOne", "FA").Once().Return(language.Language{Code: "FA", Name: "فارسی"}, nil) + defer languageRepository.AssertExpectations(t) + + lang, err := New(&languageRepository, &configRepository).Resolve("FA") + + languageRepository.AssertNotCalled(t, "Exists") + configRepository.AssertNotCalled(t, "GetLatestRevision") + assert.NoError(t, err) + assert.Equal(t, "FA", lang.Code) + }) + + t.Run("propagates not-found errors", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + configRepository configMocks.MockConfigRepository + ) + + languageRepository.On("GetOne", "DE").Once().Return(language.Language{}, domain.ErrNotExists) + defer languageRepository.AssertExpectations(t) + + _, err := New(&languageRepository, &configRepository).Resolve("DE") + + assert.ErrorIs(t, err, domain.ErrNotExists) + }) + + t.Run("propagates unexpected errors", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + configRepository configMocks.MockConfigRepository + + expectedErr = errors.New("db error") + ) + + languageRepository.On("GetOne", "EN").Once().Return(language.Language{}, expectedErr) + defer languageRepository.AssertExpectations(t) + + _, err := New(&languageRepository, &configRepository).Resolve("EN") + + assert.ErrorIs(t, err, expectedErr) + }) +} + +func TestResolver_Verify(t *testing.T) { + t.Parallel() + + t.Run("acceptable when the language exists", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + configRepository configMocks.MockConfigRepository + ) + + languageRepository.On("Exists", "FA").Once().Return(true) + defer languageRepository.AssertExpectations(t) + + valid := New(&languageRepository, &configRepository).Verify("FA") + + languageRepository.AssertNotCalled(t, "GetOne") + configRepository.AssertNotCalled(t, "GetLatestRevision") + assert.True(t, valid) + }) + + t.Run("not acceptable when the language does not exist", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + configRepository configMocks.MockConfigRepository + ) + + languageRepository.On("Exists", "DE").Once().Return(false) + defer languageRepository.AssertExpectations(t) + + valid := New(&languageRepository, &configRepository).Verify("DE") + + languageRepository.AssertNotCalled(t, "GetOne") + configRepository.AssertNotCalled(t, "GetLatestRevision") + assert.False(t, valid) + }) +} diff --git a/domain/article/article.go b/domain/article/article.go index 71abe4b7..71d7aef9 100644 --- a/domain/article/article.go +++ b/domain/article/article.go @@ -2,34 +2,40 @@ package article import ( "time" + + "github.com/khanzadimahdi/testproject/domain/language" ) type Article struct { - UUID string - Cover string - Video string - Title string - Excerpt string - Body string - PublishedAt time.Time - AuthorUUID string - Tags []string - ViewCount uint + UUID string + Cover string + Video string + Title string + Excerpt string + Body string + PublishedAt time.Time + AuthorUUID string + Tags []string + ViewCount uint + LanguageCode string + CorrelationUUID string } type Repository interface { GetAll(offset uint, limit uint) ([]Article, error) - GetAllPublished(offset uint, limit uint) ([]Article, error) + GetAllPublished(languageCode string, offset uint, limit uint) ([]Article, error) GetOne(UUID string) (Article, error) - GetOnePublished(UUID string) (Article, error) - GetByUUIDs(UUIDs []string) ([]Article, error) - GetMostViewed(limit uint) ([]Article, error) - CountPublishedByHashtags(hashtags []string) (uint, error) - GetPublishedByHashtags(hashtags []string, offset uint, limit uint) ([]Article, error) - CountPublishedByAuthor(authorUUID string) (uint, error) - GetPublishedByAuthor(authorUUID string, offset uint, limit uint) ([]Article, error) + GetOnePublished(correlationUUID string, languageCode string) (Article, error) + GetByCorrelationUUIDs(correlationUUIDs []string, languageCode string) ([]Article, error) + GetPublishedLanguages(correlationUUID string) ([]language.Language, error) + GetMostViewed(languageCode string, limit uint) ([]Article, error) + CountPublishedByHashtags(hashtags []string, languageCode string) (uint, error) + GetPublishedByHashtags(hashtags []string, languageCode string, offset uint, limit uint) ([]Article, error) + CountPublishedByAuthor(authorUUID string, languageCode string) (uint, error) + GetPublishedByAuthor(authorUUID string, languageCode string, offset uint, limit uint) ([]Article, error) Count() (uint, error) - CountPublished() (uint, error) + CountPublished(languageCode string) (uint, error) + CorrelationExist(correlationUUID string) (bool, error) Save(*Article) (string, error) Delete(UUID string) error IncreaseView(uuid string, inc uint) error diff --git a/domain/author/author.go b/domain/author/author.go deleted file mode 100644 index d3fa67e9..00000000 --- a/domain/author/author.go +++ /dev/null @@ -1,12 +0,0 @@ -package author - -type Author struct { - UUID string - Name string - Avatar string - Username string -} - -type Repository interface { - GetOne(UUID string) (Author, error) -} diff --git a/domain/bookmark/bookmark.go b/domain/bookmark/bookmark.go index 29fe5062..e9457373 100644 --- a/domain/bookmark/bookmark.go +++ b/domain/bookmark/bookmark.go @@ -19,7 +19,6 @@ type Bookmark struct { type Repository interface { Save(*Bookmark) (string, error) - Count(objectType string, objectUUID string) (uint, error) GetAllByOwnerUUID(ownerUUID string, offset uint, limit uint) ([]Bookmark, error) CountByOwnerUUID(ownerUUID string) (uint, error) diff --git a/domain/config/config.go b/domain/config/config.go index 6d9a46b4..decc69fe 100644 --- a/domain/config/config.go +++ b/domain/config/config.go @@ -3,6 +3,7 @@ package config type Config struct { Revision uint // to keep trace of config changes UserDefaultRoleUUIDs []string + DefaultLanguageCode string } type Repository interface { diff --git a/domain/element/element.go b/domain/element/element.go index ca72b5a7..bfff06d9 100644 --- a/domain/element/element.go +++ b/domain/element/element.go @@ -28,7 +28,7 @@ type Element struct { // Repository represents a repository of elements. type Repository interface { GetAll(offset uint, limit uint) ([]Element, error) - GetByVenues(Venues []string) ([]Element, error) + GetByVenues(venues []string) ([]Element, error) GetOne(UUID string) (Element, error) Count() (uint, error) Save(*Element) (string, error) diff --git a/domain/language/language.go b/domain/language/language.go new file mode 100644 index 00000000..0f174bd5 --- /dev/null +++ b/domain/language/language.go @@ -0,0 +1,15 @@ +package language + +type Language struct { + Code string + Name string +} + +type Repository interface { + GetAll(offset uint, limit uint) ([]Language, error) + GetOne(code string) (Language, error) + Exists(code string) bool + Save(*Language) (string, error) + Delete(code string) error + Count() (uint, error) +} diff --git a/domain/permission/permission.go b/domain/permission/permission.go index 43a3cce2..1a9e92a8 100644 --- a/domain/permission/permission.go +++ b/domain/permission/permission.go @@ -7,7 +7,6 @@ type Permission struct { type Repository interface { GetAll() []Permission - GetOne(value string) (Permission, error) Get(values []string) ([]Permission, error) } @@ -53,6 +52,12 @@ const ( ConfigShow = "config.show" ConfigUpdate = "config.update" + + LanguagesIndex = "languages.index" + LanguagesCreate = "languages.create" + LanguagesShow = "languages.show" + LanguagesUpdate = "languages.update" + LanguagesDelete = "languages.delete" ) // user's self related accesses diff --git a/domain/runner/node/node.go b/domain/runner/node/node.go index 68d9d52f..f85b4d01 100644 --- a/domain/runner/node/node.go +++ b/domain/runner/node/node.go @@ -35,6 +35,5 @@ type Repository interface { GetAll(offset uint, limit uint) ([]Node, error) GetOne(name string) (Node, error) Save(*Node) (string, error) - Delete(name string) error Count() (uint, error) } diff --git a/domain/user/user.go b/domain/user/user.go index fbcfe009..85bd080a 100644 --- a/domain/user/user.go +++ b/domain/user/user.go @@ -13,6 +13,7 @@ type User struct { Avatar string Email string Username string + LanguageCode string PasswordHash password.Hash CreatedAt time.Time } diff --git a/go.mod b/go.mod index 19f785d1..833fc50f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/khanzadimahdi/testproject -go 1.26 +go 1.26.0 require ( github.com/docker/docker v28.5.2+incompatible @@ -11,7 +11,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/gorilla/websocket v1.5.3 github.com/hashicorp/golang-lru/v2 v2.0.7 - github.com/minio/minio-go/v7 v7.1.0 + github.com/minio/minio-go/v7 v7.2.0 github.com/nats-io/nats.go v1.52.0 github.com/sethvargo/go-limiter v1.1.0 github.com/stretchr/testify v1.11.1 @@ -19,7 +19,7 @@ require ( github.com/swaggo/swag v1.16.6 go.mongodb.org/mongo-driver/v2 v2.6.0 go.uber.org/goleak v1.3.0 - golang.org/x/crypto v0.51.0 + golang.org/x/crypto v0.53.0 ) require ( @@ -30,25 +30,24 @@ require ( github.com/HdrHistogram/hdrhistogram-go v1.2.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/air-verse/air v1.65.0 // indirect + github.com/air-verse/air v1.65.3 // indirect github.com/andybalholm/brotli v1.2.1 // indirect github.com/antithesishq/antithesis-sdk-go v0.7.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bep/godartsass/v2 v2.5.0 // indirect github.com/bep/golibsass v1.2.0 // indirect - github.com/bits-and-blooms/bitset v1.24.4 // indirect + github.com/bits-and-blooms/bitset v1.24.5 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/choria-io/fisk v0.8.0 // indirect - github.com/choria-io/scaffold v0.0.10 // indirect + github.com/choria-io/fisk v0.8.3 // indirect + github.com/choria-io/scaffold v0.0.11 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/creack/pty v1.1.24 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -57,40 +56,39 @@ require ( github.com/expr-lang/expr v1.17.8 // indirect github.com/fatih/color v1.19.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/ghodss/yaml v1.0.0 // indirect - github.com/go-ini/ini v1.67.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.23.1 // indirect - github.com/go-openapi/jsonreference v0.21.5 // indirect - github.com/go-openapi/spec v0.22.4 // indirect - github.com/go-openapi/swag/conv v0.26.0 // indirect - github.com/go-openapi/swag/jsonname v0.26.0 // indirect - github.com/go-openapi/swag/jsonutils v0.26.0 // indirect - github.com/go-openapi/swag/loading v0.26.0 // indirect - github.com/go-openapi/swag/stringutils v0.26.0 // indirect - github.com/go-openapi/swag/typeutils v0.26.0 // indirect - github.com/go-openapi/swag/yamlutils v0.26.0 // indirect + github.com/go-openapi/jsonreference v0.21.6 // indirect + github.com/go-openapi/spec v0.22.5 // indirect + github.com/go-openapi/swag/conv v0.26.1 // indirect + github.com/go-openapi/swag/jsonname v0.26.1 // indirect + github.com/go-openapi/swag/jsonutils v0.26.1 // indirect + github.com/go-openapi/swag/loading v0.26.1 // indirect + github.com/go-openapi/swag/stringutils v0.26.1 // indirect + github.com/go-openapi/swag/typeutils v0.26.1 // indirect + github.com/go-openapi/swag/yamlutils v0.26.1 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gohugoio/hashstructure v0.6.0 // indirect - github.com/gohugoio/hugo v0.160.0 // indirect + github.com/gohugoio/hugo v0.163.0 // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gosuri/uilive v0.0.4 // indirect github.com/gosuri/uiprogress v0.0.1 // indirect github.com/huandu/xstrings v1.5.0 // indirect - github.com/jedib0t/go-pretty/v6 v6.7.9 // indirect + github.com/jedib0t/go-pretty/v6 v6.8.0 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/crc32 v1.3.0 // indirect github.com/mailru/easyjson v0.9.1 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.22 // indirect + github.com/mattn/go-colorable v0.1.15 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mattn/go-runewidth v0.0.24 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/highwayhash v1.0.4 // indirect @@ -103,24 +101,24 @@ require ( github.com/moby/term v0.5.2 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nats-io/jsm.go v0.3.1-0.20260116154816-a772222ebcf0 // indirect - github.com/nats-io/jwt/v2 v2.8.1 // indirect - github.com/nats-io/nats-server/v2 v2.12.6 // indirect - github.com/nats-io/natscli v0.3.2 // indirect - github.com/nats-io/nkeys v0.4.15 // indirect - github.com/nats-io/nsc/v2 v2.12.2 // indirect + github.com/nats-io/jsm.go v0.4.1 // indirect + github.com/nats-io/jwt/v2 v2.8.2 // indirect + github.com/nats-io/nats-server/v2 v2.14.2 // indirect + github.com/nats-io/natscli v0.4.0 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nsc/v2 v2.15.0 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/nsf/termbox-go v1.1.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.3.0 // indirect + github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/common v0.68.1 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -130,11 +128,11 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/swaggo/files/v2 v2.0.2 // indirect - github.com/synadia-io/jwt-auth-builder.go v0.0.9 // indirect + github.com/synadia-io/jwt-auth-builder.go v0.0.10 // indirect github.com/synadia-io/orbit.go/counters v0.1.1 // indirect - github.com/synadia-io/orbit.go/jetstreamext v0.2.1 // indirect + github.com/synadia-io/orbit.go/jetstreamext v0.3.1 // indirect github.com/synadia-io/orbit.go/natsext v0.1.2 // indirect - github.com/tdewolff/parse/v2 v2.8.11 // indirect + github.com/tdewolff/parse/v2 v2.8.13 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/tylertreat/hdrhistogram-writer v0.0.0-20210816161836-2e440612a39f // indirect github.com/urfave/cli/v2 v2.27.7 // indirect @@ -145,24 +143,25 @@ require ( github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect - go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect - golang.org/x/mod v0.36.0 // indirect - golang.org/x/net v0.54.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.44.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/exp v0.0.0-20260603202125-055de637280b // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.38.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.45.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/gizak/termui.v1 v1.0.0-20151021151108-e62b5929642a // indirect + gopkg.in/ini.v1 v1.67.3 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.2 // indirect diff --git a/go.sum b/go.sum index f7b12376..e974599b 100644 --- a/go.sum +++ b/go.sum @@ -18,25 +18,25 @@ github.com/HdrHistogram/hdrhistogram-go v1.2.0 h1:XMJkDWuz6bM9Fzy7zORuVFKH7ZJY41 github.com/HdrHistogram/hdrhistogram-go v1.2.0/go.mod h1:CiIeGiHSd06zjX+FypuEJ5EQ07KKtxZ+8J6hszwVQig= github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ= github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo= -github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0 h1:mklaPbT4f/EiDr1Q+zPrEt9lgKAkVrIBtWf33d9GpVA= -github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0/go.mod h1:D56Cl9r8M5i3UwAchE+LlLc5hPN3kJtdZNVJn06lSHU= +github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.1 h1:IpUgup6ucCE4wB59wAP0Y2qSApYjFhSfGVjShUBoVSw= +github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.1/go.mod h1:KUwy/WLgv9kv2yeBZkPCgDokHzg0M6EdRc17thnbVFw= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= -github.com/air-verse/air v1.65.0 h1:MCJTmXL+UBS1Ji05jsIaUBXokeW10W+Y6ODkx6GcfZk= -github.com/air-verse/air v1.65.0/go.mod h1:OaJZSfZqf7wyjS2oP/CcEVyIt0JmZuPh5x1gdtklmmY= +github.com/air-verse/air v1.65.3 h1:7NUAqofZuSf5vZbVoZpdTr/4LuKkOCj1JTiMrOdUs7A= +github.com/air-verse/air v1.65.3/go.mod h1:OaJZSfZqf7wyjS2oP/CcEVyIt0JmZuPh5x1gdtklmmY= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= -github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= +github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM= +github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/antithesishq/antithesis-sdk-go v0.7.0 h1:uWDG8BqLD1lI2ps38WDz2vXflrTX2+vLX0SvZtztJtE= @@ -57,14 +57,14 @@ github.com/bep/godartsass/v2 v2.5.0 h1:tKRvwVdyjCIr48qgtLa4gHEdtRkPF8H1OeEhJAEv7 github.com/bep/godartsass/v2 v2.5.0/go.mod h1:rjsi1YSXAl/UbsGL85RLDEjRKdIKUlMQHr6ChUNYOFU= github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI= github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= -github.com/bep/golocales v0.1.0 h1:rjWf1S4basIje+G+je5WMW8G+yzaoz4gEDFolrFVdvA= -github.com/bep/golocales v0.1.0/go.mod h1:Hl78nje8mNL3LzLeJvYN9NsIZgyFJGrGfvgO9r1+mwE= -github.com/bep/goportabletext v0.1.0 h1:8dqym2So1cEqVZiBa4ZnMM1R9l/DnC1h4ONg4J5kujw= -github.com/bep/goportabletext v0.1.0/go.mod h1:6lzSTsSue75bbcyvVc0zqd1CdApuT+xkZQ6Re5DzZFg= -github.com/bep/helpers v0.8.0 h1:plg2BFgA9AgIHF2XemyZdZLqixjzQk3uyyArV48FngQ= -github.com/bep/helpers v0.8.0/go.mod h1:PfE7MGdA8sSQ19nyDh4tYbs5rAlStlJaDI21f/fnNps= -github.com/bep/imagemeta v0.17.0 h1:0sCIQTcmERGUCazrBfmoeh7SoHutlYQqLr24GCItTxA= -github.com/bep/imagemeta v0.17.0/go.mod h1:+Hlp195TfZpzsqCxtDKTG6eWdyz2+F2V/oCYfr3CZKA= +github.com/bep/golocales v0.2.0 h1:4H1H5UPw3ainpj5zykeEfiMRQngyaIC/t+I4Dvn+fvE= +github.com/bep/golocales v0.2.0/go.mod h1:Hl78nje8mNL3LzLeJvYN9NsIZgyFJGrGfvgO9r1+mwE= +github.com/bep/goportabletext v0.2.0 h1:CZ9f8jADBWqHwBymQiJJPCTSV/tHSA+PYzlUf86Yze0= +github.com/bep/goportabletext v0.2.0/go.mod h1:xDeA5+qcgKzJq6Q6XjAiBKtxLD3Yn7f6XP4joD3J3qU= +github.com/bep/helpers v0.12.0 h1:tD6V2DQW0B+FUynF2etR/106S/TO9akm+vA/Hk24GxY= +github.com/bep/helpers v0.12.0/go.mod h1:PfE7MGdA8sSQ19nyDh4tYbs5rAlStlJaDI21f/fnNps= +github.com/bep/imagemeta v0.17.2 h1:fDyXM1eAqCfBeqGLqS6UsN4OfuLM0cdu70KuLCehjOg= +github.com/bep/imagemeta v0.17.2/go.mod h1:+Hlp195TfZpzsqCxtDKTG6eWdyz2+F2V/oCYfr3CZKA= github.com/bep/lazycache v0.8.1 h1:ko6ASLjkPxyV5DMWoNNZ8B2M0weyjqXX8IZkjBoBtvg= github.com/bep/lazycache v0.8.1/go.mod h1:pbEiFsZoq7cLXvrTll0AHOPEurB1aGGxx4jKjOtlx9w= github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ= @@ -75,20 +75,20 @@ github.com/bep/textandbinarywriter v0.1.0 h1:KXmXsRN2Uhwhm1G3e/snM8+5SPQBJrCEpIo github.com/bep/textandbinarywriter v0.1.0/go.mod h1:dAcHveajlWWU7PXhp6Dn4PIAYDg2H13Huif9xMS2w8w= github.com/bep/tmc v0.6.0 h1:5zWy4L+3gS+Kk8czzLC4g7ETaC3wkX9ZtTRdAdL8V4s= github.com/bep/tmc v0.6.0/go.mod h1:SNHxc3o2WSNMAYqJcAO0rxFY+pbhZzMwjIHe5xaAue0= -github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= -github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.24.5 h1:654xBVHc23gJMAgOTkPNoCVfiRxuIOAUnAZFtopqJ4w= +github.com/bits-and-blooms/bitset v1.24.5/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/choria-io/fisk v0.8.0 h1:5Et8H6tcDMRBWByrUuWF9lYmnzX8KvfPAgmnS9VOHvo= -github.com/choria-io/fisk v0.8.0/go.mod h1:s4e6TqrkLX8k9IQY8t7OO7GnxEinroOSrE7ZN9ClAfA= -github.com/choria-io/scaffold v0.0.10 h1:iq+DdWqVzZuHPCEK97N/ICgqddJ91hTk394FJw2n4j4= -github.com/choria-io/scaffold v0.0.10/go.mod h1:WS2tPnvan7Waxzr9ubcdvPmxfiZ27Cij3GmF+T6stXs= +github.com/choria-io/fisk v0.8.3 h1:YMGnrC93em0p+9A+uOh1EnI2tpnBVvwPc5St4TkOiVM= +github.com/choria-io/fisk v0.8.3/go.mod h1:3pAWCPh73bUYoZ8k9fd0UbuWRlxEBayiKMAIQsiKtVo= +github.com/choria-io/scaffold v0.0.11 h1:/dkwuKmWR/C74ROLoxj9qHHvi6/PLo+0NnIVkxAhwW8= +github.com/choria-io/scaffold v0.0.11/go.mod h1:jj/LTtFCCn3en20nG6I7BOP9lVLgns3gPy3cAkeAIAY= github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= -github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= -github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -108,8 +108,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= -github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= @@ -120,8 +120,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/dot v1.11.0 h1:zsrhCuFHAJge/aZIC4N4LdHy5tqYu4tWEaUzIwdYj4Y= github.com/emicklei/dot v1.11.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= -github.com/evanw/esbuild v0.27.4 h1:8opEixKkH9EDsdjxC/aPmpk1KPwQOcyknDo5m5xIFxI= -github.com/evanw/esbuild v0.27.4/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/evanw/esbuild v0.28.0 h1:V96ghtc5p5JnNUQIUsc5H3kr+AcFcMqOJll2ZmJW6Lo= +github.com/evanw/esbuild v0.28.0/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM= github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= @@ -132,10 +132,10 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/ github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/getkin/kin-openapi v0.134.0 h1:/L5+1+kfe6dXh8Ot/wqiTgUkjOIEJiC0bbYVziHB8rU= -github.com/getkin/kin-openapi v0.134.0/go.mod h1:wK6ZLG/VgoETO9pcLJ/VmAtIcl/DNlMayNTb716EUxE= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/getkin/kin-openapi v0.139.0 h1:pBFXcZJFwz9J1X64jzxlOoNgFm+TF7kNrs9+HJVN6Ic= +github.com/getkin/kin-openapi v0.139.0/go.mod h1:NGxPfE4PwS/TRXEbyx2RrxDFPZvxcWw31Tw8XXjPZLs= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -146,31 +146,31 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= -github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= -github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= -github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= -github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= +github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= +github.com/go-openapi/spec v0.22.5 h1:KhO7RBlKQfonUWX2WzQCoLIXVA6AcNqDGZ3a1Dutdlo= +github.com/go-openapi/spec v0.22.5/go.mod h1:vxpOtMya5TXtENXKE5bKqv5NjocVhyhxHrlZfvKnZ74= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= -github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= -github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= -github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= -github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= -github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= -github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= -github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= -github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= -github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= -github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= -github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= -github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= -github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE= -github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= -github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/swag/conv v0.26.1 h1:slr5FVkg9Wc3Y5zcwenD8Sd/PQ94b2I/QJI7N7KTBpg= +github.com/go-openapi/swag/conv v0.26.1/go.mod h1:mvQXgPptZk9GTrFgGwWvT4q+dN+zQej9JfmGwnipz1A= +github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE= +github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc= +github.com/go-openapi/swag/jsonutils v0.26.1 h1:2hdBfFkHg+7Wrz2VsCbeyR6hzkRDs7AztnMR2u84yOY= +github.com/go-openapi/swag/jsonutils v0.26.1/go.mod h1:U+RMJH3wa+6BRiphuRtIyI8fW9HPFqFQ4sHk2oRx0UQ= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1 h1:1CD7NiLLb/TXl3tOnFYU4b+mNfb5rtgHkaA+q7RMYYQ= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1/go.mod h1:ZWafc8nMdYzTE3uYY6W86f0n46+IF0g4uUyRhJw/kXc= +github.com/go-openapi/swag/loading v0.26.1 h1:E9K4wqXeROlhjFQ13K9zMz6ojFGXIggGe+ad1odrK9w= +github.com/go-openapi/swag/loading v0.26.1/go.mod h1:3qvRIlWzWdq1HvmldwmuJ2ohpcAryN6xVt2OTKd0/7E= +github.com/go-openapi/swag/stringutils v0.26.1 h1:f88uYyTso7TnHrKM/bUBsQ5e2wKf37cpgo6pvbzd9yU= +github.com/go-openapi/swag/stringutils v0.26.1/go.mod h1:Sc6d3bU8fgk5AyZR8/8jEQ+Is/Ald+TD/IIggPN8UJk= +github.com/go-openapi/swag/typeutils v0.26.1 h1:yg42FgMzRR6PVQ3M3qHz1s+Y6/P4HoJ3cBarXa3OVnU= +github.com/go-openapi/swag/typeutils v0.26.1/go.mod h1:VfnV+oUtSP2vCSCn2aJgnr8OevUYemyIzzS1VOzS10o= +github.com/go-openapi/swag/yamlutils v0.26.1 h1:0TSLK+lXs9vfIhAWzBeI/lOzEnIoot6WTCO1aAeWFTk= +github.com/go-openapi/swag/yamlutils v0.26.1/go.mod h1:7W5b7PRX9MxwL7TjeG7H8HkyBGRsIDRObhyMWFgBI2M= +github.com/go-openapi/testify/enable/yaml/v2 v2.5.1 h1:q9NtHwK4qHF7yZziBPvZyv7zWAIk8ok88Gh2mR6Jpc8= +github.com/go-openapi/testify/enable/yaml/v2 v2.5.1/go.mod h1:JW0MXIotCYps/XsgJnG3a8Q7rE5xAiBwoOD5OfaIQBk= +github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0an6sBo= +github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= @@ -191,8 +191,8 @@ github.com/gohugoio/hashstructure v0.6.0 h1:7wMB/2CfXoThFYhdWRGv3u3rUM761Cq29CxU github.com/gohugoio/hashstructure v0.6.0/go.mod h1:lapVLk9XidheHG1IQ4ZSbyYrXcaILU1ZEP/+vno5rBQ= github.com/gohugoio/httpcache v0.8.0 h1:hNdsmGSELztetYCsPVgjA960zSa4dfEqqF/SficorCU= github.com/gohugoio/httpcache v0.8.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI= -github.com/gohugoio/hugo v0.160.0 h1:WmmygLg2ahijM4w2VHFn/DdBR+OpJ9H9pH3d8OApNDY= -github.com/gohugoio/hugo v0.160.0/go.mod h1:+VA5jOO3iGELh+6cig098PT2Cd9iNhwUPRqCUe3Ce7w= +github.com/gohugoio/hugo v0.163.0 h1:AO/K+CxBe10sBxDODvpjUNIY1x2n3SceugFF7l7TSZM= +github.com/gohugoio/hugo v0.163.0/go.mod h1:jamkaakWQKHc8uNrUCU6Gu5H9fbYrKb3B/Cg7CmH4XA= github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0 h1:I/n6v7VImJ3aISLnn73JAHXyjcQsMVvbguQPTk9Ehus= github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0/go.mod h1:9LJNfKWFmhEJ7HW0in5znezMwH+FYMBIhNZ3VWtRcRs= github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0 h1:p13Q0DBCrBRpJGtbtlgkYNCs4TnIlZJh8vHgnAiofrI= @@ -208,8 +208,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= -github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef h1:xpF9fUHpoIrrjX24DURVKiwHcFpw19ndIs+FwTSMbno= -github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -234,8 +234,8 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU= github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA= -github.com/jedib0t/go-pretty/v6 v6.7.9 h1:frarzQWmkZd97syT81+TH8INKPpzoxQnk+Mk5EIHSrM= -github.com/jedib0t/go-pretty/v6 v6.7.9/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.8.0 h1:fQOTjATVQl5RhssBro6ZuHANFybCkmJ7FjYPo4b7sEY= +github.com/jedib0t/go-pretty/v6 v6.8.0/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -243,8 +243,6 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -270,14 +268,14 @@ github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLi github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0= github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= +github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.22 h1:76lXsPn6FyHtTY+jt2fTTvsMUCZq1k0qwRsAMuxzKAk= -github.com/mattn/go-runewidth v0.0.22/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= +github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= @@ -289,8 +287,8 @@ github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clg github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8= -github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA= +github.com/minio/minio-go/v7 v7.2.0 h1:RCJM0R1XOsRs+A3x3UCaf3ZYbByDaLjFeAi+YCQEPhs= +github.com/minio/minio-go/v7 v7.2.0/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -315,22 +313,20 @@ github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6O github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/nats-io/jsm.go v0.3.1-0.20260116154816-a772222ebcf0 h1:LuzntHfAPnkbuP/VpTc5mYIpjMluAfKMlDeOFZVeH8U= -github.com/nats-io/jsm.go v0.3.1-0.20260116154816-a772222ebcf0/go.mod h1:nkGIXcioPyU76vxXSS23b4GPNuuhkPINwFFff8ISNUo= -github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= -github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= -github.com/nats-io/nats-server/v2 v2.12.6 h1:Egbx9Vl7Ch8wTtpXPGqbehkZ+IncKqShUxvrt1+Enc8= -github.com/nats-io/nats-server/v2 v2.12.6/go.mod h1:4HPlrvtmSO3yd7KcElDNMx9kv5EBJBnJJzQPptXlheo= -github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI= -github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= +github.com/nats-io/jsm.go v0.4.1 h1:hT0Ksd8Jk5wg9uZKiM5EMAJwSoC8zAu1ivPVB5UA9cI= +github.com/nats-io/jsm.go v0.4.1/go.mod h1:rWdrAnJSsCBjjeGbkSvMCB17oPTn+A5kXidixqn0M/E= +github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc= +github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc= +github.com/nats-io/nats-server/v2 v2.14.2 h1:Q7dRhCY03Y00rETFW3KV+KGaCIajlDfWgWUVgbMxyuk= +github.com/nats-io/nats-server/v2 v2.14.2/go.mod h1:lWpb1bSpRELZfRdlMkdz8E7lbXKKyNe8RIn0vvepIHs= github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc= github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= -github.com/nats-io/natscli v0.3.2 h1:/2++ZrlLWa5UCJeQH5HFn1qYue1cbPxIYDJAYyb8tXo= -github.com/nats-io/natscli v0.3.2/go.mod h1:COLrozATZQJxoitqLy8//FRhztHAByc2pZAGaVToC48= -github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= -github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= -github.com/nats-io/nsc/v2 v2.12.2 h1:M+85ZONoDo3PymX1hYMHBt4LG9McahfpGpMfVKXsToE= -github.com/nats-io/nsc/v2 v2.12.2/go.mod h1:suWy4bhAfZ5ferGkjuzqXFEPub7D+KqRKg0wTrrYc6s= +github.com/nats-io/natscli v0.4.0 h1:TVNhf1cV1xKS21OknR/7jBvO1PP4yynRqI9Si31AEk8= +github.com/nats-io/natscli v0.4.0/go.mod h1:6NPu8au6h6p/9z55H6cltGu1CpeznjCxcIj1tYPkxqo= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nsc/v2 v2.15.0 h1:3qOeZ7iX2q3Ae9GrsgE6Z7EgW2uYa0Tr4y97z7l2pKE= +github.com/nats-io/nsc/v2 v2.15.0/go.mod h1:eirMP9u+OoJ7g0iN6ymlNrMnwM1t6/Hn2qi924aCNMo= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -338,10 +334,10 @@ github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlM github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48= github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY= github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= -github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c h1:7ACFcSaQsrWtrH4WHHfUqE1C+f8r2uv8KGaW0jTNjus= -github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c/go.mod h1:JKox4Gszkxt57kj27u7rvi7IFoIULvCZHUsBTUmQM/s= -github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b h1:vivRhVUAa9t1q0Db4ZmezBP8pWQWnXHFokZj0AOea2g= -github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/oasdiff/yaml v0.1.0 h1:0bqZjfKc/8S9urj4JuwepX41WX9EoA6ifhU3SV06cXg= +github.com/oasdiff/yaml v0.1.0/go.mod h1:kOlRmMdL2X3vucLCEQO5u61SU22RysnfXvcttrZA1O0= +github.com/oasdiff/yaml3 v0.0.13 h1:06svmvOHOVBqF81+sY2EUScvUI/iS/vl2VIeUUxZQwg= +github.com/oasdiff/yaml3 v0.0.13/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= @@ -350,10 +346,10 @@ github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA= github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= -github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= -github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= -github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= -github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/onsi/ginkgo/v2 v2.28.3 h1:4JvMdwtFU0imd8fHx25OJXoDMRexnf8v5NHKYSTTji4= +github.com/onsi/ginkgo/v2 v2.28.3/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= +github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -362,8 +358,8 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2D github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= -github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= +github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= @@ -376,24 +372,26 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= -github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY= +github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= +github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sethvargo/go-limiter v1.1.0 h1:eLeZVQ2zqJOiEs03GguqmBVG6/T6lsZB+6PP1t7J6fA= github.com/sethvargo/go-limiter v1.1.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -417,22 +415,22 @@ github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSy github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= -github.com/synadia-io/jwt-auth-builder.go v0.0.9 h1:UmXQ0JsqiA7sboE0VC46YEqrGAFA4MYHnhw54uYKRi8= -github.com/synadia-io/jwt-auth-builder.go v0.0.9/go.mod h1:0XPmvG3TihNkiM3ZFVtcCAzv0oBgFtjnw/lb/h8NHs0= +github.com/synadia-io/jwt-auth-builder.go v0.0.10 h1:R8K8mzsLVcBd+P/ky1u/rXI5Egok9AZzTg/L8zPqsU0= +github.com/synadia-io/jwt-auth-builder.go v0.0.10/go.mod h1:lLRP0ZMBa0V1lh65o+roWYoc0qKl4UaGoISpeQDYtnY= github.com/synadia-io/orbit.go/counters v0.1.1 h1:PyQ8DQ6SOV+etUKSJqAO0DUz+f6hi4jO3Dp/eefN0yw= github.com/synadia-io/orbit.go/counters v0.1.1/go.mod h1:FHCC9bwoLWUhptK+cGiiyhl3kh5RnR+eiql1Y8SDykc= -github.com/synadia-io/orbit.go/jetstreamext v0.2.1 h1:Yt6XibijE5GIjaK5FZ2o+nsfqZFeJsp8dIdX8aMpeXs= -github.com/synadia-io/orbit.go/jetstreamext v0.2.1/go.mod h1:o9Xv8U3Obf4KfzM8YTD5C4ND9i0jYfT1qOeUa/7jWvM= +github.com/synadia-io/orbit.go/jetstreamext v0.3.1 h1:SuX48TR7k/CM1nqgdfn9NQEKOMSG+3t5RHVvZ5HRXq0= +github.com/synadia-io/orbit.go/jetstreamext v0.3.1/go.mod h1:7gIPymz00nuTtfOXh5BlO8AdWxnfXwVbv1rnzXfQwyc= github.com/synadia-io/orbit.go/natsext v0.1.2 h1:OVXqbV4W/UGnumv3iodczmq/EhcQMB16dAtjzWR7SYY= github.com/synadia-io/orbit.go/natsext v0.1.2/go.mod h1:eZpcii8ISOoT4mG52INXB3dc8Jc6ENkqLkWpTlmckIs= -github.com/tdewolff/minify/v2 v2.24.11 h1:JlANsiWaRBXedoYtsiZgY3YFkdr42oF32vp2SLgQKi4= -github.com/tdewolff/minify/v2 v2.24.11/go.mod h1:exq1pjdrh9uAICdfVKQwqz6MsJmWmQahZuTC6pTO6ro= -github.com/tdewolff/parse/v2 v2.8.11 h1:SGyjEy3xEqd+W9WVzTlTQ5GkP/en4a1AZNZVJ1cvgm0= -github.com/tdewolff/parse/v2 v2.8.11/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= -github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= -github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= -github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= -github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +github.com/tdewolff/minify/v2 v2.24.13 h1:xrcF7gKDnUszseEY9WX9mUlZII2v2Go/QAcAwRASw58= +github.com/tdewolff/minify/v2 v2.24.13/go.mod h1:emvwoYeIl8bfAKqRU5ww95LX9Gpggpqv/naal9a8Yq0= +github.com/tdewolff/parse/v2 v2.8.13 h1:si/8rLw5BZZTWCCiMm9A3f6x+RmqYfrkEeXCgpX5ick= +github.com/tdewolff/parse/v2 v2.8.13/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ= +github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= +github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= +github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= +github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tylertreat/hdrhistogram-writer v0.0.0-20210816161836-2e440612a39f h1:SGznmvCovewbaSgBsHgdThtWsLj5aCLX/3ZXMLd1UD0= @@ -466,22 +464,22 @@ go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/ go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -495,41 +493,35 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= -golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= +golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q= +golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= -golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= +golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo= +golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -538,27 +530,20 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -567,8 +552,6 @@ golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -580,13 +563,13 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -595,8 +578,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/gizak/termui.v1 v1.0.0-20151021151108-e62b5929642a h1:3xTF5ZxN6yR6SZlNMCN7z4X633Zi87YC8FkxFsdgdM0= gopkg.in/gizak/termui.v1 v1.0.0-20151021151108-e62b5929642a/go.mod h1:An8M7oA8GI+74WiR1qJo67ptrdlb7LJLqoXspnSrMUA= -gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= -gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw= +gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/infrastructure/ioc/providers/blog.go b/infrastructure/ioc/providers/blog.go index 7b5baa1a..e6ab6712 100644 --- a/infrastructure/ioc/providers/blog.go +++ b/infrastructure/ioc/providers/blog.go @@ -52,6 +52,11 @@ import ( dashboardGetFiles "github.com/khanzadimahdi/testproject/application/dashboard/file/getFiles" dashboardGetUserFiles "github.com/khanzadimahdi/testproject/application/dashboard/file/getUserFiles" dashboardUploadFile "github.com/khanzadimahdi/testproject/application/dashboard/file/uploadFile" + dashboardCreateLanguage "github.com/khanzadimahdi/testproject/application/dashboard/language/createLanguage" + dashboardDeleteLanguage "github.com/khanzadimahdi/testproject/application/dashboard/language/deleteLanguage" + dashboardGetLanguage "github.com/khanzadimahdi/testproject/application/dashboard/language/getLanguage" + dashboardGetLanguages "github.com/khanzadimahdi/testproject/application/dashboard/language/getLanguages" + dashboardUpdateLanguage "github.com/khanzadimahdi/testproject/application/dashboard/language/updateLanguage" dashboardGetPermissions "github.com/khanzadimahdi/testproject/application/dashboard/permission/getPermissions" "github.com/khanzadimahdi/testproject/application/dashboard/profile/changepassword" "github.com/khanzadimahdi/testproject/application/dashboard/profile/getRoles" @@ -71,6 +76,8 @@ import ( "github.com/khanzadimahdi/testproject/application/element" getFile "github.com/khanzadimahdi/testproject/application/file/getFile" "github.com/khanzadimahdi/testproject/application/home" + getLanguages "github.com/khanzadimahdi/testproject/application/language/getLanguages" + languageresolver "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/file" "github.com/khanzadimahdi/testproject/domain/password" @@ -88,6 +95,7 @@ import ( configrepository "github.com/khanzadimahdi/testproject/infrastructure/repository/mongodb/config" elementsrepository "github.com/khanzadimahdi/testproject/infrastructure/repository/mongodb/elements" filesrepository "github.com/khanzadimahdi/testproject/infrastructure/repository/mongodb/files" + languagesrepository "github.com/khanzadimahdi/testproject/infrastructure/repository/mongodb/languages" permissionsrepository "github.com/khanzadimahdi/testproject/infrastructure/repository/mongodb/permissions" rolesrepository "github.com/khanzadimahdi/testproject/infrastructure/repository/mongodb/roles" userrepository "github.com/khanzadimahdi/testproject/infrastructure/repository/mongodb/users" @@ -103,6 +111,7 @@ import ( dashboardConfigAPI "github.com/khanzadimahdi/testproject/presentation/http/blog/api/dashboard/config" dashboardElementAPI "github.com/khanzadimahdi/testproject/presentation/http/blog/api/dashboard/element" dashboardFileAPI "github.com/khanzadimahdi/testproject/presentation/http/blog/api/dashboard/file" + dashboardLanguageAPI "github.com/khanzadimahdi/testproject/presentation/http/blog/api/dashboard/language" dashboardPermissionAPI "github.com/khanzadimahdi/testproject/presentation/http/blog/api/dashboard/permission" "github.com/khanzadimahdi/testproject/presentation/http/blog/api/dashboard/profile" dashboardRoleAPI "github.com/khanzadimahdi/testproject/presentation/http/blog/api/dashboard/role" @@ -110,6 +119,7 @@ import ( fileAPI "github.com/khanzadimahdi/testproject/presentation/http/blog/api/file" hashtagAPI "github.com/khanzadimahdi/testproject/presentation/http/blog/api/hashtag" homeapi "github.com/khanzadimahdi/testproject/presentation/http/blog/api/home" + languageAPI "github.com/khanzadimahdi/testproject/presentation/http/blog/api/language" "github.com/khanzadimahdi/testproject/presentation/http/blog/openapi" "github.com/khanzadimahdi/testproject/presentation/http/middleware" "github.com/nats-io/nats.go" @@ -289,24 +299,27 @@ func blog( rolesRepository := rolesrepository.NewRepository(database) bookmarkRepository := bookmarksrepository.NewRepository(database) configRepository := configrepository.NewRepository(database) + languageRepository := languagesrepository.NewRepository(database) + languageResolver := languageresolver.New(languageRepository, configRepository) authTokenGenerator := auth.NewTokenGenerator(jwt, rolesRepository) elementRetriever := element.NewRetriever(articlesRepository, elementsRepository, userRepository) // ---- public ---- - homeUseCase := home.NewUseCase(articlesRepository, userRepository, elementRetriever) + homeUseCase := home.NewUseCase(articlesRepository, userRepository, elementRetriever, languageResolver) loginUseCase := login.NewUseCase(userRepository, authTokenGenerator, hasher, translator, validator) refreshUseCase := refresh.NewUseCase(userRepository, jwt, authTokenGenerator, translator, validator) forgetPasswordUseCase := forgetpassword.NewUseCase(userRepository, asyncProduceConsumer, translator, validator) resetPasswordUseCase := resetpassword.NewUseCase(userRepository, hasher, jwt, translator, validator) registerUseCase := register.NewUseCase(userRepository, asyncProduceConsumer, translator, validator) - verifyUseCase := verify.NewUseCase(userRepository, rolesRepository, configRepository, hasher, jwt, translator, validator) + verifyUseCase := verify.NewUseCase(userRepository, rolesRepository, configRepository, languageResolver, hasher, jwt, translator, validator) - getArticleUsecase := getArticle.NewUseCase(articlesRepository, userRepository, elementRetriever) - getArticlesUsecase := getArticles.NewUseCase(articlesRepository, userRepository) - getArticlesByHashtagUseCase := getArticlesByHashtag.NewUseCase(articlesRepository, userRepository, validator) - getArticlesByAuthorUseCase := getArticlesByAuthor.NewUseCase(articlesRepository, userRepository, validator) + getArticleUsecase := getArticle.NewUseCase(articlesRepository, userRepository, languageResolver, elementRetriever, validator) + getArticlesUsecase := getArticles.NewUseCase(articlesRepository, userRepository, languageResolver) + getArticlesByHashtagUseCase := getArticlesByHashtag.NewUseCase(articlesRepository, userRepository, languageResolver, validator) + getArticlesByAuthorUseCase := getArticlesByAuthor.NewUseCase(articlesRepository, userRepository, languageResolver, validator) + getLanguagesUseCase := getLanguages.NewUseCase(languageRepository, languageResolver) getFileUseCase := getFile.NewUseCase(filesRepository, fileStorage) getCommentsUseCase := getComments.NewUseCase(commentsRepository, userRepository, validator) createCommentUseCase := createComment.NewUseCase(commentsRepository, validator) @@ -323,15 +336,15 @@ func blog( // ---- dashboard ---- getProfileUseCase := getprofile.NewUseCase(userRepository) - updateProfileUseCase := updateprofile.NewUseCase(userRepository, validator, translator) + updateProfileUseCase := updateprofile.NewUseCase(userRepository, languageResolver, validator, translator) dashboardProfileChangePasswordUseCase := changepassword.NewUseCase(userRepository, hasher, validator, translator) dashboardProfileGetRolesUseCase := getRoles.NewUseCase(rolesRepository) - dashboardCreateArticleUsecase := dashboardCreateArticle.NewUseCase(articlesRepository, validator) + dashboardCreateArticleUsecase := dashboardCreateArticle.NewUseCase(articlesRepository, languageRepository, validator) dashboardDeleteArticleUsecase := dashboardDeleteArticle.NewUseCase(articlesRepository) dashboardGetArticleUsecase := dashboardGetArticle.NewUseCase(articlesRepository, userRepository) dashboardGetArticlesUsecase := dashboardGetArticles.NewUseCase(articlesRepository, userRepository) - dashboardUpdateArticleUsecase := dashboardUpdateArticle.NewUseCase(articlesRepository, validator) + dashboardUpdateArticleUsecase := dashboardUpdateArticle.NewUseCase(articlesRepository, languageRepository, validator) dashboardCreateCommentUsecase := dashboardCreateComment.NewUseCase(commentsRepository, validator) dashboardDeleteCommentUsecase := dashboardDeleteComment.NewUseCase(commentsRepository) @@ -362,6 +375,12 @@ func blog( dashboardGetRolesUsecase := dashboardGetRoles.NewUseCase(rolesRepository) dashboardUpdateRoleUsecase := dashboardUpdateRole.NewUseCase(rolesRepository, permissionRepository, validator, translator) + dashboardCreateLanguageUsecase := dashboardCreateLanguage.NewUseCase(languageRepository, validator) + dashboardDeleteLanguageUsecase := dashboardDeleteLanguage.NewUseCase(languageRepository) + dashboardGetLanguageUsecase := dashboardGetLanguage.NewUseCase(languageRepository) + dashboardGetLanguagesUsecase := dashboardGetLanguages.NewUseCase(languageRepository) + dashboardUpdateLanguageUsecase := dashboardUpdateLanguage.NewUseCase(languageRepository, validator) + dashboardGetFilesUseCase := dashboardGetFiles.NewUseCase(filesRepository) dashboardGetFileUseCase := dashboardGetFile.NewUseCase(filesRepository, fileStorage) dashboardUploadFileUseCase := dashboardUploadFile.NewUseCase(filesRepository, fileStorage, validator) @@ -377,7 +396,7 @@ func blog( dashboardUpdateElementUsecase := dashboardUpdateElement.NewUseCase(elementsRepository, validator) dashboardGetConfigUsecase := dashboardGetConfig.NewUseCase(configRepository) - dashboardUpdateConfigUsecase := dashboardUpdateConfig.NewUseCase(configRepository, validator) + dashboardUpdateConfigUsecase := dashboardUpdateConfig.NewUseCase(configRepository, languageRepository, validator) mux := http.NewServeMux() @@ -412,6 +431,9 @@ func blog( mux.Handle("POST /api/bookmarks/exists", middleware.NewAuthenticateMiddleware(bookmarkAPI.NewExistsHandler(bookmarkExistsUseCase), jwt, userRepository)) mux.Handle("PUT /api/bookmarks", middleware.NewAuthenticateMiddleware(bookmarkAPI.NewUpdateHandler(updateABookmark), jwt, userRepository)) + // languages + mux.Handle("GET /api/languages", middleware.NewCacheMiddleware(languageAPI.NewIndexHandler(getLanguagesUseCase), httpCache)) + // hashtags mux.Handle("GET /api/hashtags/{hashtag}", middleware.NewCacheMiddleware(hashtagAPI.NewShowHandler(getArticlesByHashtagUseCase), httpCache)) @@ -447,6 +469,13 @@ func blog( mux.Handle("GET /api/dashboard/roles/{uuid}", middleware.NewAuthenticateMiddleware(middleware.NewAuthorizeMiddleware(dashboardRoleAPI.NewShowHandler(dashboardGetRoleUsecase), authorizer, permission.RolesShow), jwt, userRepository)) mux.Handle("PUT /api/dashboard/roles", middleware.NewAuthenticateMiddleware(middleware.NewAuthorizeMiddleware(dashboardRoleAPI.NewUpdateHandler(dashboardUpdateRoleUsecase), authorizer, permission.RolesUpdate), jwt, userRepository)) + // languages + mux.Handle("POST /api/dashboard/languages", middleware.NewAuthenticateMiddleware(middleware.NewAuthorizeMiddleware(dashboardLanguageAPI.NewCreateHandler(dashboardCreateLanguageUsecase), authorizer, permission.LanguagesCreate), jwt, userRepository)) + mux.Handle("DELETE /api/dashboard/languages/{code}", middleware.NewAuthenticateMiddleware(middleware.NewAuthorizeMiddleware(dashboardLanguageAPI.NewDeleteHandler(dashboardDeleteLanguageUsecase), authorizer, permission.LanguagesDelete), jwt, userRepository)) + mux.Handle("GET /api/dashboard/languages", middleware.NewAuthenticateMiddleware(middleware.NewAuthorizeMiddleware(dashboardLanguageAPI.NewIndexHandler(dashboardGetLanguagesUsecase), authorizer, permission.LanguagesIndex), jwt, userRepository)) + mux.Handle("GET /api/dashboard/languages/{code}", middleware.NewAuthenticateMiddleware(middleware.NewAuthorizeMiddleware(dashboardLanguageAPI.NewShowHandler(dashboardGetLanguageUsecase), authorizer, permission.LanguagesShow), jwt, userRepository)) + mux.Handle("PUT /api/dashboard/languages", middleware.NewAuthenticateMiddleware(middleware.NewAuthorizeMiddleware(dashboardLanguageAPI.NewUpdateHandler(dashboardUpdateLanguageUsecase), authorizer, permission.LanguagesUpdate), jwt, userRepository)) + // articles mux.Handle("POST /api/dashboard/articles", middleware.NewAuthenticateMiddleware(middleware.NewAuthorizeMiddleware(dashboardArticleAPI.NewCreateHandler(dashboardCreateArticleUsecase), authorizer, permission.ArticlesCreate), jwt, userRepository)) mux.Handle("DELETE /api/dashboard/articles/{uuid}", middleware.NewAuthenticateMiddleware(middleware.NewAuthorizeMiddleware(dashboardArticleAPI.NewDeleteHandler(dashboardDeleteArticleUsecase), authorizer, permission.ArticlesDelete), jwt, userRepository)) diff --git a/infrastructure/jwt/claims.go b/infrastructure/jwt/claims.go index 5e7af289..ae8dc3a9 100644 --- a/infrastructure/jwt/claims.go +++ b/infrastructure/jwt/claims.go @@ -48,6 +48,10 @@ func (b builder) SetPermissions(value []string) { b.Set("permissions", value) } +func (b builder) SetLanguage(code string) { + b.Set("lang", code) +} + func (c builder) Set(name string, value any) { c[name] = value } diff --git a/infrastructure/jwt/claims_test.go b/infrastructure/jwt/claims_test.go index 62a38d0e..a2f9b89b 100644 --- a/infrastructure/jwt/claims_test.go +++ b/infrastructure/jwt/claims_test.go @@ -22,8 +22,9 @@ func TestBuilder(t *testing.T) { "aud": []string{"test-audience-1", "test-audience-2"}, "exp": exp.Unix(), "nbf": nbf.Unix(), - "iat": iat.Unix(), - "jti": "test-id", + "iat": iat.Unix(), + "jti": "test-id", + "lang": "EN", } builder.SetIssuer(expectedClaims["iss"].(string)) @@ -33,6 +34,7 @@ func TestBuilder(t *testing.T) { builder.SetNotBefore(nbf) builder.SetIssuedAt(iat) builder.SetID(expectedClaims["jti"].(string)) + builder.SetLanguage(expectedClaims["lang"].(string)) claims := builder.Build() diff --git a/infrastructure/repository/memory/articles/repository.go b/infrastructure/repository/memory/articles/repository.go index bb78f019..a406d781 100644 --- a/infrastructure/repository/memory/articles/repository.go +++ b/infrastructure/repository/memory/articles/repository.go @@ -8,6 +8,7 @@ import ( "github.com/gofrs/uuid/v5" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" ) type ArticlesRepository struct { @@ -48,7 +49,7 @@ func (r *ArticlesRepository) GetAll(offset uint, limit uint) ([]article.Article, return a, nil } -func (r *ArticlesRepository) GetAllPublished(offset uint, limit uint) ([]article.Article, error) { +func (r *ArticlesRepository) GetAllPublished(languageCode string, offset uint, limit uint) ([]article.Article, error) { var ( a []article.Article i uint @@ -61,48 +62,108 @@ func (r *ArticlesRepository) GetAllPublished(offset uint, limit uint) ([]article return true } - if article := value.(article.Article); article.PublishedAt.Before(time.Now()) { - a = append(a, article) - j++ + article := value.(article.Article) + if !article.PublishedAt.Before(time.Now()) { + return true + } + if len(languageCode) > 0 && article.LanguageCode != languageCode { + return true } + a = append(a, article) + j++ + return j < limit }) return a, nil } -func (r *ArticlesRepository) GetByUUIDs(UUIDs []string) ([]article.Article, error) { - a := make([]article.Article, 0, len(UUIDs)) +func (r *ArticlesRepository) GetByCorrelationUUIDs(correlationUUIDs []string, languageCode string) ([]article.Article, error) { + a := make([]article.Article, 0, len(correlationUUIDs)) r.datastore.Range(func(key, value any) bool { - if v := value.(article.Article); slices.Contains(UUIDs, v.UUID) { - a = append(a, value.(article.Article)) + v := value.(article.Article) + if !slices.Contains(correlationUUIDs, v.CorrelationUUID) { + return true + } + if len(languageCode) > 0 && v.LanguageCode != languageCode { + return true } + a = append(a, v) + return true }) return a, nil } -func (r *ArticlesRepository) GetMostViewed(limit uint) ([]article.Article, error) { +func (r *ArticlesRepository) GetPublishedLanguages(correlationUUID string) ([]language.Language, error) { + if len(correlationUUID) == 0 { + return []language.Language{}, nil + } + + seen := make(map[string]struct{}) + languages := make([]language.Language, 0, 2) + + r.datastore.Range(func(_, value any) bool { + v := value.(article.Article) + if v.CorrelationUUID != correlationUUID { + return true + } + if v.PublishedAt.After(time.Now()) { + return true + } + if _, ok := seen[v.LanguageCode]; ok { + return true + } + + seen[v.LanguageCode] = struct{}{} + languages = append(languages, language.Language{Code: v.LanguageCode}) + + return true + }) + + return languages, nil +} + +func (r *ArticlesRepository) CorrelationExist(correlationUUID string) (bool, error) { + if len(correlationUUID) == 0 { + return false, nil + } + + exist := false + + r.datastore.Range(func(_, value any) bool { + if v := value.(article.Article); v.CorrelationUUID == correlationUUID { + exist = true + return false + } + + return true + }) + + return exist, nil +} + +func (r *ArticlesRepository) GetMostViewed(languageCode string, limit uint) ([]article.Article, error) { return nil, nil } -func (r *ArticlesRepository) CountPublishedByHashtags(hashtags []string) (uint, error) { +func (r *ArticlesRepository) CountPublishedByHashtags(hashtags []string, languageCode string) (uint, error) { return 0, nil } -func (r *ArticlesRepository) GetPublishedByHashtags(hashtags []string, offset uint, limit uint) ([]article.Article, error) { +func (r *ArticlesRepository) GetPublishedByHashtags(hashtags []string, languageCode string, offset uint, limit uint) ([]article.Article, error) { return nil, nil } -func (r *ArticlesRepository) CountPublishedByAuthor(authorUUID string) (uint, error) { +func (r *ArticlesRepository) CountPublishedByAuthor(authorUUID string, languageCode string) (uint, error) { return 0, nil } -func (r *ArticlesRepository) GetPublishedByAuthor(authorUUID string, offset uint, limit uint) ([]article.Article, error) { +func (r *ArticlesRepository) GetPublishedByAuthor(authorUUID string, languageCode string, offset uint, limit uint) ([]article.Article, error) { return nil, nil } @@ -115,18 +176,35 @@ func (r *ArticlesRepository) GetOne(UUID string) (article.Article, error) { return a.(article.Article), nil } -func (r *ArticlesRepository) GetOnePublished(UUID string) (article.Article, error) { - a, ok := r.datastore.Load(UUID) - if !ok { - return article.Article{}, domain.ErrNotExists - } +func (r *ArticlesRepository) GetOnePublished(correlationUUID string, languageCode string) (article.Article, error) { + var ( + found article.Article + ok bool + ) + + r.datastore.Range(func(_, value any) bool { + item := value.(article.Article) + if item.CorrelationUUID != correlationUUID { + return true + } + if item.PublishedAt.After(time.Now()) { + return true + } + if len(languageCode) > 0 && item.LanguageCode != languageCode { + return true + } + + found = item + ok = true + + return false + }) - item := a.(article.Article) - if item.PublishedAt.After(time.Now()) { + if !ok { return article.Article{}, domain.ErrNotExists } - return item, nil + return found, nil } func (r *ArticlesRepository) Count() (uint, error) { @@ -141,13 +219,18 @@ func (r *ArticlesRepository) Count() (uint, error) { return c, nil } -func (r *ArticlesRepository) CountPublished() (uint, error) { +func (r *ArticlesRepository) CountPublished(languageCode string) (uint, error) { var c uint r.datastore.Range(func(_, value any) bool { - if article := value.(article.Article); article.PublishedAt.Before(time.Now()) { - c++ + article := value.(article.Article) + if !article.PublishedAt.Before(time.Now()) { + return true + } + if len(languageCode) > 0 && article.LanguageCode != languageCode { + return true } + c++ return true }) @@ -164,6 +247,10 @@ func (r *ArticlesRepository) Save(a *article.Article) (string, error) { a.UUID = UUID.String() } + if len(a.CorrelationUUID) == 0 { + a.CorrelationUUID = a.UUID + } + r.datastore.Store(a.UUID, *a) return a.UUID, nil diff --git a/infrastructure/repository/mocks/articles/repository.go b/infrastructure/repository/mocks/articles/repository.go index 4d121357..cbd74fcb 100644 --- a/infrastructure/repository/mocks/articles/repository.go +++ b/infrastructure/repository/mocks/articles/repository.go @@ -4,6 +4,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" ) type MockArticlesRepository struct { @@ -22,8 +23,8 @@ func (r *MockArticlesRepository) GetAll(offset uint, limit uint) ([]article.Arti return nil, args.Error(1) } -func (r *MockArticlesRepository) GetAllPublished(offset uint, limit uint) ([]article.Article, error) { - args := r.Mock.Called(offset, limit) +func (r *MockArticlesRepository) GetAllPublished(language string, offset uint, limit uint) ([]article.Article, error) { + args := r.Mock.Called(language, offset, limit) if a, ok := args.Get(0).([]article.Article); ok { return a, args.Error(1) @@ -38,14 +39,14 @@ func (r *MockArticlesRepository) GetOne(UUID string) (article.Article, error) { return args.Get(0).(article.Article), args.Error(1) } -func (r *MockArticlesRepository) GetOnePublished(UUID string) (article.Article, error) { - args := r.Mock.Called(UUID) +func (r *MockArticlesRepository) GetOnePublished(correlationUUID string, languageCode string) (article.Article, error) { + args := r.Mock.Called(correlationUUID, languageCode) return args.Get(0).(article.Article), args.Error(1) } -func (r *MockArticlesRepository) GetByUUIDs(UUIDs []string) ([]article.Article, error) { - args := r.Mock.Called(UUIDs) +func (r *MockArticlesRepository) GetByCorrelationUUIDs(correlationUUIDs []string, languageCode string) ([]article.Article, error) { + args := r.Mock.Called(correlationUUIDs, languageCode) if a, ok := args.Get(0).([]article.Article); ok { return a, args.Error(1) @@ -54,8 +55,24 @@ func (r *MockArticlesRepository) GetByUUIDs(UUIDs []string) ([]article.Article, return nil, args.Error(1) } -func (r *MockArticlesRepository) GetMostViewed(limit uint) ([]article.Article, error) { - args := r.Mock.Called(limit) +func (r *MockArticlesRepository) GetPublishedLanguages(correlationUUID string) ([]language.Language, error) { + args := r.Mock.Called(correlationUUID) + + if l, ok := args.Get(0).([]language.Language); ok { + return l, args.Error(1) + } + + return nil, args.Error(1) +} + +func (r *MockArticlesRepository) CorrelationExist(correlationUUID string) (bool, error) { + args := r.Mock.Called(correlationUUID) + + return args.Bool(0), args.Error(1) +} + +func (r *MockArticlesRepository) GetMostViewed(language string, limit uint) ([]article.Article, error) { + args := r.Mock.Called(language, limit) if a, ok := args.Get(0).([]article.Article); ok { return a, args.Error(1) @@ -64,14 +81,14 @@ func (r *MockArticlesRepository) GetMostViewed(limit uint) ([]article.Article, e return nil, args.Error(1) } -func (r *MockArticlesRepository) CountPublishedByHashtags(hashtags []string) (uint, error) { - args := r.Mock.Called(hashtags) +func (r *MockArticlesRepository) CountPublishedByHashtags(hashtags []string, language string) (uint, error) { + args := r.Mock.Called(hashtags, language) return args.Get(0).(uint), args.Error(1) } -func (r *MockArticlesRepository) GetPublishedByHashtags(hashtags []string, offset uint, limit uint) ([]article.Article, error) { - args := r.Mock.Called(hashtags, offset, limit) +func (r *MockArticlesRepository) GetPublishedByHashtags(hashtags []string, language string, offset uint, limit uint) ([]article.Article, error) { + args := r.Mock.Called(hashtags, language, offset, limit) if a, ok := args.Get(0).([]article.Article); ok { return a, args.Error(1) @@ -80,14 +97,14 @@ func (r *MockArticlesRepository) GetPublishedByHashtags(hashtags []string, offse return nil, args.Error(1) } -func (r *MockArticlesRepository) CountPublishedByAuthor(authorUUID string) (uint, error) { - args := r.Mock.Called(authorUUID) +func (r *MockArticlesRepository) CountPublishedByAuthor(authorUUID string, language string) (uint, error) { + args := r.Mock.Called(authorUUID, language) return args.Get(0).(uint), args.Error(1) } -func (r *MockArticlesRepository) GetPublishedByAuthor(authorUUID string, offset uint, limit uint) ([]article.Article, error) { - args := r.Mock.Called(authorUUID, offset, limit) +func (r *MockArticlesRepository) GetPublishedByAuthor(authorUUID string, language string, offset uint, limit uint) ([]article.Article, error) { + args := r.Mock.Called(authorUUID, language, offset, limit) if a, ok := args.Get(0).([]article.Article); ok { return a, args.Error(1) @@ -102,8 +119,8 @@ func (r *MockArticlesRepository) Count() (uint, error) { return args.Get(0).(uint), args.Error(1) } -func (r *MockArticlesRepository) CountPublished() (uint, error) { - args := r.Mock.Called() +func (r *MockArticlesRepository) CountPublished(language string) (uint, error) { + args := r.Mock.Called(language) return args.Get(0).(uint), args.Error(1) } diff --git a/infrastructure/repository/mocks/bookmarks/repository.go b/infrastructure/repository/mocks/bookmarks/repository.go index 41462007..9c61b511 100644 --- a/infrastructure/repository/mocks/bookmarks/repository.go +++ b/infrastructure/repository/mocks/bookmarks/repository.go @@ -18,12 +18,6 @@ func (r *MockBookmarksRepository) Save(b *bookmark.Bookmark) (string, error) { return args.String(0), args.Error(1) } -func (r *MockBookmarksRepository) Count(objectType string, objectUUID string) (uint, error) { - args := r.Mock.Called(objectType, objectUUID) - - return args.Get(0).(uint), args.Error(1) -} - func (r *MockBookmarksRepository) GetAllByOwnerUUID(ownerUUID string, offset uint, limit uint) ([]bookmark.Bookmark, error) { args := r.Mock.Called(ownerUUID, offset, limit) diff --git a/infrastructure/repository/mocks/languages/repository.go b/infrastructure/repository/mocks/languages/repository.go new file mode 100644 index 00000000..e244bdef --- /dev/null +++ b/infrastructure/repository/mocks/languages/repository.go @@ -0,0 +1,53 @@ +package languages + +import ( + "github.com/stretchr/testify/mock" + + "github.com/khanzadimahdi/testproject/domain/language" +) + +type MockLanguagesRepository struct { + mock.Mock +} + +var _ language.Repository = &MockLanguagesRepository{} + +func (r *MockLanguagesRepository) GetAll(offset uint, limit uint) ([]language.Language, error) { + args := r.Mock.Called(offset, limit) + + if a, ok := args.Get(0).([]language.Language); ok { + return a, args.Error(1) + } + + return nil, args.Error(1) +} + +func (r *MockLanguagesRepository) GetOne(key string) (language.Language, error) { + args := r.Called(key) + + return args.Get(0).(language.Language), args.Error(1) +} + +func (r *MockLanguagesRepository) Exists(key string) bool { + args := r.Called(key) + + return args.Bool(0) +} + +func (r *MockLanguagesRepository) Save(l *language.Language) (string, error) { + args := r.Mock.Called(l) + + return args.String(0), args.Error(1) +} + +func (r *MockLanguagesRepository) Delete(code string) error { + args := r.Mock.Called(code) + + return args.Error(0) +} + +func (r *MockLanguagesRepository) Count() (uint, error) { + args := r.Mock.Called() + + return args.Get(0).(uint), args.Error(1) +} diff --git a/infrastructure/repository/mocks/permissions/repository.go b/infrastructure/repository/mocks/permissions/repository.go index 1560981a..7568f60b 100644 --- a/infrastructure/repository/mocks/permissions/repository.go +++ b/infrastructure/repository/mocks/permissions/repository.go @@ -18,12 +18,6 @@ func (r *MockPermissionsRepository) GetAll() []permission.Permission { return args.Get(0).([]permission.Permission) } -func (r *MockPermissionsRepository) GetOne(value string) (permission.Permission, error) { - args := r.Called(value) - - return args.Get(0).(permission.Permission), args.Error(1) -} - func (r *MockPermissionsRepository) Get(values []string) ([]permission.Permission, error) { args := r.Mock.Called(values) diff --git a/infrastructure/repository/mongodb/articles/model.go b/infrastructure/repository/mongodb/articles/model.go index d6accc5c..571c35b8 100644 --- a/infrastructure/repository/mongodb/articles/model.go +++ b/infrastructure/repository/mongodb/articles/model.go @@ -5,16 +5,18 @@ import ( ) type ArticleBson struct { - UUID string `bson:"_id,omitempty"` - Cover string `bson:"cover"` - Video string `bson:"video"` - Title string `bson:"title"` - Excerpt string `bson:"excerpt"` - Body string `bson:"body"` - PublishedAt time.Time `bson:"published_at"` - AuthorUUID string `bson:"author_uuid"` - Tags []string `bson:"tags"` - ViewCount uint `bson:"view_count,omitempty"` - CreatedAt time.Time `bson:"created_at,omitempty"` - UpdatedAt time.Time `bson:"updated_at,omitempty"` + UUID string `bson:"_id,omitempty"` + Cover string `bson:"cover"` + Video string `bson:"video"` + Title string `bson:"title"` + Excerpt string `bson:"excerpt"` + Body string `bson:"body"` + PublishedAt time.Time `bson:"published_at"` + AuthorUUID string `bson:"author_uuid"` + Tags []string `bson:"tags"` + LanguageCode string `bson:"language_code"` + CorrelationUUID string `bson:"correlation_uuid"` + ViewCount uint `bson:"view_count,omitempty"` + CreatedAt time.Time `bson:"created_at,omitempty"` + UpdatedAt time.Time `bson:"updated_at,omitempty"` } diff --git a/infrastructure/repository/mongodb/articles/repository.go b/infrastructure/repository/mongodb/articles/repository.go index 043ddff1..66e3dcd5 100644 --- a/infrastructure/repository/mongodb/articles/repository.go +++ b/infrastructure/repository/mongodb/articles/repository.go @@ -12,6 +12,7 @@ import ( "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" ) const ( @@ -35,6 +36,23 @@ func NewRepository(database *mongo.Database) *ArticlesRepository { } } +func toDomain(a ArticleBson) article.Article { + return article.Article{ + UUID: a.UUID, + Cover: a.Cover, + Video: a.Video, + Title: a.Title, + Excerpt: a.Excerpt, + Body: a.Body, + PublishedAt: a.PublishedAt, + AuthorUUID: a.AuthorUUID, + Tags: a.Tags, + ViewCount: a.ViewCount, + LanguageCode: a.LanguageCode, + CorrelationUUID: a.CorrelationUUID, + } +} + func (r *ArticlesRepository) GetAll(offset uint, limit uint) ([]article.Article, error) { ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) defer cancel() @@ -56,16 +74,7 @@ func (r *ArticlesRepository) GetAll(offset uint, limit uint) ([]article.Article, if err := cur.Decode(&a); err != nil { return nil, err } - items = append(items, article.Article{ - UUID: a.UUID, - Cover: a.Cover, - Video: a.Video, - Title: a.Title, - Excerpt: a.Excerpt, - Tags: a.Tags, - PublishedAt: a.PublishedAt, - AuthorUUID: a.AuthorUUID, - }) + items = append(items, toDomain(a)) } if err := cur.Err(); err != nil { @@ -75,7 +84,7 @@ func (r *ArticlesRepository) GetAll(offset uint, limit uint) ([]article.Article, return items, nil } -func (r *ArticlesRepository) GetAllPublished(offset uint, limit uint) ([]article.Article, error) { +func (r *ArticlesRepository) GetAllPublished(language string, offset uint, limit uint) ([]article.Article, error) { ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) defer cancel() @@ -84,10 +93,10 @@ func (r *ArticlesRepository) GetAllPublished(offset uint, limit uint) ([]article desc := bson.D{{Key: "published_at", Value: -1}} filter := bson.M{ - "published_at": bson.M{ - "$lte": bson.NewDateTimeFromTime(time.Now()), - "$ne": time.Time{}, - }, + "published_at": publishedFilter(), + } + if len(language) > 0 { + filter["language_code"] = language } cur, err := r.collection.Find(ctx, filter, options.Find().SetLimit(l).SetSkip(o).SetSort(desc)) @@ -103,16 +112,7 @@ func (r *ArticlesRepository) GetAllPublished(offset uint, limit uint) ([]article if err := cur.Decode(&a); err != nil { return nil, err } - items = append(items, article.Article{ - UUID: a.UUID, - Cover: a.Cover, - Video: a.Video, - Title: a.Title, - Excerpt: a.Excerpt, - Tags: a.Tags, - PublishedAt: a.PublishedAt, - AuthorUUID: a.AuthorUUID, - }) + items = append(items, toDomain(a)) } if err := cur.Err(); err != nil { @@ -122,12 +122,15 @@ func (r *ArticlesRepository) GetAllPublished(offset uint, limit uint) ([]article return items, nil } -func (r *ArticlesRepository) GetByUUIDs(UUIDs []string) ([]article.Article, error) { +func (r *ArticlesRepository) GetByCorrelationUUIDs(correlationUUIDs []string, languageCode string) ([]article.Article, error) { ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) defer cancel() desc := bson.D{{Key: "published_at", Value: -1}} - filter := bson.M{"_id": bson.M{"$in": UUIDs}} + filter := bson.M{"correlation_uuid": bson.M{"$in": correlationUUIDs}} + if len(languageCode) > 0 { + filter["language_code"] = languageCode + } cur, err := r.collection.Find(ctx, filter, options.Find().SetSort(desc)) if err != nil { @@ -135,24 +138,14 @@ func (r *ArticlesRepository) GetByUUIDs(UUIDs []string) ([]article.Article, erro } defer cur.Close(ctx) - items := make([]article.Article, 0, len(UUIDs)) + items := make([]article.Article, 0, len(correlationUUIDs)) for cur.Next(ctx) { var a ArticleBson if err := cur.Decode(&a); err != nil { return nil, err } - items = append(items, article.Article{ - UUID: a.UUID, - Cover: a.Cover, - Video: a.Video, - Title: a.Title, - Body: a.Body, - Excerpt: a.Excerpt, - Tags: a.Tags, - PublishedAt: a.PublishedAt, - AuthorUUID: a.AuthorUUID, - }) + items = append(items, toDomain(a)) } if err := cur.Err(); err != nil { @@ -162,17 +155,61 @@ func (r *ArticlesRepository) GetByUUIDs(UUIDs []string) ([]article.Article, erro return items, nil } -func (r *ArticlesRepository) GetMostViewed(limit uint) ([]article.Article, error) { +func (r *ArticlesRepository) GetPublishedLanguages(correlationUUID string) ([]language.Language, error) { + ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) + defer cancel() + + if len(correlationUUID) == 0 { + return []language.Language{}, nil + } + + filter := bson.M{ + "correlation_uuid": correlationUUID, + "published_at": publishedFilter(), + } + + var languageCodes []string + if err := r.collection.Distinct(ctx, "language_code", filter).Decode(&languageCodes); err != nil { + return nil, err + } + + languages := make([]language.Language, 0, len(languageCodes)) + for _, code := range languageCodes { + languages = append(languages, language.Language{Code: code}) + } + + return languages, nil +} + +func (r *ArticlesRepository) CorrelationExist(correlationUUID string) (bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) + defer cancel() + + if len(correlationUUID) == 0 { + return false, nil + } + + filter := bson.M{"correlation_uuid": correlationUUID} + + c, err := r.collection.CountDocuments(ctx, filter, options.Count().SetLimit(1)) + if err != nil { + return false, err + } + + return c > 0, nil +} + +func (r *ArticlesRepository) GetMostViewed(language string, limit uint) ([]article.Article, error) { ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) defer cancel() l := int64(limit) desc := bson.D{{Key: "view_count", Value: -1}} filter := bson.M{ - "published_at": bson.M{ - "$lte": bson.NewDateTimeFromTime(time.Now()), - "$ne": time.Time{}, - }, + "published_at": publishedFilter(), + } + if len(language) > 0 { + filter["language_code"] = language } cur, err := r.collection.Find(ctx, filter, options.Find().SetLimit(l).SetSort(desc)) @@ -188,16 +225,7 @@ func (r *ArticlesRepository) GetMostViewed(limit uint) ([]article.Article, error if err := cur.Decode(&a); err != nil { return nil, err } - items = append(items, article.Article{ - UUID: a.UUID, - Cover: a.Cover, - Video: a.Video, - Title: a.Title, - Excerpt: a.Excerpt, - Tags: a.Tags, - PublishedAt: a.PublishedAt, - AuthorUUID: a.AuthorUUID, - }) + items = append(items, toDomain(a)) } if err := cur.Err(); err != nil { @@ -207,18 +235,16 @@ func (r *ArticlesRepository) GetMostViewed(limit uint) ([]article.Article, error return items, nil } -func (r *ArticlesRepository) CountPublishedByHashtags(hashtags []string) (uint, error) { +func (r *ArticlesRepository) CountPublishedByHashtags(hashtags []string, language string) (uint, error) { ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) defer cancel() filter := bson.M{ - "tags": bson.M{ - "$in": hashtags, - }, - "published_at": bson.M{ - "$lte": bson.NewDateTimeFromTime(time.Now()), - "$ne": time.Time{}, - }, + "tags": bson.M{"$in": hashtags}, + "published_at": publishedFilter(), + } + if len(language) > 0 { + filter["language_code"] = language } c, err := r.collection.CountDocuments(ctx, filter) @@ -229,7 +255,7 @@ func (r *ArticlesRepository) CountPublishedByHashtags(hashtags []string) (uint, return uint(c), nil } -func (r *ArticlesRepository) GetPublishedByHashtags(hashtags []string, offset uint, limit uint) ([]article.Article, error) { +func (r *ArticlesRepository) GetPublishedByHashtags(hashtags []string, language string, offset uint, limit uint) ([]article.Article, error) { ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) defer cancel() @@ -237,13 +263,11 @@ func (r *ArticlesRepository) GetPublishedByHashtags(hashtags []string, offset ui l := int64(limit) desc := bson.D{{Key: "published_at", Value: -1}} filter := bson.M{ - "tags": bson.M{ - "$in": hashtags, - }, - "published_at": bson.M{ - "$lte": bson.NewDateTimeFromTime(time.Now()), - "$ne": time.Time{}, - }, + "tags": bson.M{"$in": hashtags}, + "published_at": publishedFilter(), + } + if len(language) > 0 { + filter["language_code"] = language } cur, err := r.collection.Find( @@ -264,16 +288,7 @@ func (r *ArticlesRepository) GetPublishedByHashtags(hashtags []string, offset ui if err := cur.Decode(&a); err != nil { return nil, err } - items = append(items, article.Article{ - UUID: a.UUID, - Cover: a.Cover, - Video: a.Video, - Title: a.Title, - Excerpt: a.Excerpt, - Tags: a.Tags, - PublishedAt: a.PublishedAt, - AuthorUUID: a.AuthorUUID, - }) + items = append(items, toDomain(a)) } if err := cur.Err(); err != nil { @@ -283,16 +298,16 @@ func (r *ArticlesRepository) GetPublishedByHashtags(hashtags []string, offset ui return items, nil } -func (r *ArticlesRepository) CountPublishedByAuthor(authorUUID string) (uint, error) { +func (r *ArticlesRepository) CountPublishedByAuthor(authorUUID string, language string) (uint, error) { ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) defer cancel() filter := bson.M{ - "author_uuid": authorUUID, - "published_at": bson.M{ - "$lte": bson.NewDateTimeFromTime(time.Now()), - "$ne": time.Time{}, - }, + "author_uuid": authorUUID, + "published_at": publishedFilter(), + } + if len(language) > 0 { + filter["language_code"] = language } c, err := r.collection.CountDocuments(ctx, filter) @@ -303,7 +318,7 @@ func (r *ArticlesRepository) CountPublishedByAuthor(authorUUID string) (uint, er return uint(c), nil } -func (r *ArticlesRepository) GetPublishedByAuthor(authorUUID string, offset uint, limit uint) ([]article.Article, error) { +func (r *ArticlesRepository) GetPublishedByAuthor(authorUUID string, language string, offset uint, limit uint) ([]article.Article, error) { ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) defer cancel() @@ -311,11 +326,11 @@ func (r *ArticlesRepository) GetPublishedByAuthor(authorUUID string, offset uint l := int64(limit) desc := bson.D{{Key: "published_at", Value: -1}} filter := bson.M{ - "author_uuid": authorUUID, - "published_at": bson.M{ - "$lte": bson.NewDateTimeFromTime(time.Now()), - "$ne": time.Time{}, - }, + "author_uuid": authorUUID, + "published_at": publishedFilter(), + } + if len(language) > 0 { + filter["language_code"] = language } cur, err := r.collection.Find( @@ -336,16 +351,7 @@ func (r *ArticlesRepository) GetPublishedByAuthor(authorUUID string, offset uint if err := cur.Decode(&a); err != nil { return nil, err } - items = append(items, article.Article{ - UUID: a.UUID, - Cover: a.Cover, - Video: a.Video, - Title: a.Title, - Excerpt: a.Excerpt, - Tags: a.Tags, - PublishedAt: a.PublishedAt, - AuthorUUID: a.AuthorUUID, - }) + items = append(items, toDomain(a)) } if err := cur.Err(); err != nil { @@ -369,30 +375,19 @@ func (r *ArticlesRepository) GetOne(UUID string) (article.Article, error) { return article.Article{}, err } - return article.Article{ - UUID: a.UUID, - Cover: a.Cover, - Video: a.Video, - Title: a.Title, - Excerpt: a.Excerpt, - Body: a.Body, - PublishedAt: a.PublishedAt, - AuthorUUID: a.AuthorUUID, - Tags: a.Tags, - ViewCount: a.ViewCount, - }, nil + return toDomain(a), nil } -func (r *ArticlesRepository) GetOnePublished(UUID string) (article.Article, error) { +func (r *ArticlesRepository) GetOnePublished(correlationUUID string, languageCode string) (article.Article, error) { ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) defer cancel() filter := bson.M{ - "_id": UUID, - "published_at": bson.M{ - "$lte": bson.NewDateTimeFromTime(time.Now()), - "$ne": time.Time{}, - }, + "correlation_uuid": correlationUUID, + "published_at": publishedFilter(), + } + if len(languageCode) > 0 { + filter["language_code"] = languageCode } var a ArticleBson @@ -403,18 +398,7 @@ func (r *ArticlesRepository) GetOnePublished(UUID string) (article.Article, erro return article.Article{}, err } - return article.Article{ - UUID: a.UUID, - Cover: a.Cover, - Video: a.Video, - Title: a.Title, - Excerpt: a.Excerpt, - Body: a.Body, - PublishedAt: a.PublishedAt, - AuthorUUID: a.AuthorUUID, - Tags: a.Tags, - ViewCount: a.ViewCount, - }, nil + return toDomain(a), nil } func (r *ArticlesRepository) Count() (uint, error) { @@ -429,15 +413,15 @@ func (r *ArticlesRepository) Count() (uint, error) { return uint(c), nil } -func (r *ArticlesRepository) CountPublished() (uint, error) { +func (r *ArticlesRepository) CountPublished(language string) (uint, error) { ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) defer cancel() filter := bson.M{ - "published_at": bson.M{ - "$lte": bson.NewDateTimeFromTime(time.Now()), - "$ne": time.Time{}, - }, + "published_at": publishedFilter(), + } + if len(language) > 0 { + filter["language_code"] = language } c, err := r.collection.CountDocuments(ctx, filter) @@ -460,19 +444,25 @@ func (r *ArticlesRepository) Save(a *article.Article) (string, error) { a.UUID = UUID.String() } + if len(a.CorrelationUUID) == 0 { + a.CorrelationUUID = a.UUID + } + update := ArticleBson{ - UUID: a.UUID, - Cover: a.Cover, - Title: a.Title, - Video: a.Video, - Excerpt: a.Excerpt, - Body: a.Body, - PublishedAt: a.PublishedAt, - AuthorUUID: a.AuthorUUID, - Tags: a.Tags, - ViewCount: a.ViewCount, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + UUID: a.UUID, + Cover: a.Cover, + Title: a.Title, + Video: a.Video, + Excerpt: a.Excerpt, + Body: a.Body, + PublishedAt: a.PublishedAt, + AuthorUUID: a.AuthorUUID, + Tags: a.Tags, + ViewCount: a.ViewCount, + LanguageCode: a.LanguageCode, + CorrelationUUID: a.CorrelationUUID, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } if _, err := r.collection.UpdateOne( @@ -508,3 +498,10 @@ func (r *ArticlesRepository) IncreaseView(uuid string, inc uint) error { return err } + +func publishedFilter() bson.M { + return bson.M{ + "$lte": bson.NewDateTimeFromTime(time.Now()), + "$ne": time.Time{}, + } +} diff --git a/infrastructure/repository/mongodb/authors/model.go b/infrastructure/repository/mongodb/authors/model.go deleted file mode 100644 index 19170708..00000000 --- a/infrastructure/repository/mongodb/authors/model.go +++ /dev/null @@ -1 +0,0 @@ -package authors diff --git a/infrastructure/repository/mongodb/authors/repository.go b/infrastructure/repository/mongodb/authors/repository.go deleted file mode 100644 index 19170708..00000000 --- a/infrastructure/repository/mongodb/authors/repository.go +++ /dev/null @@ -1 +0,0 @@ -package authors diff --git a/infrastructure/repository/mongodb/bookmarks/repository.go b/infrastructure/repository/mongodb/bookmarks/repository.go index ae385210..3ddc9185 100644 --- a/infrastructure/repository/mongodb/bookmarks/repository.go +++ b/infrastructure/repository/mongodb/bookmarks/repository.go @@ -69,23 +69,6 @@ func (r *BookmarksRepository) Save(b *bookmark.Bookmark) (string, error) { return b.ObjectUUID, nil } -func (r *BookmarksRepository) Count(objectType string, objectUUID string) (uint, error) { - ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) - defer cancel() - - filter := bson.M{ - "object_uuid": objectUUID, - "object_type": objectType, - } - - c, err := r.collection.CountDocuments(ctx, filter) - if err != nil { - return uint(c), err - } - - return uint(c), nil -} - func (r *BookmarksRepository) GetAllByOwnerUUID(ownerUUID string, offset uint, limit uint) ([]bookmark.Bookmark, error) { ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) defer cancel() diff --git a/infrastructure/repository/mongodb/config/model.go b/infrastructure/repository/mongodb/config/model.go index df55a351..86589dac 100644 --- a/infrastructure/repository/mongodb/config/model.go +++ b/infrastructure/repository/mongodb/config/model.go @@ -8,5 +8,6 @@ type configBson struct { UUID string `bson:"_id,omitempty"` Revision uint `bson:"revision"` UserDefaultRoleUUIDs []string `bson:"user_default_role_uuids"` + DefaultLanguageCode string `bson:"default_language_code"` CreatedAt time.Time `bson:"created_at,omitempty"` } diff --git a/infrastructure/repository/mongodb/config/repository.go b/infrastructure/repository/mongodb/config/repository.go index 7e3a3f52..9bfac102 100644 --- a/infrastructure/repository/mongodb/config/repository.go +++ b/infrastructure/repository/mongodb/config/repository.go @@ -53,6 +53,7 @@ func (r *ConfigRepository) GetLatestRevision() (config.Config, error) { return config.Config{ Revision: c.Revision, UserDefaultRoleUUIDs: c.UserDefaultRoleUUIDs, + DefaultLanguageCode: c.DefaultLanguageCode, }, nil } @@ -69,6 +70,7 @@ func (r *ConfigRepository) Save(a *config.Config) (string, error) { UUID: UUID.String(), Revision: a.Revision + 1, UserDefaultRoleUUIDs: a.UserDefaultRoleUUIDs, + DefaultLanguageCode: a.DefaultLanguageCode, CreatedAt: time.Now(), } diff --git a/infrastructure/repository/mongodb/languages/model.go b/infrastructure/repository/mongodb/languages/model.go new file mode 100644 index 00000000..f3801d0f --- /dev/null +++ b/infrastructure/repository/mongodb/languages/model.go @@ -0,0 +1,12 @@ +package languages + +import ( + "time" +) + +type LanguageBson struct { + Code string `bson:"_id,omitempty"` + Name string `bson:"name"` + CreatedAt time.Time `bson:"created_at,omitempty"` + UpdatedAt time.Time `bson:"updated_at,omitempty"` +} diff --git a/infrastructure/repository/mongodb/languages/repository.go b/infrastructure/repository/mongodb/languages/repository.go new file mode 100644 index 00000000..f41ad054 --- /dev/null +++ b/infrastructure/repository/mongodb/languages/repository.go @@ -0,0 +1,148 @@ +package languages + +import ( + "context" + "errors" + "strings" + "time" + + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" + + "github.com/khanzadimahdi/testproject/domain" + "github.com/khanzadimahdi/testproject/domain/language" +) + +const ( + collectionName = "languages" + queryTimeout = 3 * time.Second +) + +type LanguagesRepository struct { + collection *mongo.Collection +} + +var _ language.Repository = &LanguagesRepository{} + +func NewRepository(database *mongo.Database) *LanguagesRepository { + if database == nil { + panic("database should not be nil") + } + + return &LanguagesRepository{ + collection: database.Collection(collectionName), + } +} + +func (r *LanguagesRepository) GetAll(offset uint, limit uint) ([]language.Language, error) { + ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) + defer cancel() + + o := int64(offset) + l := int64(limit) + asc := bson.D{{Key: "_id", Value: 1}} + + cur, err := r.collection.Find(ctx, bson.D{}, options.Find().SetSkip(o).SetLimit(l).SetSort(asc)) + if err != nil { + return nil, err + } + defer cur.Close(ctx) + + items := make([]language.Language, 0, limit) + for cur.Next(ctx) { + var lb LanguageBson + + if err := cur.Decode(&lb); err != nil { + return nil, err + } + items = append(items, language.Language{ + Code: lb.Code, + Name: lb.Name, + }) + } + + if err := cur.Err(); err != nil { + return nil, err + } + + return items, nil +} + +func (r *LanguagesRepository) GetOne(code string) (language.Language, error) { + ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) + defer cancel() + + var lb LanguageBson + if err := r.collection.FindOne(ctx, bson.D{{Key: "_id", Value: code}}).Decode(&lb); err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + err = domain.ErrNotExists + } + return language.Language{}, err + } + + return language.Language{ + Code: lb.Code, + Name: lb.Name, + }, nil +} + +func (r *LanguagesRepository) Exists(code string) bool { + ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) + defer cancel() + + c, err := r.collection.CountDocuments(ctx, bson.D{{Key: "_id", Value: code}}, options.Count().SetLimit(1)) + if err != nil { + return false + } + + return c > 0 +} + +func (r *LanguagesRepository) Save(l *language.Language) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) + defer cancel() + + at := time.Now() + + update := LanguageBson{ + Name: l.Name, + UpdatedAt: at, + } + + // language code should be always uppercase + l.Code = strings.ToUpper(l.Code) + + _, err := r.collection.UpdateOne( + ctx, + bson.D{{Key: "_id", Value: l.Code}}, + bson.M{ + "$set": bson.M{"name": update.Name, "updated_at": update.UpdatedAt}, + "$setOnInsert": bson.M{"created_at": at}, + }, + options.UpdateOne().SetUpsert(true), + ) + + return l.Code, err +} + +func (r *LanguagesRepository) Delete(code string) error { + ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) + defer cancel() + + _, err := r.collection.DeleteOne(ctx, bson.D{{Key: "_id", Value: code}}) + + return err +} + +func (r *LanguagesRepository) Count() (uint, error) { + ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) + defer cancel() + + c, err := r.collection.CountDocuments(ctx, bson.D{}) + if err != nil { + return uint(c), err + } + + return uint(c), nil +} diff --git a/infrastructure/repository/mongodb/permissions/collection.go b/infrastructure/repository/mongodb/permissions/collection.go index 2cfb8357..b92a6e00 100644 --- a/infrastructure/repository/mongodb/permissions/collection.go +++ b/infrastructure/repository/mongodb/permissions/collection.go @@ -52,6 +52,13 @@ var collection []permission.Permission = []permission.Permission{ {Name: "show configuration", Value: permission.ConfigShow}, {Name: "update configuration", Value: permission.ConfigUpdate}, + // languages + {Name: "list of languages", Value: permission.LanguagesIndex}, + {Name: "create a language", Value: permission.LanguagesCreate}, + {Name: "show a language", Value: permission.LanguagesShow}, + {Name: "update a language", Value: permission.LanguagesUpdate}, + {Name: "delete a language", Value: permission.LanguagesDelete}, + // self bookmarks {Name: "list of self bookmarks", Value: permission.SelfBookmarksIndex}, {Name: "delete a self bookmark", Value: permission.SelfBookmarksDelete}, diff --git a/infrastructure/repository/mongodb/runner/nodes/repository.go b/infrastructure/repository/mongodb/runner/nodes/repository.go index f6e28090..23a26a12 100644 --- a/infrastructure/repository/mongodb/runner/nodes/repository.go +++ b/infrastructure/repository/mongodb/runner/nodes/repository.go @@ -147,15 +147,6 @@ func (r *NodesRepository) Save(n *node.Node) (string, error) { return n.Name, nil } -func (r *NodesRepository) Delete(UUID string) error { - ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) - defer cancel() - - _, err := r.collection.DeleteOne(ctx, bson.D{{Key: "_id", Value: UUID}}) - - return err -} - func (r *NodesRepository) Count() (uint, error) { ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) defer cancel() diff --git a/infrastructure/repository/mongodb/users/model.go b/infrastructure/repository/mongodb/users/model.go index 1f55572b..6eb5aabb 100644 --- a/infrastructure/repository/mongodb/users/model.go +++ b/infrastructure/repository/mongodb/users/model.go @@ -10,6 +10,7 @@ type UserBson struct { Avatar string `bson:"avatar"` Email string `bson:"email"` Username string `bson:"username"` + LanguageCode string `bson:"language_code"` PasswordHash PasswordHashBson `bson:"hash,omitempty"` CreatedAt time.Time `bson:"created_at,omitempty"` } diff --git a/infrastructure/repository/mongodb/users/repository.go b/infrastructure/repository/mongodb/users/repository.go index 8c7386a1..fed531e3 100644 --- a/infrastructure/repository/mongodb/users/repository.go +++ b/infrastructure/repository/mongodb/users/repository.go @@ -58,11 +58,12 @@ func (r *UsersRepository) GetAll(offset uint, limit uint) ([]user.User, error) { return nil, err } items = append(items, user.User{ - UUID: a.UUID, - Name: a.Name, - Avatar: a.Avatar, - Email: a.Email, - Username: a.Username, + UUID: a.UUID, + Name: a.Name, + Avatar: a.Avatar, + Email: a.Email, + Username: a.Username, + LanguageCode: a.LanguageCode, PasswordHash: password.Hash{ Value: a.PasswordHash.Value, Salt: a.PasswordHash.Salt, @@ -98,11 +99,12 @@ func (r *UsersRepository) GetByUUIDs(UUIDs []string) ([]user.User, error) { return nil, err } items = append(items, user.User{ - UUID: a.UUID, - Name: a.Name, - Avatar: a.Avatar, - Email: a.Email, - Username: a.Username, + UUID: a.UUID, + Name: a.Name, + Avatar: a.Avatar, + Email: a.Email, + Username: a.Username, + LanguageCode: a.LanguageCode, PasswordHash: password.Hash{ Value: a.PasswordHash.Value, Salt: a.PasswordHash.Salt, @@ -131,11 +133,12 @@ func (r *UsersRepository) GetOne(UUID string) (user.User, error) { } return user.User{ - UUID: a.UUID, - Name: a.Name, - Avatar: a.Avatar, - Email: a.Email, - Username: a.Username, + UUID: a.UUID, + Name: a.Name, + Avatar: a.Avatar, + Email: a.Email, + Username: a.Username, + LanguageCode: a.LanguageCode, PasswordHash: password.Hash{ Value: a.PasswordHash.Value, Salt: a.PasswordHash.Salt, @@ -169,11 +172,12 @@ func (r *UsersRepository) GetOneByIdentity(identity string) (user.User, error) { } return user.User{ - UUID: a.UUID, - Name: a.Name, - Avatar: a.Avatar, - Email: a.Email, - Username: a.Username, + UUID: a.UUID, + Name: a.Name, + Avatar: a.Avatar, + Email: a.Email, + Username: a.Username, + LanguageCode: a.LanguageCode, PasswordHash: password.Hash{ Value: a.PasswordHash.Value, Salt: a.PasswordHash.Salt, @@ -195,11 +199,12 @@ func (r *UsersRepository) Save(a *user.User) (string, error) { } update := UserBson{ - UUID: a.UUID, - Name: a.Name, - Avatar: a.Avatar, - Email: a.Email, - Username: a.Username, + UUID: a.UUID, + Name: a.Name, + Avatar: a.Avatar, + Email: a.Email, + Username: a.Username, + LanguageCode: a.LanguageCode, PasswordHash: PasswordHashBson{ Value: a.PasswordHash.Value, Salt: a.PasswordHash.Salt, diff --git a/infrastructure/websocket/websocket_test.go b/infrastructure/websocket/websocket_test.go index 4eb41b16..29f209c9 100644 --- a/infrastructure/websocket/websocket_test.go +++ b/infrastructure/websocket/websocket_test.go @@ -694,7 +694,9 @@ func TestWebsocket(t *testing.T) { transientErr := errors.New("transient registry error") requestRegistryMock.On("GetClientSideID", "server-1").Return("", transientErr).Once() requestRegistryMock.On("GetClientSideID", "server-1").Return("client-1", nil).Once() - requestRegistryMock.On("DeleteByServerSideID", "server-1").Return(nil).Once() + + deleted := make(chan struct{}, 1) + requestRegistryMock.On("DeleteByServerSideID", "server-1").Run(func(args mock.Arguments) { deleted <- struct{}{} }).Return(nil).Once() defer requestRegistryMock.AssertExpectations(t) var replyHandler domain.MessageHandler @@ -736,6 +738,14 @@ func TestWebsocket(t *testing.T) { assert.NoError(t, client.ReadJSON(&response)) assert.Equal(t, "client-1", response.RequestID) assert.Equal(t, []byte("retry-success"), response.Payload) + + // DeleteByServerSideID runs after the reply is written to the client, so + // wait for it before the deferred AssertExpectations checks the mock. + select { + case <-deleted: + case <-time.After(time.Second): + t.Fatal("DeleteByServerSideID was not called after the reply was sent") + } }) t.Run("concurrent connect and disconnect does not corrupt replyChans", func(t *testing.T) { diff --git a/presentation/http/blog/api/article/index.go b/presentation/http/blog/api/article/index.go index 01ac6f79..ad986248 100644 --- a/presentation/http/blog/api/article/index.go +++ b/presentation/http/blog/api/article/index.go @@ -21,12 +21,13 @@ func NewIndexHandler(useCase *getarticles.UseCase) *indexHandler { // @Summary List published articles // @Description return a page of the most recent published articles -// @Tags articles -// @Accept json +// @Tags articles +// @Accept json // @Produce json -// @Param page query int false "Page number" default(1) -// @Success 200 {object} getarticles.Response -// @Failure 500 {object} map[string]interface{} +// @Param page query int false "Page number" default(1) +// @Param language_code query string false "Language key (e.g. EN, FA)" default(EN) +// @Success 200 {object} getarticles.Response +// @Failure 500 {object} map[string]interface{} // @Router /articles [get] func (h *indexHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { var page uint = 1 @@ -38,7 +39,8 @@ func (h *indexHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { } request := &getarticles.Request{ - Page: page, + Page: page, + LanguageCode: r.URL.Query().Get("language_code"), } response, err := h.useCase.Execute(request) diff --git a/presentation/http/blog/api/article/index_test.go b/presentation/http/blog/api/article/index_test.go index fd588a1f..ce72fb7e 100644 --- a/presentation/http/blog/api/article/index_test.go +++ b/presentation/http/blog/api/article/index_test.go @@ -11,7 +11,9 @@ import ( "github.com/stretchr/testify/assert" getarticles "github.com/khanzadimahdi/testproject/application/article/getArticles" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/users" @@ -26,6 +28,7 @@ func TestIndexHandler(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver ) publishedAt, err := time.Parse(time.RFC3339, "2024-09-29T15:56:25Z") @@ -67,14 +70,20 @@ func TestIndexHandler(t *testing.T) { {UUID: "author-uuid-2", Name: "author-name", Avatar: "author-avatar", Username: "author-username-2"}, } - articlesRepository.On("CountPublished").Once().Return(uint(len(articles)), nil) - articlesRepository.On("GetAllPublished", uint(0), uint(10)).Once().Return(articles, nil) + articlesRepository.On("CountPublished", "EN").Once().Return(uint(len(articles)), nil) + articlesRepository.On("GetAllPublished", "EN", uint(0), uint(10)).Once().Return(articles, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetByUUIDs", []string{"author-uuid-1", "author-uuid-1", "author-uuid-2"}).Once().Return(users, nil) defer userRepository.AssertExpectations(t) - handler := NewIndexHandler(getarticles.NewUseCase(&articlesRepository, &userRepository)) + articlesRepository.On("GetPublishedLanguages", "").Return([]language.Language{}, nil) + + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN", Name: "English"}, nil) + defer languageResolver.AssertExpectations(t) + + handler := NewIndexHandler(getarticles.NewUseCase(&articlesRepository, &userRepository, &languageResolver)) request := httptest.NewRequest(http.MethodGet, "/?page=1", nil) response := httptest.NewRecorder() @@ -95,16 +104,21 @@ func TestIndexHandler(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver ) - articlesRepository.On("CountPublished").Once().Return(uint(0), nil) - articlesRepository.On("GetAllPublished", uint(0), uint(10)).Once().Return(nil, nil) + articlesRepository.On("CountPublished", "EN").Once().Return(uint(0), nil) + articlesRepository.On("GetAllPublished", "EN", uint(0), uint(10)).Once().Return(nil, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetByUUIDs", []string{}).Once().Return([]user.User{}, nil) defer userRepository.AssertExpectations(t) - handler := NewIndexHandler(getarticles.NewUseCase(&articlesRepository, &userRepository)) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN", Name: "English"}, nil) + defer languageResolver.AssertExpectations(t) + + handler := NewIndexHandler(getarticles.NewUseCase(&articlesRepository, &userRepository, &languageResolver)) request := httptest.NewRequest(http.MethodGet, "/?page=1", nil) response := httptest.NewRecorder() @@ -125,12 +139,17 @@ func TestIndexHandler(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver ) - articlesRepository.On("CountPublished").Once().Return(uint(0), errors.New("something faulty has happened")) + articlesRepository.On("CountPublished", "EN").Once().Return(uint(0), errors.New("something faulty has happened")) defer articlesRepository.AssertExpectations(t) - handler := NewIndexHandler(getarticles.NewUseCase(&articlesRepository, &userRepository)) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN", Name: "English"}, nil) + defer languageResolver.AssertExpectations(t) + + handler := NewIndexHandler(getarticles.NewUseCase(&articlesRepository, &userRepository, &languageResolver)) request := httptest.NewRequest(http.MethodGet, "/?page=1", nil) response := httptest.NewRecorder() diff --git a/presentation/http/blog/api/article/show.go b/presentation/http/blog/api/article/show.go index be48fbd7..b013091b 100644 --- a/presentation/http/blog/api/article/show.go +++ b/presentation/http/blog/api/article/show.go @@ -21,18 +21,22 @@ func NewShowHandler(useCase *getarticle.UseCase) *showHandler { // @Summary Retrieve a single article // @Description get one published article by UUID -// @Tags articles -// @Accept json +// @Tags articles +// @Accept json // @Produce json -// @Param uuid path string true "Article UUID" -// @Success 200 {object} getarticle.Response -// @Failure 404 {object} map[string]interface{} -// @Failure 500 {object} map[string]interface{} +// @Param uuid path string true "Article UUID" +// @Param language_code query string false "Language key (e.g. EN, FA)" default(EN) +// @Success 200 {object} getarticle.Response +// @Failure 404 {object} map[string]interface{} +// @Failure 500 {object} map[string]interface{} // @Router /articles/{uuid} [get] func (h *showHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { UUID := r.PathValue("uuid") - response, err := h.useCase.Execute(UUID) + response, err := h.useCase.Execute(&getarticle.Request{ + CorrelationUUID: UUID, + LanguageCode: r.URL.Query().Get("language_code"), + }) switch { case errors.Is(err, domain.ErrNotExists): diff --git a/presentation/http/blog/api/article/show_test.go b/presentation/http/blog/api/article/show_test.go index 819f03fb..21c29e3f 100644 --- a/presentation/http/blog/api/article/show_test.go +++ b/presentation/http/blog/api/article/show_test.go @@ -13,12 +13,15 @@ import ( getarticle "github.com/khanzadimahdi/testproject/application/article/getArticle" "github.com/khanzadimahdi/testproject/application/element" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/elements" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/users" + "github.com/khanzadimahdi/testproject/infrastructure/validator" ) func TestShowHandler(t *testing.T) { @@ -31,18 +34,22 @@ func TestShowHandler(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + requestValidator validator.MockValidator ) publishedAt, err := time.Parse(time.RFC3339, "2024-09-29T15:56:25Z") assert.NoError(t, err) a := article.Article{ - UUID: "test-uuid-1", - Title: "test title", - Body: "test body", - PublishedAt: publishedAt, - AuthorUUID: "author-uuid", - ViewCount: 11, + UUID: "test-uuid-1", + Title: "test title", + Body: "test body", + PublishedAt: publishedAt, + AuthorUUID: "author-uuid", + ViewCount: 11, + LanguageCode: "EN", + CorrelationUUID: "test-uuid-1", } u := user.User{ UUID: "author-uuid", @@ -51,9 +58,17 @@ func TestShowHandler(t *testing.T) { Username: "author-username", } - articlesRepository.On("GetOnePublished", a.UUID).Once().Return(a, nil) + requestValidator.On("Validate", &getarticle.Request{CorrelationUUID: a.CorrelationUUID}).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN", Name: "English"}, nil) + defer languageResolver.AssertExpectations(t) + + articlesRepository.On("GetOnePublished", a.CorrelationUUID, "EN").Once().Return(a, nil) + articlesRepository.On("GetPublishedLanguages", a.CorrelationUUID).Once().Return([]language.Language{{Code: "EN", Name: "English"}}, nil) articlesRepository.On("IncreaseView", a.UUID, uint(1)).Once().Return(nil) - articlesRepository.On("GetByUUIDs", []string{}).Once().Return(nil, nil) + articlesRepository.On("GetByCorrelationUUIDs", []string{}, "EN").Once().Return(nil, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetOne", "author-uuid").Once().Return(u, nil) @@ -64,10 +79,10 @@ func TestShowHandler(t *testing.T) { defer elementsRepository.AssertExpectations(t) elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - handler := NewShowHandler(getarticle.NewUseCase(&articlesRepository, &userRepository, elementRetriever)) + handler := NewShowHandler(getarticle.NewUseCase(&articlesRepository, &userRepository, &languageResolver, elementRetriever, &requestValidator)) request := httptest.NewRequest(http.MethodGet, "/", nil) - request.SetPathValue("uuid", a.UUID) + request.SetPathValue("uuid", a.CorrelationUUID) response := httptest.NewRecorder() @@ -88,27 +103,34 @@ func TestShowHandler(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + requestValidator validator.MockValidator + + correlationUUID = "test-uuid-1" ) - a := article.Article{ - UUID: "test-uuid-1", - } + requestValidator.On("Validate", &getarticle.Request{CorrelationUUID: correlationUUID}).Once().Return(nil) + defer requestValidator.AssertExpectations(t) - articlesRepository.On("GetOnePublished", a.UUID).Once().Return(article.Article{}, domain.ErrNotExists) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) + + articlesRepository.On("GetOnePublished", correlationUUID, "EN").Once().Return(article.Article{}, domain.ErrNotExists) defer articlesRepository.AssertExpectations(t) elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - handler := NewShowHandler(getarticle.NewUseCase(&articlesRepository, &userRepository, elementRetriever)) + handler := NewShowHandler(getarticle.NewUseCase(&articlesRepository, &userRepository, &languageResolver, elementRetriever, &requestValidator)) request := httptest.NewRequest(http.MethodGet, "/", nil) - request.SetPathValue("uuid", a.UUID) + request.SetPathValue("uuid", correlationUUID) response := httptest.NewRecorder() handler.ServeHTTP(response, request) articlesRepository.AssertNotCalled(t, "IncreaseView") - articlesRepository.AssertNotCalled(t, "GetByUUIDs") + articlesRepository.AssertNotCalled(t, "GetByCorrelationUUIDs") userRepository.AssertNotCalled(t, "GetOne") elementsRepository.AssertNotCalled(t, "GetByVenues") @@ -123,27 +145,34 @@ func TestShowHandler(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver + requestValidator validator.MockValidator + + correlationUUID = "test-uuid-1" ) - a := article.Article{ - UUID: "test-uuid-1", - } + requestValidator.On("Validate", &getarticle.Request{CorrelationUUID: correlationUUID}).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN"}, nil) + defer languageResolver.AssertExpectations(t) - articlesRepository.On("GetOnePublished", a.UUID).Once().Return(article.Article{}, errors.New("an error has happened")) + articlesRepository.On("GetOnePublished", correlationUUID, "EN").Once().Return(article.Article{}, errors.New("an error has happened")) defer articlesRepository.AssertExpectations(t) elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - handler := NewShowHandler(getarticle.NewUseCase(&articlesRepository, &userRepository, elementRetriever)) + handler := NewShowHandler(getarticle.NewUseCase(&articlesRepository, &userRepository, &languageResolver, elementRetriever, &requestValidator)) request := httptest.NewRequest(http.MethodGet, "/", nil) - request.SetPathValue("uuid", a.UUID) + request.SetPathValue("uuid", correlationUUID) response := httptest.NewRecorder() handler.ServeHTTP(response, request) articlesRepository.AssertNotCalled(t, "IncreaseView") - articlesRepository.AssertNotCalled(t, "GetByUUIDs") + articlesRepository.AssertNotCalled(t, "GetByCorrelationUUIDs") userRepository.AssertNotCalled(t, "GetOne") elementsRepository.AssertNotCalled(t, "GetByVenues") diff --git a/presentation/http/blog/api/article/testdata/response-index-no-data-response.json b/presentation/http/blog/api/article/testdata/response-index-no-data-response.json index 43936acd..aed95918 100644 --- a/presentation/http/blog/api/article/testdata/response-index-no-data-response.json +++ b/presentation/http/blog/api/article/testdata/response-index-no-data-response.json @@ -1,7 +1,11 @@ { "items": [], + "language_code": { + "code": "EN", + "name": "English" + }, "pagination": { "total_pages": 0, "current_page": 1 } -} \ No newline at end of file +} diff --git a/presentation/http/blog/api/article/testdata/response-index-response.json b/presentation/http/blog/api/article/testdata/response-index-response.json index 2235a86a..422e3bca 100644 --- a/presentation/http/blog/api/article/testdata/response-index-response.json +++ b/presentation/http/blog/api/article/testdata/response-index-response.json @@ -12,7 +12,8 @@ "name": "author-name", "avatar": "author-avatar", "username": "author-username-1" - } + }, + "available_languages": null }, { "uuid": "article-uuid-2", @@ -26,7 +27,8 @@ "name": "author-name", "avatar": "author-avatar", "username": "author-username-1" - } + }, + "available_languages": null }, { "uuid": "article-uuid-3", @@ -40,9 +42,14 @@ "name": "author-name", "avatar": "author-avatar", "username": "author-username-2" - } + }, + "available_languages": null } ], + "language_code": { + "code": "EN", + "name": "English" + }, "pagination": { "total_pages": 1, "current_page": 1 diff --git a/presentation/http/blog/api/article/testdata/response-show.json b/presentation/http/blog/api/article/testdata/response-show.json index e6fc62fe..d1d58e25 100644 --- a/presentation/http/blog/api/article/testdata/response-show.json +++ b/presentation/http/blog/api/article/testdata/response-show.json @@ -1,5 +1,5 @@ { - "uuid": "test-uuid-1", + "correlation_uuid": "test-uuid-1", "cover": "", "video": "", "title": "test title", @@ -14,5 +14,15 @@ }, "tags": [], "view_count": 11, + "language_code": { + "code": "EN", + "name": "English" + }, + "available_languages": [ + { + "code": "EN", + "name": "English" + } + ], "elements": [] -} \ No newline at end of file +} diff --git a/presentation/http/blog/api/auth/verify_test.go b/presentation/http/blog/api/auth/verify_test.go index 1cb7b238..9466cdf9 100644 --- a/presentation/http/blog/api/auth/verify_test.go +++ b/presentation/http/blog/api/auth/verify_test.go @@ -16,6 +16,7 @@ import ( "github.com/khanzadimahdi/testproject/application/auth" "github.com/khanzadimahdi/testproject/application/auth/verify" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/config" "github.com/khanzadimahdi/testproject/domain/role" @@ -45,6 +46,7 @@ func TestVerifyHandler(t *testing.T) { userRepository users.MockUsersRepository roleRepository roles.MockRolesRepository configRepository configRepo.MockConfigRepository + languageResolver resolver.MockResolver hasher crypto.MockCrypto requestValidator validator.MockValidator translator translator.TranslatorMock @@ -54,11 +56,12 @@ func TestVerifyHandler(t *testing.T) { } r = verify.Request{ - Token: generateToken(t, j, u, time.Now().Add(10*time.Second), auth.RegistrationToken), - Name: "test name", - Username: "test-user-name", - Password: "test-password", - Repassword: "test-password", + Token: generateToken(t, j, u, time.Now().Add(10*time.Second), auth.RegistrationToken), + Name: "test name", + Username: "test-user-name", + LanguageCode: "EN", + Password: "test-password", + Repassword: "test-password", } roles = []role.Role{ @@ -93,6 +96,9 @@ func TestVerifyHandler(t *testing.T) { userRepository.On("Save", mock.Anything).Once().Return(u.UUID, nil) defer userRepository.AssertExpectations(t) + languageResolver.On("Verify", r.LanguageCode).Once().Return(true) + defer languageResolver.AssertExpectations(t) + hasher.On("Hash", []byte(r.Password), mock.AnythingOfType("[]uint8")).Once().Return([]byte("hashed-password"), nil) defer hasher.AssertExpectations(t) @@ -109,6 +115,7 @@ func TestVerifyHandler(t *testing.T) { &userRepository, &roleRepository, &configRepository, + &languageResolver, &hasher, j, &translator, @@ -138,6 +145,7 @@ func TestVerifyHandler(t *testing.T) { userRepository users.MockUsersRepository roleRepository roles.MockRolesRepository configRepository configRepo.MockConfigRepository + languageResolver resolver.MockResolver hasher crypto.MockCrypto requestValidator validator.MockValidator translator translator.TranslatorMock @@ -157,6 +165,7 @@ func TestVerifyHandler(t *testing.T) { &userRepository, &roleRepository, &configRepository, + &languageResolver, &hasher, j, &translator, @@ -192,6 +201,7 @@ func TestVerifyHandler(t *testing.T) { userRepository users.MockUsersRepository roleRepository roles.MockRolesRepository configRepository configRepo.MockConfigRepository + languageResolver resolver.MockResolver hasher crypto.MockCrypto requestValidator validator.MockValidator translator translator.TranslatorMock @@ -201,11 +211,12 @@ func TestVerifyHandler(t *testing.T) { } r = verify.Request{ - Token: generateToken(t, j, u, time.Now().Add(10*time.Second), auth.RegistrationToken), - Name: "test name", - Username: "test-user-name", - Password: "test-password", - Repassword: "test-password", + Token: generateToken(t, j, u, time.Now().Add(10*time.Second), auth.RegistrationToken), + Name: "test name", + Username: "test-user-name", + LanguageCode: "EN", + Password: "test-password", + Repassword: "test-password", } ) @@ -217,6 +228,7 @@ func TestVerifyHandler(t *testing.T) { &userRepository, &roleRepository, &configRepository, + &languageResolver, &hasher, j, &translator, diff --git a/presentation/http/blog/api/author/article/index.go b/presentation/http/blog/api/author/article/index.go index 08ff8ba0..c82fc56e 100644 --- a/presentation/http/blog/api/author/article/index.go +++ b/presentation/http/blog/api/author/article/index.go @@ -28,8 +28,9 @@ func NewIndexHandler(useCase *getArticlesByAuthor.UseCase) *indexHandler { // @Tags authors // @Accept json // @Produce json -// @Param identity path string true "Author UUID or username" -// @Param page query int false "Page number" default(1) +// @Param identity path string true "Author UUID or username" +// @Param page query int false "Page number" default(1) +// @Param language_code query string false "Language key (e.g. EN, FA)" default(EN) // @Success 200 {object} getArticlesByAuthor.Response // @Failure 400 {object} map[string]interface{} // @Failure 404 {object} map[string]interface{} @@ -44,7 +45,11 @@ func (h *indexHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { } } - request := &getArticlesByAuthor.Request{Page: page} + request := &getArticlesByAuthor.Request{ + Page: page, + LanguageCode: r.URL.Query().Get("language_code"), + } + identity := r.PathValue("identity") if _, err := uuid.FromString(identity); err == nil { request.AuthorUUID = identity diff --git a/presentation/http/blog/api/author/article/index_test.go b/presentation/http/blog/api/author/article/index_test.go index da5c0d06..ad16ac58 100644 --- a/presentation/http/blog/api/author/article/index_test.go +++ b/presentation/http/blog/api/author/article/index_test.go @@ -11,8 +11,10 @@ import ( "github.com/stretchr/testify/assert" getArticlesByAuthor "github.com/khanzadimahdi/testproject/application/article/getArticlesByAuthor" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/users" @@ -33,6 +35,7 @@ func TestIndexHandler(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator ) @@ -66,14 +69,19 @@ func TestIndexHandler(t *testing.T) { requestValidator.On("Validate", r).Once().Return(nil) defer requestValidator.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN", Name: "English"}, nil) + defer languageResolver.AssertExpectations(t) + userRepository.On("GetOneByIdentity", testUsername).Once().Return(u, nil) defer userRepository.AssertExpectations(t) - articlesRepository.On("CountPublishedByAuthor", testAuthorUUID).Once().Return(uint(len(fetched)), nil) - articlesRepository.On("GetPublishedByAuthor", testAuthorUUID, uint(0), uint(10)).Once().Return(fetched, nil) + articlesRepository.On("CountPublishedByAuthor", testAuthorUUID, "EN").Once().Return(uint(len(fetched)), nil) + articlesRepository.On("GetPublishedByAuthor", testAuthorUUID, "EN", uint(0), uint(10)).Once().Return(fetched, nil) + articlesRepository.On("GetPublishedLanguages", "").Return([]language.Language{}, nil) defer articlesRepository.AssertExpectations(t) - useCase := getArticlesByAuthor.NewUseCase(&articlesRepository, &userRepository, &requestValidator) + useCase := getArticlesByAuthor.NewUseCase(&articlesRepository, &userRepository, &languageResolver, &requestValidator) handler := NewIndexHandler(useCase) request := httptest.NewRequest(http.MethodGet, "/?page=1", nil) @@ -97,6 +105,7 @@ func TestIndexHandler(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator ) @@ -110,15 +119,19 @@ func TestIndexHandler(t *testing.T) { requestValidator.On("Validate", r).Once().Return(nil) defer requestValidator.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN", Name: "English"}, nil) + defer languageResolver.AssertExpectations(t) + userRepository.On("GetOne", testAuthorUUID).Once().Return(u, nil) userRepository.AssertNotCalled(t, "GetOneByIdentity") defer userRepository.AssertExpectations(t) - articlesRepository.On("CountPublishedByAuthor", testAuthorUUID).Once().Return(uint(0), nil) - articlesRepository.On("GetPublishedByAuthor", testAuthorUUID, uint(0), uint(10)).Once().Return(nil, nil) + articlesRepository.On("CountPublishedByAuthor", testAuthorUUID, "EN").Once().Return(uint(0), nil) + articlesRepository.On("GetPublishedByAuthor", testAuthorUUID, "EN", uint(0), uint(10)).Once().Return(nil, nil) defer articlesRepository.AssertExpectations(t) - useCase := getArticlesByAuthor.NewUseCase(&articlesRepository, &userRepository, &requestValidator) + useCase := getArticlesByAuthor.NewUseCase(&articlesRepository, &userRepository, &languageResolver, &requestValidator) handler := NewIndexHandler(useCase) request := httptest.NewRequest(http.MethodGet, "/?page=1", nil) @@ -138,6 +151,7 @@ func TestIndexHandler(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator ) @@ -149,10 +163,14 @@ func TestIndexHandler(t *testing.T) { requestValidator.On("Validate", r).Once().Return(nil) defer requestValidator.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN", Name: "English"}, nil) + defer languageResolver.AssertExpectations(t) + userRepository.On("GetOneByIdentity", "ghost").Once().Return(user.User{}, domain.ErrNotExists) defer userRepository.AssertExpectations(t) - useCase := getArticlesByAuthor.NewUseCase(&articlesRepository, &userRepository, &requestValidator) + useCase := getArticlesByAuthor.NewUseCase(&articlesRepository, &userRepository, &languageResolver, &requestValidator) handler := NewIndexHandler(useCase) request := httptest.NewRequest(http.MethodGet, "/?page=1", nil) @@ -175,6 +193,7 @@ func TestIndexHandler(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator ) @@ -186,10 +205,14 @@ func TestIndexHandler(t *testing.T) { requestValidator.On("Validate", r).Once().Return(nil) defer requestValidator.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN", Name: "English"}, nil) + defer languageResolver.AssertExpectations(t) + userRepository.On("GetOneByIdentity", testUsername).Once().Return(user.User{}, errors.New("boom")) defer userRepository.AssertExpectations(t) - useCase := getArticlesByAuthor.NewUseCase(&articlesRepository, &userRepository, &requestValidator) + useCase := getArticlesByAuthor.NewUseCase(&articlesRepository, &userRepository, &languageResolver, &requestValidator) handler := NewIndexHandler(useCase) request := httptest.NewRequest(http.MethodGet, "/?page=1", nil) diff --git a/presentation/http/blog/api/author/article/testdata/response-01.json b/presentation/http/blog/api/author/article/testdata/response-01.json index 7d601caf..cf7deef4 100644 --- a/presentation/http/blog/api/author/article/testdata/response-01.json +++ b/presentation/http/blog/api/author/article/testdata/response-01.json @@ -13,9 +13,14 @@ "video": "article-video-1", "title": "article-title-1", "excerpt": "article-excerpt-1", - "published_at": "2024-09-29T15:56:25Z" + "published_at": "2024-09-29T15:56:25Z", + "available_languages": null } ], + "language_code": { + "code": "EN", + "name": "English" + }, "pagination": { "total_pages": 1, "current_page": 1 diff --git a/presentation/http/blog/api/dashboard/article/create_test.go b/presentation/http/blog/api/dashboard/article/create_test.go index 02dddbb5..1ae87701 100644 --- a/presentation/http/blog/api/dashboard/article/create_test.go +++ b/presentation/http/blog/api/dashboard/article/create_test.go @@ -16,6 +16,7 @@ import ( "github.com/khanzadimahdi/testproject/domain/article" "github.com/khanzadimahdi/testproject/domain/user" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" "github.com/khanzadimahdi/testproject/infrastructure/validator" ) @@ -26,15 +27,17 @@ func TestCreateHandler(t *testing.T) { t.Parallel() var ( - articleRepository articles.MockArticlesRepository - requestValidator validator.MockValidator + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator r = createarticle.Request{ - Title: "test title", - Excerpt: "test excerpt", - Body: "test body", - AuthorUUID: "test-author-uuid", - Tags: []string{"tag1", "tag2"}, + Title: "test title", + Excerpt: "test excerpt", + Body: "test body", + AuthorUUID: "test-author-uuid", + Tags: []string{"tag1", "tag2"}, + LanguageCode: "EN", } u = user.User{ @@ -42,14 +45,15 @@ func TestCreateHandler(t *testing.T) { } a = article.Article{ - Cover: r.Cover, - Video: r.Video, - Title: r.Title, - Excerpt: r.Excerpt, - Body: r.Body, - PublishedAt: r.PublishedAt, - AuthorUUID: r.AuthorUUID, - Tags: r.Tags, + Cover: r.Cover, + Video: r.Video, + Title: r.Title, + Excerpt: r.Excerpt, + Body: r.Body, + PublishedAt: r.PublishedAt, + AuthorUUID: r.AuthorUUID, + Tags: r.Tags, + LanguageCode: r.LanguageCode, } au = "article-uuid" @@ -58,10 +62,13 @@ func TestCreateHandler(t *testing.T) { requestValidator.On("Validate", &r).Once().Return(nil) defer requestValidator.AssertExpectations(t) + languageRepository.On("Exists", "EN").Once().Return(true) + defer languageRepository.AssertExpectations(t) + articleRepository.On("Save", &a).Once().Return(au, nil) defer articleRepository.AssertExpectations(t) - handler := NewCreateHandler(createarticle.NewUseCase(&articleRepository, &requestValidator)) + handler := NewCreateHandler(createarticle.NewUseCase(&articleRepository, &languageRepository, &requestValidator)) var payload bytes.Buffer err := json.NewEncoder(&payload).Encode(r) @@ -85,8 +92,9 @@ func TestCreateHandler(t *testing.T) { t.Parallel() var ( - articleRepository articles.MockArticlesRepository - requestValidator validator.MockValidator + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator u = user.User{ UUID: "test-author-uuid", @@ -94,13 +102,14 @@ func TestCreateHandler(t *testing.T) { ) requestValidator.On("Validate", &createarticle.Request{AuthorUUID: u.UUID}).Once().Return(domain.ValidationErrors{ - "body": "body is required", - "excerpt": "excerpt is required", - "title": "title is required", + "body": "body is required", + "excerpt": "excerpt is required", + "title": "title is required", + "language_code": "language is required", }) defer requestValidator.AssertExpectations(t) - handler := NewCreateHandler(createarticle.NewUseCase(&articleRepository, &requestValidator)) + handler := NewCreateHandler(createarticle.NewUseCase(&articleRepository, &languageRepository, &requestValidator)) request := httptest.NewRequest(http.MethodGet, "/", bytes.NewBufferString("{}")) request = request.WithContext(auth.ToContext(request.Context(), &u)) diff --git a/presentation/http/blog/api/dashboard/article/testdata/create-article-validation-errors-response.json b/presentation/http/blog/api/dashboard/article/testdata/create-article-validation-errors-response.json index 33635f6d..50f8c028 100644 --- a/presentation/http/blog/api/dashboard/article/testdata/create-article-validation-errors-response.json +++ b/presentation/http/blog/api/dashboard/article/testdata/create-article-validation-errors-response.json @@ -2,6 +2,7 @@ "errors": { "body": "body is required", "excerpt": "excerpt is required", - "title": "title is required" + "title": "title is required", + "language_code": "language is required" } -} \ No newline at end of file +} diff --git a/presentation/http/blog/api/dashboard/article/testdata/show-article-response.json b/presentation/http/blog/api/dashboard/article/testdata/show-article-response.json index 2f82459a..dedcff95 100644 --- a/presentation/http/blog/api/dashboard/article/testdata/show-article-response.json +++ b/presentation/http/blog/api/dashboard/article/testdata/show-article-response.json @@ -1 +1 @@ -{"uuid":"article-uuid-1","cover":"","video":"","title":"article-title-1","excerpt":"excerpt-1","body":"body-1","published_at":"2024-10-11T04:27:44Z","author":{"uuid":"","name":"","avatar":"","username":""},"tags":["tag-1","tag-2"],"view_count":0} +{"uuid":"article-uuid-1","cover":"","video":"","title":"article-title-1","excerpt":"excerpt-1","body":"body-1","published_at":"2024-10-11T04:27:44Z","author":{"uuid":"","name":"","avatar":"","username":""},"tags":["tag-1","tag-2"],"view_count":0,"language_code":""} diff --git a/presentation/http/blog/api/dashboard/article/testdata/update-article-validation-errors-response.json b/presentation/http/blog/api/dashboard/article/testdata/update-article-validation-errors-response.json index 33635f6d..50f8c028 100644 --- a/presentation/http/blog/api/dashboard/article/testdata/update-article-validation-errors-response.json +++ b/presentation/http/blog/api/dashboard/article/testdata/update-article-validation-errors-response.json @@ -2,6 +2,7 @@ "errors": { "body": "body is required", "excerpt": "excerpt is required", - "title": "title is required" + "title": "title is required", + "language_code": "language is required" } -} \ No newline at end of file +} diff --git a/presentation/http/blog/api/dashboard/article/update_test.go b/presentation/http/blog/api/dashboard/article/update_test.go index f586fdbd..59f6f72b 100644 --- a/presentation/http/blog/api/dashboard/article/update_test.go +++ b/presentation/http/blog/api/dashboard/article/update_test.go @@ -17,6 +17,7 @@ import ( "github.com/khanzadimahdi/testproject/domain/article" "github.com/khanzadimahdi/testproject/domain/user" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" "github.com/khanzadimahdi/testproject/infrastructure/validator" ) @@ -27,27 +28,31 @@ func TestUpdateHandler(t *testing.T) { t.Parallel() var ( - articleRepository articles.MockArticlesRepository - requestValidator validator.MockValidator + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator r = updatearticle.Request{ - UUID: "test-article-uuid", - Title: "test title", - Excerpt: "test excerpt", - Body: "test body", - AuthorUUID: "test-author-uuid", - Tags: []string{"tag1", "tag2"}, + UUID: "test-article-uuid", + Title: "test title", + Excerpt: "test excerpt", + Body: "test body", + AuthorUUID: "test-author-uuid", + Tags: []string{"tag1", "tag2"}, + LanguageCode: "EN", } - a = article.Article{ - UUID: r.UUID, - Cover: r.Cover, - Video: r.Video, - Title: r.Title, - Excerpt: r.Excerpt, - Body: r.Body, - PublishedAt: r.PublishedAt, - AuthorUUID: r.AuthorUUID, - Tags: r.Tags, + existing = article.Article{UUID: r.UUID, LanguageCode: "EN"} + a = article.Article{ + UUID: r.UUID, + Cover: r.Cover, + Video: r.Video, + Title: r.Title, + Excerpt: r.Excerpt, + Body: r.Body, + PublishedAt: r.PublishedAt, + AuthorUUID: r.AuthorUUID, + Tags: r.Tags, + LanguageCode: r.LanguageCode, } u = user.User{ @@ -58,10 +63,14 @@ func TestUpdateHandler(t *testing.T) { requestValidator.On("Validate", &r).Once().Return(nil) defer requestValidator.AssertExpectations(t) + languageRepository.On("Exists", "EN").Once().Return(true) + defer languageRepository.AssertExpectations(t) + + articleRepository.On("GetOne", r.UUID).Once().Return(existing, nil) articleRepository.On("Save", &a).Once().Return(a.UUID, nil) defer articleRepository.AssertExpectations(t) - handler := NewUpdateHandler(updatearticle.NewUseCase(&articleRepository, &requestValidator)) + handler := NewUpdateHandler(updatearticle.NewUseCase(&articleRepository, &languageRepository, &requestValidator)) var payload bytes.Buffer err := json.NewEncoder(&payload).Encode(r) @@ -81,8 +90,9 @@ func TestUpdateHandler(t *testing.T) { t.Parallel() var ( - articleRepository articles.MockArticlesRepository - requestValidator validator.MockValidator + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator u = user.User{ UUID: "test-author-uuid", @@ -90,13 +100,14 @@ func TestUpdateHandler(t *testing.T) { ) requestValidator.On("Validate", &updatearticle.Request{AuthorUUID: u.UUID}).Once().Return(domain.ValidationErrors{ - "body": "body is required", - "excerpt": "excerpt is required", - "title": "title is required", + "body": "body is required", + "excerpt": "excerpt is required", + "title": "title is required", + "language_code": "language is required", }) defer requestValidator.AssertExpectations(t) - handler := NewUpdateHandler(updatearticle.NewUseCase(&articleRepository, &requestValidator)) + handler := NewUpdateHandler(updatearticle.NewUseCase(&articleRepository, &languageRepository, &requestValidator)) request := httptest.NewRequest(http.MethodPatch, "/", bytes.NewBufferString("{}")) request = request.WithContext(auth.ToContext(request.Context(), &u)) @@ -118,27 +129,31 @@ func TestUpdateHandler(t *testing.T) { t.Parallel() var ( - articleRepository articles.MockArticlesRepository - requestValidator validator.MockValidator + articleRepository articles.MockArticlesRepository + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator r = updatearticle.Request{ - UUID: "test-article-uuid", - Title: "test title", - Excerpt: "test excerpt", - Body: "test body", - AuthorUUID: "test-author-uuid", - Tags: []string{"tag1", "tag2"}, + UUID: "test-article-uuid", + Title: "test title", + Excerpt: "test excerpt", + Body: "test body", + AuthorUUID: "test-author-uuid", + Tags: []string{"tag1", "tag2"}, + LanguageCode: "EN", } - a = article.Article{ - UUID: r.UUID, - Cover: r.Cover, - Video: r.Video, - Title: r.Title, - Excerpt: r.Excerpt, - Body: r.Body, - PublishedAt: r.PublishedAt, - AuthorUUID: r.AuthorUUID, - Tags: r.Tags, + existing = article.Article{UUID: r.UUID, LanguageCode: "EN"} + a = article.Article{ + UUID: r.UUID, + Cover: r.Cover, + Video: r.Video, + Title: r.Title, + Excerpt: r.Excerpt, + Body: r.Body, + PublishedAt: r.PublishedAt, + AuthorUUID: r.AuthorUUID, + Tags: r.Tags, + LanguageCode: r.LanguageCode, } u = user.User{ @@ -149,10 +164,14 @@ func TestUpdateHandler(t *testing.T) { requestValidator.On("Validate", &r).Once().Return(nil) defer requestValidator.AssertExpectations(t) + languageRepository.On("Exists", "EN").Once().Return(true) + defer languageRepository.AssertExpectations(t) + + articleRepository.On("GetOne", r.UUID).Once().Return(existing, nil) articleRepository.On("Save", &a).Once().Return("", errors.New("unexpected error")) defer articleRepository.AssertExpectations(t) - handler := NewUpdateHandler(updatearticle.NewUseCase(&articleRepository, &requestValidator)) + handler := NewUpdateHandler(updatearticle.NewUseCase(&articleRepository, &languageRepository, &requestValidator)) var payload bytes.Buffer err := json.NewEncoder(&payload).Encode(r) diff --git a/presentation/http/blog/api/dashboard/config/show_test.go b/presentation/http/blog/api/dashboard/config/show_test.go index 4bbd8f36..b0e6d2e1 100644 --- a/presentation/http/blog/api/dashboard/config/show_test.go +++ b/presentation/http/blog/api/dashboard/config/show_test.go @@ -29,6 +29,7 @@ func TestShowHandler(t *testing.T) { loadedConfig = config.Config{ Revision: 1, UserDefaultRoleUUIDs: []string{"role1"}, + DefaultLanguageCode: "EN", } ) diff --git a/presentation/http/blog/api/dashboard/config/testdata/show-config-response.json b/presentation/http/blog/api/dashboard/config/testdata/show-config-response.json index 7e54276f..5213ae1f 100644 --- a/presentation/http/blog/api/dashboard/config/testdata/show-config-response.json +++ b/presentation/http/blog/api/dashboard/config/testdata/show-config-response.json @@ -2,5 +2,6 @@ "revision": 1, "user_default_roles": [ "role1" - ] + ], + "default_language_code": "EN" } \ No newline at end of file diff --git a/presentation/http/blog/api/dashboard/config/testdata/update-config-validation-errors-response.json b/presentation/http/blog/api/dashboard/config/testdata/update-config-validation-errors-response.json index 2ed80548..0d1d039a 100644 --- a/presentation/http/blog/api/dashboard/config/testdata/update-config-validation-errors-response.json +++ b/presentation/http/blog/api/dashboard/config/testdata/update-config-validation-errors-response.json @@ -1,5 +1,6 @@ { "errors": { - "user_default_roles": "user_default_roles is required" + "user_default_roles": "user_default_roles is required", + "default_language_code": "default_language_code is required" } } \ No newline at end of file diff --git a/presentation/http/blog/api/dashboard/config/update_test.go b/presentation/http/blog/api/dashboard/config/update_test.go index c56bc743..1e648491 100644 --- a/presentation/http/blog/api/dashboard/config/update_test.go +++ b/presentation/http/blog/api/dashboard/config/update_test.go @@ -16,6 +16,7 @@ import ( "github.com/khanzadimahdi/testproject/domain/config" "github.com/khanzadimahdi/testproject/domain/user" configMocks "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/config" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" "github.com/khanzadimahdi/testproject/infrastructure/validator" ) @@ -26,13 +27,15 @@ func TestUpdateHandler(t *testing.T) { t.Parallel() var ( - configRepository configMocks.MockConfigRepository - requestValidator validator.MockValidator + configRepository configMocks.MockConfigRepository + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator u = user.User{UUID: "auth-user-uuid"} r = updateConfig.Request{ - UserDefaultRoles: []string{"role1", "role2"}, + UserDefaultRoles: []string{"role1", "role2"}, + DefaultLanguageCode: "EN", } loadedConfig = config.Config{ @@ -43,17 +46,21 @@ func TestUpdateHandler(t *testing.T) { savedConfig = config.Config{ Revision: loadedConfig.Revision, UserDefaultRoleUUIDs: r.UserDefaultRoles, + DefaultLanguageCode: r.DefaultLanguageCode, } ) requestValidator.On("Validate", &r).Once().Return(nil) defer requestValidator.AssertExpectations(t) + languageRepository.On("Exists", r.DefaultLanguageCode).Once().Return(true) + defer languageRepository.AssertExpectations(t) + configRepository.On("GetLatestRevision").Once().Return(loadedConfig, nil) configRepository.On("Save", &savedConfig).Once().Return("new-revision-uuid", nil) defer configRepository.AssertExpectations(t) - handler := NewUpdateHandler(updateConfig.NewUseCase(&configRepository, &requestValidator)) + handler := NewUpdateHandler(updateConfig.NewUseCase(&configRepository, &languageRepository, &requestValidator)) var payload bytes.Buffer json.NewEncoder(&payload).Encode(r) @@ -72,18 +79,20 @@ func TestUpdateHandler(t *testing.T) { t.Parallel() var ( - configRepository configMocks.MockConfigRepository - requestValidator validator.MockValidator + configRepository configMocks.MockConfigRepository + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator u = user.User{UUID: "auth-user-uuid"} ) requestValidator.On("Validate", &updateConfig.Request{}).Once().Return(domain.ValidationErrors{ - "user_default_roles": "user_default_roles is required", + "user_default_roles": "user_default_roles is required", + "default_language_code": "default_language_code is required", }) defer requestValidator.AssertExpectations(t) - handler := NewUpdateHandler(updateConfig.NewUseCase(&configRepository, &requestValidator)) + handler := NewUpdateHandler(updateConfig.NewUseCase(&configRepository, &languageRepository, &requestValidator)) request := httptest.NewRequest(http.MethodPut, "/", bytes.NewBufferString("{}")) request = request.WithContext(auth.ToContext(request.Context(), &u)) diff --git a/presentation/http/blog/api/dashboard/language/create.go b/presentation/http/blog/api/dashboard/language/create.go new file mode 100644 index 00000000..b74548ad --- /dev/null +++ b/presentation/http/blog/api/dashboard/language/create.go @@ -0,0 +1,55 @@ +package language + +import ( + "encoding/json" + "errors" + "net/http" + + createlanguage "github.com/khanzadimahdi/testproject/application/dashboard/language/createLanguage" + "github.com/khanzadimahdi/testproject/domain" +) + +type createHandler struct { + useCase *createlanguage.UseCase +} + +func NewCreateHandler(useCase *createlanguage.UseCase) *createHandler { + return &createHandler{ + useCase: useCase, + } +} + +// @Summary Create language +// @Description add a new language via dashboard +// @Tags dashboard languages +// @Accept json +// @Produce json +// @Param body body createlanguage.Request true "Language data" +// @Success 201 {object} createlanguage.Response +// @Failure 400 {object} map[string]interface{} +// @Failure 500 {object} map[string]interface{} +// @Router /dashboard/languages [post] +func (h *createHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + var request createlanguage.Request + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + rw.WriteHeader(http.StatusBadRequest) + return + } + + response, err := h.useCase.Execute(&request) + + switch { + case errors.Is(err, domain.ErrNotExists): + rw.WriteHeader(http.StatusNotFound) + case err != nil: + rw.WriteHeader(http.StatusInternalServerError) + case len(response.ValidationErrors) > 0: + rw.Header().Add("Content-Type", "application/json") + rw.WriteHeader(http.StatusBadRequest) + json.NewEncoder(rw).Encode(response) + default: + rw.Header().Add("Content-Type", "application/json") + rw.WriteHeader(http.StatusCreated) + json.NewEncoder(rw).Encode(response) + } +} diff --git a/presentation/http/blog/api/dashboard/language/create_test.go b/presentation/http/blog/api/dashboard/language/create_test.go new file mode 100644 index 00000000..6b1f2d52 --- /dev/null +++ b/presentation/http/blog/api/dashboard/language/create_test.go @@ -0,0 +1,105 @@ +package language + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/khanzadimahdi/testproject/application/auth" + createlanguage "github.com/khanzadimahdi/testproject/application/dashboard/language/createLanguage" + "github.com/khanzadimahdi/testproject/domain" + "github.com/khanzadimahdi/testproject/domain/user" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" + "github.com/khanzadimahdi/testproject/infrastructure/validator" +) + +func TestCreateHandler(t *testing.T) { + t.Parallel() + + t.Run("create language", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator + + u = user.User{UUID: "auth-user-uuid"} + + r = createlanguage.Request{Code: "DE", Name: "Deutsch"} + ) + + requestValidator.On("Validate", &r).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageRepository.On("Exists", r.Code).Once().Return(false) + languageRepository.On("Save", mock.AnythingOfType("*language.Language")).Once().Return(r.Code, nil) + defer languageRepository.AssertExpectations(t) + + handler := NewCreateHandler(createlanguage.NewUseCase(&languageRepository, &requestValidator)) + + var payload bytes.Buffer + err := json.NewEncoder(&payload).Encode(r) + assert.NoError(t, err) + + request := httptest.NewRequest(http.MethodPost, "/", &payload) + request = request.WithContext(auth.ToContext(request.Context(), &u)) + response := httptest.NewRecorder() + + handler.ServeHTTP(response, request) + + expectedBody, err := os.ReadFile("testdata/create-language-response.json") + assert.NoError(t, err) + + assert.Equal(t, "application/json", response.Header().Get("content-type")) + assert.JSONEq(t, string(expectedBody), response.Body.String()) + assert.Equal(t, http.StatusCreated, response.Code) + }) + + t.Run("validation fails", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator + + u = user.User{UUID: "auth-user-uuid"} + + r = createlanguage.Request{} + + validationErrors = domain.ValidationErrors{ + "code": "required_field", + "name": "required_field", + } + ) + + requestValidator.On("Validate", &r).Once().Return(validationErrors) + defer requestValidator.AssertExpectations(t) + + handler := NewCreateHandler(createlanguage.NewUseCase(&languageRepository, &requestValidator)) + + var payload bytes.Buffer + err := json.NewEncoder(&payload).Encode(r) + assert.NoError(t, err) + + request := httptest.NewRequest(http.MethodPost, "/", &payload) + request = request.WithContext(auth.ToContext(request.Context(), &u)) + response := httptest.NewRecorder() + + handler.ServeHTTP(response, request) + + languageRepository.AssertNotCalled(t, "Save") + + expectedBody, err := os.ReadFile("testdata/create-language-validation-errors-response.json") + assert.NoError(t, err) + + assert.Equal(t, "application/json", response.Header().Get("content-type")) + assert.JSONEq(t, string(expectedBody), response.Body.String()) + assert.Equal(t, http.StatusBadRequest, response.Code) + }) +} diff --git a/presentation/http/blog/api/dashboard/language/delete.go b/presentation/http/blog/api/dashboard/language/delete.go new file mode 100644 index 00000000..104587dd --- /dev/null +++ b/presentation/http/blog/api/dashboard/language/delete.go @@ -0,0 +1,47 @@ +package language + +import ( + "errors" + "net/http" + + deletelanguage "github.com/khanzadimahdi/testproject/application/dashboard/language/deleteLanguage" + "github.com/khanzadimahdi/testproject/domain" +) + +type deleteHandler struct { + useCase *deletelanguage.UseCase +} + +func NewDeleteHandler(useCase *deletelanguage.UseCase) *deleteHandler { + return &deleteHandler{ + useCase: useCase, + } +} + +// @Summary Delete language +// @Description remove a language by code +// @Tags dashboard languages +// @Accept json +// @Produce json +// @Param code path string true "Language code" +// @Success 204 +// @Failure 404 {object} map[string]interface{} +// @Failure 500 {object} map[string]interface{} +// @Router /dashboard/languages/{code} [delete] +func (h *deleteHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + code := r.PathValue("code") + + request := &deletelanguage.Request{ + Code: code, + } + + err := h.useCase.Execute(request) + switch { + case errors.Is(err, domain.ErrNotExists): + rw.WriteHeader(http.StatusNotFound) + case err != nil: + rw.WriteHeader(http.StatusInternalServerError) + default: + rw.WriteHeader(http.StatusNoContent) + } +} diff --git a/presentation/http/blog/api/dashboard/language/delete_test.go b/presentation/http/blog/api/dashboard/language/delete_test.go new file mode 100644 index 00000000..848a6efe --- /dev/null +++ b/presentation/http/blog/api/dashboard/language/delete_test.go @@ -0,0 +1,45 @@ +package language + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/khanzadimahdi/testproject/application/auth" + deletelanguage "github.com/khanzadimahdi/testproject/application/dashboard/language/deleteLanguage" + "github.com/khanzadimahdi/testproject/domain/user" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" +) + +func TestDeleteHandler(t *testing.T) { + t.Parallel() + + t.Run("delete language", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + + u = user.User{UUID: "auth-user-uuid"} + + r = deletelanguage.Request{Code: "EN"} + ) + + languageRepository.On("Delete", r.Code).Once().Return(nil) + defer languageRepository.AssertExpectations(t) + + handler := NewDeleteHandler(deletelanguage.NewUseCase(&languageRepository)) + + request := httptest.NewRequest(http.MethodDelete, "/", nil) + request = request.WithContext(auth.ToContext(request.Context(), &u)) + request.SetPathValue("code", r.Code) + response := httptest.NewRecorder() + + handler.ServeHTTP(response, request) + + assert.Len(t, response.Body.Bytes(), 0) + assert.Equal(t, http.StatusNoContent, response.Code) + }) +} diff --git a/presentation/http/blog/api/dashboard/language/index.go b/presentation/http/blog/api/dashboard/language/index.go new file mode 100644 index 00000000..79cb58c0 --- /dev/null +++ b/presentation/http/blog/api/dashboard/language/index.go @@ -0,0 +1,53 @@ +package language + +import ( + "encoding/json" + "net/http" + "strconv" + "unsafe" + + getlanguages "github.com/khanzadimahdi/testproject/application/dashboard/language/getLanguages" +) + +type indexHandler struct { + useCase *getlanguages.UseCase +} + +func NewIndexHandler(useCase *getlanguages.UseCase) *indexHandler { + return &indexHandler{ + useCase: useCase, + } +} + +// @Summary List languages +// @Description paginated list of languages +// @Tags dashboard languages +// @Accept json +// @Produce json +// @Param page query int false "Page" default(1) +// @Success 200 {object} getlanguages.Response +// @Failure 500 {object} map[string]interface{} +// @Router /dashboard/languages [get] +func (h *indexHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + var page uint = 1 + if r.URL.Query().Has("page") { + parsedPage, err := strconv.ParseUint(r.URL.Query().Get("page"), 10, int(unsafe.Sizeof(page))) + if err == nil { + page = uint(parsedPage) + } + } + + request := &getlanguages.Request{ + Page: page, + } + + response, err := h.useCase.Execute(request) + switch { + case err != nil: + rw.WriteHeader(http.StatusInternalServerError) + default: + rw.Header().Add("Content-Type", "application/json") + rw.WriteHeader(http.StatusOK) + json.NewEncoder(rw).Encode(response) + } +} diff --git a/presentation/http/blog/api/dashboard/language/index_test.go b/presentation/http/blog/api/dashboard/language/index_test.go new file mode 100644 index 00000000..e9b963e9 --- /dev/null +++ b/presentation/http/blog/api/dashboard/language/index_test.go @@ -0,0 +1,54 @@ +package language + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/khanzadimahdi/testproject/application/auth" + getlanguages "github.com/khanzadimahdi/testproject/application/dashboard/language/getLanguages" + "github.com/khanzadimahdi/testproject/domain/language" + "github.com/khanzadimahdi/testproject/domain/user" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" +) + +func TestIndexHandler(t *testing.T) { + t.Parallel() + + t.Run("languages", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + + u = user.User{UUID: "auth-user-uuid"} + + l = []language.Language{ + {Code: "EN", Name: "English"}, + {Code: "FA", Name: "فارسی"}, + } + ) + + languageRepository.On("Count").Once().Return(uint(len(l)), nil) + languageRepository.On("GetAll", uint(0), uint(10)).Once().Return(l, nil) + defer languageRepository.AssertExpectations(t) + + handler := NewIndexHandler(getlanguages.NewUseCase(&languageRepository)) + + request := httptest.NewRequest(http.MethodGet, "/", nil) + request = request.WithContext(auth.ToContext(request.Context(), &u)) + response := httptest.NewRecorder() + + handler.ServeHTTP(response, request) + + expectedBody, err := os.ReadFile("testdata/index-languages-response.json") + assert.NoError(t, err) + + assert.Equal(t, "application/json", response.Header().Get("content-type")) + assert.JSONEq(t, string(expectedBody), response.Body.String()) + assert.Equal(t, http.StatusOK, response.Code) + }) +} diff --git a/presentation/http/blog/api/dashboard/language/show.go b/presentation/http/blog/api/dashboard/language/show.go new file mode 100644 index 00000000..491357d1 --- /dev/null +++ b/presentation/http/blog/api/dashboard/language/show.go @@ -0,0 +1,47 @@ +package language + +import ( + "encoding/json" + "errors" + "net/http" + + getlanguage "github.com/khanzadimahdi/testproject/application/dashboard/language/getLanguage" + "github.com/khanzadimahdi/testproject/domain" +) + +type showHandler struct { + useCase *getlanguage.UseCase +} + +func NewShowHandler(useCase *getlanguage.UseCase) *showHandler { + return &showHandler{ + useCase: useCase, + } +} + +// @Summary Get language +// @Description fetch language by code +// @Tags dashboard languages +// @Accept json +// @Produce json +// @Param code path string true "Language code" +// @Success 200 {object} getlanguage.Response +// @Failure 404 {object} map[string]interface{} +// @Failure 500 {object} map[string]interface{} +// @Router /dashboard/languages/{code} [get] +func (h *showHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + code := r.PathValue("code") + + response, err := h.useCase.Execute(code) + + switch { + case errors.Is(err, domain.ErrNotExists): + rw.WriteHeader(http.StatusNotFound) + case err != nil: + rw.WriteHeader(http.StatusInternalServerError) + default: + rw.Header().Add("Content-Type", "application/json") + rw.WriteHeader(http.StatusOK) + json.NewEncoder(rw).Encode(response) + } +} diff --git a/presentation/http/blog/api/dashboard/language/show_test.go b/presentation/http/blog/api/dashboard/language/show_test.go new file mode 100644 index 00000000..64fa715f --- /dev/null +++ b/presentation/http/blog/api/dashboard/language/show_test.go @@ -0,0 +1,75 @@ +package language + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/khanzadimahdi/testproject/application/auth" + getlanguage "github.com/khanzadimahdi/testproject/application/dashboard/language/getLanguage" + "github.com/khanzadimahdi/testproject/domain" + "github.com/khanzadimahdi/testproject/domain/language" + "github.com/khanzadimahdi/testproject/domain/user" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" +) + +func TestShowHandler(t *testing.T) { + t.Parallel() + + t.Run("show language", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + + u = user.User{UUID: "auth-user-uuid"} + l = language.Language{Code: "EN", Name: "English"} + ) + + languageRepository.On("GetOne", l.Code).Once().Return(l, nil) + defer languageRepository.AssertExpectations(t) + + handler := NewShowHandler(getlanguage.NewUseCase(&languageRepository)) + + request := httptest.NewRequest(http.MethodGet, "/", nil) + request = request.WithContext(auth.ToContext(request.Context(), &u)) + request.SetPathValue("code", l.Code) + response := httptest.NewRecorder() + + handler.ServeHTTP(response, request) + + expectedBody, err := os.ReadFile("testdata/show-language-response.json") + assert.NoError(t, err) + + assert.Equal(t, "application/json", response.Header().Get("content-type")) + assert.JSONEq(t, string(expectedBody), response.Body.String()) + assert.Equal(t, http.StatusOK, response.Code) + }) + + t.Run("language does not exist", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + + u = user.User{UUID: "auth-user-uuid"} + ) + + languageRepository.On("GetOne", "DE").Once().Return(language.Language{}, domain.ErrNotExists) + defer languageRepository.AssertExpectations(t) + + handler := NewShowHandler(getlanguage.NewUseCase(&languageRepository)) + + request := httptest.NewRequest(http.MethodGet, "/", nil) + request = request.WithContext(auth.ToContext(request.Context(), &u)) + request.SetPathValue("code", "DE") + response := httptest.NewRecorder() + + handler.ServeHTTP(response, request) + + assert.Equal(t, http.StatusNotFound, response.Code) + }) +} diff --git a/presentation/http/blog/api/dashboard/language/testdata/create-language-response.json b/presentation/http/blog/api/dashboard/language/testdata/create-language-response.json new file mode 100644 index 00000000..1a395cbd --- /dev/null +++ b/presentation/http/blog/api/dashboard/language/testdata/create-language-response.json @@ -0,0 +1,3 @@ +{ + "code": "DE" +} diff --git a/presentation/http/blog/api/dashboard/language/testdata/create-language-validation-errors-response.json b/presentation/http/blog/api/dashboard/language/testdata/create-language-validation-errors-response.json new file mode 100644 index 00000000..92b318e6 --- /dev/null +++ b/presentation/http/blog/api/dashboard/language/testdata/create-language-validation-errors-response.json @@ -0,0 +1,6 @@ +{ + "errors": { + "code": "required_field", + "name": "required_field" + } +} diff --git a/presentation/http/blog/api/dashboard/language/testdata/index-languages-response.json b/presentation/http/blog/api/dashboard/language/testdata/index-languages-response.json new file mode 100644 index 00000000..b824efed --- /dev/null +++ b/presentation/http/blog/api/dashboard/language/testdata/index-languages-response.json @@ -0,0 +1,16 @@ +{ + "items": [ + { + "code": "EN", + "name": "English" + }, + { + "code": "FA", + "name": "فارسی" + } + ], + "pagination": { + "total_pages": 1, + "current_page": 1 + } +} diff --git a/presentation/http/blog/api/dashboard/language/testdata/show-language-response.json b/presentation/http/blog/api/dashboard/language/testdata/show-language-response.json new file mode 100644 index 00000000..9aa8fb63 --- /dev/null +++ b/presentation/http/blog/api/dashboard/language/testdata/show-language-response.json @@ -0,0 +1,4 @@ +{ + "code": "EN", + "name": "English" +} diff --git a/presentation/http/blog/api/dashboard/language/update.go b/presentation/http/blog/api/dashboard/language/update.go new file mode 100644 index 00000000..a3eb5414 --- /dev/null +++ b/presentation/http/blog/api/dashboard/language/update.go @@ -0,0 +1,53 @@ +package language + +import ( + "encoding/json" + "errors" + "net/http" + + updatelanguage "github.com/khanzadimahdi/testproject/application/dashboard/language/updateLanguage" + "github.com/khanzadimahdi/testproject/domain" +) + +type updateHandler struct { + useCase *updatelanguage.UseCase +} + +func NewUpdateHandler(useCase *updatelanguage.UseCase) *updateHandler { + return &updateHandler{ + useCase: useCase, + } +} + +// @Summary Update language +// @Description modify an existing language +// @Tags dashboard languages +// @Accept json +// @Produce json +// @Param body body updatelanguage.Request true "Language update" +// @Success 204 +// @Failure 400 {object} map[string]interface{} +// @Failure 404 {object} map[string]interface{} +// @Failure 500 {object} map[string]interface{} +// @Router /dashboard/languages [put] +func (h *updateHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + var request updatelanguage.Request + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + rw.WriteHeader(http.StatusBadRequest) + return + } + + response, err := h.useCase.Execute(&request) + switch { + case errors.Is(err, domain.ErrNotExists): + rw.WriteHeader(http.StatusNotFound) + case err != nil: + rw.WriteHeader(http.StatusInternalServerError) + case response != nil && len(response.ValidationErrors) > 0: + rw.Header().Add("Content-Type", "application/json") + rw.WriteHeader(http.StatusBadRequest) + json.NewEncoder(rw).Encode(response) + default: + rw.WriteHeader(http.StatusNoContent) + } +} diff --git a/presentation/http/blog/api/dashboard/language/update_test.go b/presentation/http/blog/api/dashboard/language/update_test.go new file mode 100644 index 00000000..1a86dbe7 --- /dev/null +++ b/presentation/http/blog/api/dashboard/language/update_test.go @@ -0,0 +1,91 @@ +package language + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/khanzadimahdi/testproject/application/auth" + updatelanguage "github.com/khanzadimahdi/testproject/application/dashboard/language/updateLanguage" + "github.com/khanzadimahdi/testproject/domain/user" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" + "github.com/khanzadimahdi/testproject/infrastructure/validator" +) + +func TestUpdateHandler(t *testing.T) { + t.Parallel() + + t.Run("update language", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator + + u = user.User{UUID: "auth-user-uuid"} + + r = updatelanguage.Request{Code: "EN", Name: "English (US)"} + ) + + requestValidator.On("Validate", &r).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageRepository.On("Exists", r.Code).Once().Return(true) + languageRepository.On("Save", mock.AnythingOfType("*language.Language")).Once().Return(r.Code, nil) + defer languageRepository.AssertExpectations(t) + + handler := NewUpdateHandler(updatelanguage.NewUseCase(&languageRepository, &requestValidator)) + + var payload bytes.Buffer + err := json.NewEncoder(&payload).Encode(r) + assert.NoError(t, err) + + request := httptest.NewRequest(http.MethodPut, "/", &payload) + request = request.WithContext(auth.ToContext(request.Context(), &u)) + response := httptest.NewRecorder() + + handler.ServeHTTP(response, request) + + assert.Len(t, response.Body.Bytes(), 0) + assert.Equal(t, http.StatusNoContent, response.Code) + }) + + t.Run("language does not exist", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + requestValidator validator.MockValidator + + u = user.User{UUID: "auth-user-uuid"} + + r = updatelanguage.Request{Code: "DE", Name: "Deutsch"} + ) + + requestValidator.On("Validate", &r).Once().Return(nil) + defer requestValidator.AssertExpectations(t) + + languageRepository.On("Exists", r.Code).Once().Return(false) + defer languageRepository.AssertExpectations(t) + + handler := NewUpdateHandler(updatelanguage.NewUseCase(&languageRepository, &requestValidator)) + + var payload bytes.Buffer + err := json.NewEncoder(&payload).Encode(r) + assert.NoError(t, err) + + request := httptest.NewRequest(http.MethodPut, "/", &payload) + request = request.WithContext(auth.ToContext(request.Context(), &u)) + response := httptest.NewRecorder() + + handler.ServeHTTP(response, request) + + languageRepository.AssertNotCalled(t, "Save") + assert.Equal(t, http.StatusNotFound, response.Code) + }) +} diff --git a/presentation/http/blog/api/dashboard/profile/getprofile_test.go b/presentation/http/blog/api/dashboard/profile/getprofile_test.go index c2c32e4e..39040686 100644 --- a/presentation/http/blog/api/dashboard/profile/getprofile_test.go +++ b/presentation/http/blog/api/dashboard/profile/getprofile_test.go @@ -28,11 +28,12 @@ func TestGetProfileHandler(t *testing.T) { userUUID = "user-uuid" u = user.User{ - UUID: userUUID, - Name: "test name", - Avatar: "test-avatar", - Email: "test@test.com", - Username: "test-username", + UUID: userUUID, + Name: "test name", + Avatar: "test-avatar", + Email: "test@test.com", + Username: "test-username", + LanguageCode: "EN", } ) diff --git a/presentation/http/blog/api/dashboard/profile/testdata/get-profile-response.json b/presentation/http/blog/api/dashboard/profile/testdata/get-profile-response.json index cc0470db..b7799df4 100644 --- a/presentation/http/blog/api/dashboard/profile/testdata/get-profile-response.json +++ b/presentation/http/blog/api/dashboard/profile/testdata/get-profile-response.json @@ -3,5 +3,6 @@ "name": "test name", "avatar": "test-avatar", "email": "test@test.com", - "username": "test-username" + "username": "test-username", + "language_code": "EN" } \ No newline at end of file diff --git a/presentation/http/blog/api/dashboard/profile/updateprofile_test.go b/presentation/http/blog/api/dashboard/profile/updateprofile_test.go index 1478d883..f105cb4e 100644 --- a/presentation/http/blog/api/dashboard/profile/updateprofile_test.go +++ b/presentation/http/blog/api/dashboard/profile/updateprofile_test.go @@ -13,6 +13,7 @@ import ( "github.com/khanzadimahdi/testproject/application/auth" "github.com/khanzadimahdi/testproject/application/dashboard/profile/updateprofile" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/user" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/users" @@ -28,23 +29,26 @@ func TestUpdateProfileHandler(t *testing.T) { var ( userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator translator translator.TranslatorMock r = updateprofile.Request{ - UserUUID: "test-user-uuid", - Name: "John Doe", - Avatar: "test-avatar", - Email: "test@test.com", - Username: "john.doe", + UserUUID: "test-user-uuid", + Name: "John Doe", + Avatar: "test-avatar", + Email: "test@test.com", + Username: "john.doe", + LanguageCode: "EN", } u = user.User{ - UUID: r.UserUUID, - Name: r.Name, - Avatar: r.Avatar, - Email: r.Email, - Username: r.Username, + UUID: r.UserUUID, + Name: r.Name, + Avatar: r.Avatar, + Email: r.Email, + Username: r.Username, + LanguageCode: r.LanguageCode, } ) @@ -57,7 +61,10 @@ func TestUpdateProfileHandler(t *testing.T) { userRepository.On("Save", &u).Once().Return(r.UserUUID, nil) defer userRepository.AssertExpectations(t) - handler := NewUpdateProfileHandler(updateprofile.NewUseCase(&userRepository, &requestValidator, &translator)) + languageResolver.On("Verify", r.LanguageCode).Once().Return(true) + defer languageResolver.AssertExpectations(t) + + handler := NewUpdateProfileHandler(updateprofile.NewUseCase(&userRepository, &languageResolver, &requestValidator, &translator)) var payload bytes.Buffer err := json.NewEncoder(&payload).Encode(r) @@ -80,6 +87,7 @@ func TestUpdateProfileHandler(t *testing.T) { var ( userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator translator translator.TranslatorMock @@ -95,7 +103,9 @@ func TestUpdateProfileHandler(t *testing.T) { }) defer requestValidator.AssertExpectations(t) - handler := NewUpdateProfileHandler(updateprofile.NewUseCase(&userRepository, &requestValidator, &translator)) + languageResolver.AssertNotCalled(t, "Verify") + + handler := NewUpdateProfileHandler(updateprofile.NewUseCase(&userRepository, &languageResolver, &requestValidator, &translator)) request := httptest.NewRequest(http.MethodPatch, "/", bytes.NewBufferString("{}")) request = request.WithContext(auth.ToContext(request.Context(), &u)) @@ -122,23 +132,26 @@ func TestUpdateProfileHandler(t *testing.T) { var ( userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator translator translator.TranslatorMock r = updateprofile.Request{ - UserUUID: "test-user-uuid", - Name: "John Doe", - Avatar: "test-avatar", - Email: "test@test.com", - Username: "john.doe", + UserUUID: "test-user-uuid", + Name: "John Doe", + Avatar: "test-avatar", + Email: "test@test.com", + Username: "john.doe", + LanguageCode: "EN", } u = user.User{ - UUID: r.UserUUID, - Name: r.Name, - Avatar: r.Avatar, - Email: r.Email, - Username: r.Username, + UUID: r.UserUUID, + Name: r.Name, + Avatar: r.Avatar, + Email: r.Email, + Username: r.Username, + LanguageCode: r.LanguageCode, } ) @@ -150,7 +163,10 @@ func TestUpdateProfileHandler(t *testing.T) { userRepository.On("GetOne", r.UserUUID).Once().Return(u, domain.ErrNotExists) defer userRepository.AssertExpectations(t) - handler := NewUpdateProfileHandler(updateprofile.NewUseCase(&userRepository, &requestValidator, &translator)) + languageResolver.On("Verify", r.LanguageCode).Once().Return(true) + defer languageResolver.AssertExpectations(t) + + handler := NewUpdateProfileHandler(updateprofile.NewUseCase(&userRepository, &languageResolver, &requestValidator, &translator)) var payload bytes.Buffer err := json.NewEncoder(&payload).Encode(r) @@ -174,23 +190,26 @@ func TestUpdateProfileHandler(t *testing.T) { var ( userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator translator translator.TranslatorMock r = updateprofile.Request{ - UserUUID: "test-user-uuid", - Name: "John Doe", - Avatar: "test-avatar", - Email: "test@test.com", - Username: "john.doe", + UserUUID: "test-user-uuid", + Name: "John Doe", + Avatar: "test-avatar", + Email: "test@test.com", + Username: "john.doe", + LanguageCode: "EN", } u = user.User{ - UUID: r.UserUUID, - Name: r.Name, - Avatar: r.Avatar, - Email: r.Email, - Username: r.Username, + UUID: r.UserUUID, + Name: r.Name, + Avatar: r.Avatar, + Email: r.Email, + Username: r.Username, + LanguageCode: r.LanguageCode, } ) @@ -200,7 +219,9 @@ func TestUpdateProfileHandler(t *testing.T) { userRepository.On("GetOneByIdentity", r.Email).Once().Return(u, errors.New("unexpected error")) defer userRepository.AssertExpectations(t) - handler := NewUpdateProfileHandler(updateprofile.NewUseCase(&userRepository, &requestValidator, &translator)) + languageResolver.AssertNotCalled(t, "Verify") + + handler := NewUpdateProfileHandler(updateprofile.NewUseCase(&userRepository, &languageResolver, &requestValidator, &translator)) var payload bytes.Buffer err := json.NewEncoder(&payload).Encode(r) diff --git a/presentation/http/blog/api/hashtag/show.go b/presentation/http/blog/api/hashtag/show.go index 4778d8a4..ecd802cf 100644 --- a/presentation/http/blog/api/hashtag/show.go +++ b/presentation/http/blog/api/hashtag/show.go @@ -21,11 +21,12 @@ func NewShowHandler(useCase *getArticlesByHashtag.UseCase) *showHandler { // @Summary List articles by hashtag // @Description return a page of the most recent published articles with the given hashtag -// @Tags hashtags -// @Accept json +// @Tags hashtags +// @Accept json // @Produce json -// @Param hashtag path string true "Hashtag" -// @Param page query int false "Page number" default(1) +// @Param hashtag path string true "Hashtag" +// @Param page query int false "Page number" default(1) +// @Param language_code query string false "Language key (e.g. EN, FA)" default(EN) // @Success 200 {object} getArticlesByHashtag.Response // @Failure 400 {object} map[string]interface{} // @Failure 500 {object} map[string]interface{} @@ -42,8 +43,9 @@ func (h *showHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { hashtag := r.PathValue("hashtag") request := &getArticlesByHashtag.Request{ - Page: page, - Hashtag: hashtag, + Page: page, + Hashtag: hashtag, + LanguageCode: r.URL.Query().Get("language_code"), } response, err := h.useCase.Execute(request) diff --git a/presentation/http/blog/api/hashtag/show_test.go b/presentation/http/blog/api/hashtag/show_test.go index 67a1e993..1c798a2d 100644 --- a/presentation/http/blog/api/hashtag/show_test.go +++ b/presentation/http/blog/api/hashtag/show_test.go @@ -11,8 +11,10 @@ import ( "github.com/stretchr/testify/assert" getArticlesByHashtag "github.com/khanzadimahdi/testproject/application/article/getArticlesByHashtag" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/users" @@ -28,6 +30,7 @@ func TestShowHandler(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator ) @@ -77,19 +80,25 @@ func TestShowHandler(t *testing.T) { {UUID: "author-uuid-2", Name: "author-name", Avatar: "author-avatar", Username: "author-username-2"}, } - articlesRepository.On("CountPublishedByHashtags", []string{hashtag}).Once().Return(uint(len(articles)), nil) + articlesRepository.On("CountPublishedByHashtags", []string{hashtag}, "EN").Once().Return(uint(len(articles)), nil) defer articlesRepository.AssertExpectations(t) - articlesRepository.On("GetPublishedByHashtags", []string{hashtag}, uint(0), uint(10)).Once().Return(articles, nil) + articlesRepository.On("GetPublishedByHashtags", []string{hashtag}, "EN", uint(0), uint(10)).Once().Return(articles, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetByUUIDs", []string{"author-uuid-1", "author-uuid-1", "author-uuid-2"}).Once().Return(users, nil) defer userRepository.AssertExpectations(t) + articlesRepository.On("GetPublishedLanguages", "").Return([]language.Language{}, nil) + requestValidator.On("Validate", r).Once().Return(nil) defer requestValidator.AssertExpectations(t) - useCase := getArticlesByHashtag.NewUseCase(&articlesRepository, &userRepository, &requestValidator) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN", Name: "English"}, nil) + defer languageResolver.AssertExpectations(t) + + useCase := getArticlesByHashtag.NewUseCase(&articlesRepository, &userRepository, &languageResolver, &requestValidator) handler := NewShowHandler(useCase) request := httptest.NewRequest(http.MethodGet, "/", nil) @@ -113,6 +122,7 @@ func TestShowHandler(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator ) @@ -127,7 +137,7 @@ func TestShowHandler(t *testing.T) { requestValidator.On("Validate", r).Once().Return(validationErrors) defer requestValidator.AssertExpectations(t) - useCase := getArticlesByHashtag.NewUseCase(&articlesRepository, &userRepository, &requestValidator) + useCase := getArticlesByHashtag.NewUseCase(&articlesRepository, &userRepository, &languageResolver, &requestValidator) handler := NewShowHandler(useCase) request := httptest.NewRequest(http.MethodGet, "/", nil) @@ -153,6 +163,7 @@ func TestShowHandler(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator ) @@ -163,10 +174,10 @@ func TestShowHandler(t *testing.T) { Hashtag: hashtag, } - articlesRepository.On("CountPublishedByHashtags", []string{hashtag}).Once().Return(uint(0), nil) + articlesRepository.On("CountPublishedByHashtags", []string{hashtag}, "EN").Once().Return(uint(0), nil) defer articlesRepository.AssertExpectations(t) - articlesRepository.On("GetPublishedByHashtags", []string{hashtag}, uint(0), uint(10)).Once().Return(nil, nil) + articlesRepository.On("GetPublishedByHashtags", []string{hashtag}, "EN", uint(0), uint(10)).Once().Return(nil, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetByUUIDs", []string{}).Once().Return([]user.User{}, nil) @@ -175,7 +186,11 @@ func TestShowHandler(t *testing.T) { requestValidator.On("Validate", r).Once().Return(nil) defer requestValidator.AssertExpectations(t) - useCase := getArticlesByHashtag.NewUseCase(&articlesRepository, &userRepository, &requestValidator) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN", Name: "English"}, nil) + defer languageResolver.AssertExpectations(t) + + useCase := getArticlesByHashtag.NewUseCase(&articlesRepository, &userRepository, &languageResolver, &requestValidator) handler := NewShowHandler(useCase) request := httptest.NewRequest(http.MethodGet, "/", nil) @@ -199,6 +214,7 @@ func TestShowHandler(t *testing.T) { var ( articlesRepository articles.MockArticlesRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver requestValidator validator.MockValidator ) @@ -209,16 +225,20 @@ func TestShowHandler(t *testing.T) { Hashtag: hashtag, } - articlesRepository.On("CountPublishedByHashtags", []string{hashtag}).Once().Return(uint(5), nil) + articlesRepository.On("CountPublishedByHashtags", []string{hashtag}, "EN").Once().Return(uint(5), nil) defer articlesRepository.AssertExpectations(t) - articlesRepository.On("GetPublishedByHashtags", []string{hashtag}, uint(0), uint(10)).Once().Return(nil, errors.New("some error happened")) + articlesRepository.On("GetPublishedByHashtags", []string{hashtag}, "EN", uint(0), uint(10)).Once().Return(nil, errors.New("some error happened")) defer articlesRepository.AssertExpectations(t) requestValidator.On("Validate", r).Once().Return(nil) defer requestValidator.AssertExpectations(t) - useCase := getArticlesByHashtag.NewUseCase(&articlesRepository, &userRepository, &requestValidator) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN", Name: "English"}, nil) + defer languageResolver.AssertExpectations(t) + + useCase := getArticlesByHashtag.NewUseCase(&articlesRepository, &userRepository, &languageResolver, &requestValidator) handler := NewShowHandler(useCase) request := httptest.NewRequest(http.MethodGet, "/", nil) diff --git a/presentation/http/blog/api/hashtag/testdata/response-01.txt b/presentation/http/blog/api/hashtag/testdata/response-01.txt index 2235a86a..422e3bca 100644 --- a/presentation/http/blog/api/hashtag/testdata/response-01.txt +++ b/presentation/http/blog/api/hashtag/testdata/response-01.txt @@ -12,7 +12,8 @@ "name": "author-name", "avatar": "author-avatar", "username": "author-username-1" - } + }, + "available_languages": null }, { "uuid": "article-uuid-2", @@ -26,7 +27,8 @@ "name": "author-name", "avatar": "author-avatar", "username": "author-username-1" - } + }, + "available_languages": null }, { "uuid": "article-uuid-3", @@ -40,9 +42,14 @@ "name": "author-name", "avatar": "author-avatar", "username": "author-username-2" - } + }, + "available_languages": null } ], + "language_code": { + "code": "EN", + "name": "English" + }, "pagination": { "total_pages": 1, "current_page": 1 diff --git a/presentation/http/blog/api/hashtag/testdata/response-02.txt b/presentation/http/blog/api/hashtag/testdata/response-02.txt index 41ed6ad2..aed95918 100644 --- a/presentation/http/blog/api/hashtag/testdata/response-02.txt +++ b/presentation/http/blog/api/hashtag/testdata/response-02.txt @@ -1,5 +1,9 @@ { "items": [], + "language_code": { + "code": "EN", + "name": "English" + }, "pagination": { "total_pages": 0, "current_page": 1 diff --git a/presentation/http/blog/api/hashtag/testdata/response-validation-failed.txt b/presentation/http/blog/api/hashtag/testdata/response-validation-failed.txt index fe59bb3f..e8e2422d 100755 --- a/presentation/http/blog/api/hashtag/testdata/response-validation-failed.txt +++ b/presentation/http/blog/api/hashtag/testdata/response-validation-failed.txt @@ -3,6 +3,10 @@ "hashtag": "this field is required" }, "items": null, + "language_code": { + "code": "", + "name": "" + }, "pagination": { "total_pages": 0, "current_page": 0 diff --git a/presentation/http/blog/api/home/home.go b/presentation/http/blog/api/home/home.go index b1ac7daa..99fc4665 100644 --- a/presentation/http/blog/api/home/home.go +++ b/presentation/http/blog/api/home/home.go @@ -19,14 +19,17 @@ func NewHomeHandler(useCase *home.UseCase) *homeHandler { // @Summary Application home endpoint // @Description returns the contents used for home page -// @Tags home -// @Accept json +// @Tags home +// @Accept json // @Produce json -// @Success 200 {object} home.Response -// @Failure 500 {object} map[string]interface{} +// @Param language query string false "Language key (e.g. EN, FA)" default(EN) +// @Success 200 {object} home.Response +// @Failure 500 {object} map[string]interface{} // @Router /home [get] func (h *homeHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - response, err := h.useCase.Execute() + response, err := h.useCase.Execute(&home.Request{ + LanguageCode: r.URL.Query().Get("language_code"), + }) switch { case err != nil: diff --git a/presentation/http/blog/api/home/home_test.go b/presentation/http/blog/api/home/home_test.go index 90ca8421..b7205f1d 100644 --- a/presentation/http/blog/api/home/home_test.go +++ b/presentation/http/blog/api/home/home_test.go @@ -12,7 +12,9 @@ import ( "github.com/khanzadimahdi/testproject/application/element" "github.com/khanzadimahdi/testproject/application/home" + "github.com/khanzadimahdi/testproject/application/language/resolver" "github.com/khanzadimahdi/testproject/domain/article" + "github.com/khanzadimahdi/testproject/domain/language" "github.com/khanzadimahdi/testproject/domain/user" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/articles" "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/elements" @@ -29,6 +31,7 @@ func TestHomeHandler(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver ) publishedAt, err := time.Parse(time.RFC3339, "2024-09-29T15:56:25Z") @@ -70,9 +73,9 @@ func TestHomeHandler(t *testing.T) { {UUID: "author-uuid-2", Name: "author-name", Avatar: "author-avatar", Username: "author-username-2"}, } - articlesRepository.On("GetMostViewed", uint(4)).Once().Return(articles, nil) - articlesRepository.On("GetAllPublished", uint(0), uint(3)).Once().Return(articles, nil) - articlesRepository.On("GetByUUIDs", []string{}).Once().Return(nil, nil) + articlesRepository.On("GetMostViewed", "EN", uint(4)).Once().Return(articles, nil) + articlesRepository.On("GetAllPublished", "EN", uint(0), uint(3)).Once().Return(articles, nil) + articlesRepository.On("GetByCorrelationUUIDs", []string{}, "EN").Once().Return(nil, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetByUUIDs", []string{"author-uuid-1", "author-uuid-1", "author-uuid-2", "author-uuid-1", "author-uuid-1", "author-uuid-2"}).Once().Return(users, nil) @@ -82,8 +85,12 @@ func TestHomeHandler(t *testing.T) { elementsRepository.On("GetByVenues", []string{"home"}).Once().Return(nil, nil) defer elementsRepository.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN", Name: "English"}, nil) + defer languageResolver.AssertExpectations(t) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - useCase := home.NewUseCase(&articlesRepository, &userRepository, elementRetriever) + useCase := home.NewUseCase(&articlesRepository, &userRepository, elementRetriever, &languageResolver) handler := NewHomeHandler(useCase) request := httptest.NewRequest(http.MethodGet, "/", nil) @@ -106,11 +113,12 @@ func TestHomeHandler(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver ) - articlesRepository.On("GetMostViewed", uint(4)).Once().Return([]article.Article{}, nil) - articlesRepository.On("GetAllPublished", uint(0), uint(3)).Once().Return([]article.Article{}, nil) - articlesRepository.On("GetByUUIDs", []string{}).Once().Return([]article.Article{}, nil) + articlesRepository.On("GetMostViewed", "EN", uint(4)).Once().Return([]article.Article{}, nil) + articlesRepository.On("GetAllPublished", "EN", uint(0), uint(3)).Once().Return([]article.Article{}, nil) + articlesRepository.On("GetByCorrelationUUIDs", []string{}, "EN").Once().Return([]article.Article{}, nil) defer articlesRepository.AssertExpectations(t) userRepository.On("GetByUUIDs", []string{}).Return([]user.User{}, nil) @@ -119,8 +127,12 @@ func TestHomeHandler(t *testing.T) { elementsRepository.On("GetByVenues", []string{"home"}).Once().Return(nil, nil) defer elementsRepository.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN", Name: "English"}, nil) + defer languageResolver.AssertExpectations(t) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - useCase := home.NewUseCase(&articlesRepository, &userRepository, elementRetriever) + useCase := home.NewUseCase(&articlesRepository, &userRepository, elementRetriever, &languageResolver) handler := NewHomeHandler(useCase) request := httptest.NewRequest(http.MethodGet, "/", nil) @@ -141,13 +153,18 @@ func TestHomeHandler(t *testing.T) { articlesRepository articles.MockArticlesRepository elementsRepository elements.MockElementsRepository userRepository users.MockUsersRepository + languageResolver resolver.MockResolver ) - articlesRepository.On("GetMostViewed", uint(4)).Once().Return(nil, errors.New("an error has happened")) + articlesRepository.On("GetMostViewed", "EN", uint(4)).Once().Return(nil, errors.New("an error has happened")) defer articlesRepository.AssertExpectations(t) + languageResolver.On("DefaultCode").Once().Return("EN", nil) + languageResolver.On("Resolve", "EN").Once().Return(language.Language{Code: "EN", Name: "English"}, nil) + defer languageResolver.AssertExpectations(t) + elementRetriever := element.NewRetriever(&articlesRepository, &elementsRepository, &userRepository) - useCase := home.NewUseCase(&articlesRepository, &userRepository, elementRetriever) + useCase := home.NewUseCase(&articlesRepository, &userRepository, elementRetriever, &languageResolver) handler := NewHomeHandler(useCase) request := httptest.NewRequest(http.MethodGet, "/", nil) @@ -156,7 +173,7 @@ func TestHomeHandler(t *testing.T) { handler.ServeHTTP(response, request) articlesRepository.AssertNotCalled(t, "GetAllPublished") - articlesRepository.AssertNotCalled(t, "GetByUUIDs") + articlesRepository.AssertNotCalled(t, "GetByCorrelationUUIDs") userRepository.AssertNotCalled(t, "GetByUUIDs") elementsRepository.AssertNotCalled(t, "GetByVenues") diff --git a/presentation/http/blog/api/home/testdata/response-01.txt b/presentation/http/blog/api/home/testdata/response-01.txt index 7a00879f..9bbfd1db 100644 --- a/presentation/http/blog/api/home/testdata/response-01.txt +++ b/presentation/http/blog/api/home/testdata/response-01.txt @@ -95,5 +95,9 @@ "tags": null } ], - "elements": [] + "elements": [], + "language_code": { + "code": "EN", + "name": "English" + } } diff --git a/presentation/http/blog/api/home/testdata/response-02.txt b/presentation/http/blog/api/home/testdata/response-02.txt index 6d2891bf..09300952 100644 --- a/presentation/http/blog/api/home/testdata/response-02.txt +++ b/presentation/http/blog/api/home/testdata/response-02.txt @@ -1,5 +1,9 @@ { "all": [], "popular": [], - "elements": [] + "elements": [], + "language_code": { + "code": "EN", + "name": "English" + } } \ No newline at end of file diff --git a/presentation/http/blog/api/language/index.go b/presentation/http/blog/api/language/index.go new file mode 100644 index 00000000..7e609ec4 --- /dev/null +++ b/presentation/http/blog/api/language/index.go @@ -0,0 +1,38 @@ +package language + +import ( + "encoding/json" + "net/http" + + getlanguages "github.com/khanzadimahdi/testproject/application/language/getLanguages" +) + +type indexHandler struct { + useCase *getlanguages.UseCase +} + +func NewIndexHandler(useCase *getlanguages.UseCase) *indexHandler { + return &indexHandler{ + useCase: useCase, + } +} + +// @Summary List languages +// @Description retrieve all available languages +// @Tags languages +// @Accept json +// @Produce json +// @Success 200 {object} getlanguages.Response +// @Failure 500 {object} map[string]interface{} +// @Router /languages [get] +func (h *indexHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + response, err := h.useCase.Execute() + switch { + case err != nil: + rw.WriteHeader(http.StatusInternalServerError) + default: + rw.Header().Add("Content-Type", "application/json") + rw.WriteHeader(http.StatusOK) + json.NewEncoder(rw).Encode(response) + } +} diff --git a/presentation/http/blog/api/language/index_test.go b/presentation/http/blog/api/language/index_test.go new file mode 100644 index 00000000..051e803c --- /dev/null +++ b/presentation/http/blog/api/language/index_test.go @@ -0,0 +1,76 @@ +package language + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + getlanguages "github.com/khanzadimahdi/testproject/application/language/getLanguages" + "github.com/khanzadimahdi/testproject/application/language/resolver" + "github.com/khanzadimahdi/testproject/domain/language" + "github.com/khanzadimahdi/testproject/infrastructure/repository/mocks/languages" +) + +func TestIndexHandler(t *testing.T) { + t.Parallel() + + t.Run("languages", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + languageResolver resolver.MockResolver + + l = []language.Language{ + {Code: "EN", Name: "English"}, + {Code: "FA", Name: "فارسی"}, + } + ) + + languageRepository.On("Count").Once().Return(uint(len(l)), nil) + languageRepository.On("GetAll", uint(0), uint(len(l))).Once().Return(l, nil) + defer languageRepository.AssertExpectations(t) + + languageResolver.On("DefaultCode").Once().Return(l[0].Code, nil) + languageResolver.On("Resolve", l[0].Code).Once().Return(l[0], nil) + defer languageResolver.AssertExpectations(t) + + handler := NewIndexHandler(getlanguages.NewUseCase(&languageRepository, &languageResolver)) + + request := httptest.NewRequest(http.MethodGet, "/", nil) + response := httptest.NewRecorder() + + handler.ServeHTTP(response, request) + + expectedBody, err := os.ReadFile("testdata/index-languages-response.json") + assert.NoError(t, err) + + assert.Equal(t, "application/json", response.Header().Get("content-type")) + assert.JSONEq(t, string(expectedBody), response.Body.String()) + assert.Equal(t, http.StatusOK, response.Code) + }) + + t.Run("getting languages fails", func(t *testing.T) { + t.Parallel() + + var ( + languageRepository languages.MockLanguagesRepository + languageResolver resolver.MockResolver + ) + + languageRepository.On("Count").Once().Return(uint(0), assert.AnError) + defer languageRepository.AssertExpectations(t) + + handler := NewIndexHandler(getlanguages.NewUseCase(&languageRepository, &languageResolver)) + + request := httptest.NewRequest(http.MethodGet, "/", nil) + response := httptest.NewRecorder() + + handler.ServeHTTP(response, request) + + assert.Equal(t, http.StatusInternalServerError, response.Code) + }) +} diff --git a/presentation/http/blog/api/language/testdata/index-languages-response.json b/presentation/http/blog/api/language/testdata/index-languages-response.json new file mode 100644 index 00000000..2e731187 --- /dev/null +++ b/presentation/http/blog/api/language/testdata/index-languages-response.json @@ -0,0 +1,16 @@ +{ + "items": [ + { + "code": "EN", + "name": "English" + }, + { + "code": "FA", + "name": "فارسی" + } + ], + "default_language": { + "code": "EN", + "name": "English" + } +} diff --git a/resources/docs/blog/openapi/docs.go b/resources/docs/blog/openapi/docs.go index 90d657ea..211f6597 100644 --- a/resources/docs/blog/openapi/docs.go +++ b/resources/docs/blog/openapi/docs.go @@ -40,6 +40,13 @@ const docTemplate = `{ "description": "Page number", "name": "page", "in": "query" + }, + { + "type": "string", + "default": "EN", + "description": "Language key (e.g. EN, FA)", + "name": "language_code", + "in": "query" } ], "responses": { @@ -79,6 +86,13 @@ const docTemplate = `{ "name": "uuid", "in": "path", "required": true + }, + { + "type": "string", + "default": "EN", + "description": "Language key (e.g. EN, FA)", + "name": "language_code", + "in": "query" } ], "responses": { @@ -424,6 +438,13 @@ const docTemplate = `{ "description": "Page number", "name": "page", "in": "query" + }, + { + "type": "string", + "default": "EN", + "description": "Language key (e.g. EN, FA)", + "name": "language_code", + "in": "query" } ], "responses": { @@ -1780,6 +1801,228 @@ const docTemplate = `{ } } }, + "/dashboard/languages": { + "get": { + "description": "paginated list of languages", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard languages" + ], + "summary": "List languages", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "Page", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_khanzadimahdi_testproject_application_dashboard_language_getLanguages.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "description": "modify an existing language", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard languages" + ], + "summary": "Update language", + "parameters": [ + { + "description": "Language update", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/updatelanguage.Request" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "description": "add a new language via dashboard", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard languages" + ], + "summary": "Create language", + "parameters": [ + { + "description": "Language data", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/createlanguage.Request" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/createlanguage.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/dashboard/languages/{code}": { + "get": { + "description": "fetch language by code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard languages" + ], + "summary": "Get language", + "parameters": [ + { + "type": "string", + "description": "Language code", + "name": "code", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/getlanguage.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "delete": { + "description": "remove a language by code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard languages" + ], + "summary": "Delete language", + "parameters": [ + { + "type": "string", + "description": "Language code", + "name": "code", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/dashboard/my/bookmarks": { "get": { "description": "paginated list of bookmarks for authenticated user", @@ -2691,6 +2934,13 @@ const docTemplate = `{ "description": "Page number", "name": "page", "in": "query" + }, + { + "type": "string", + "default": "EN", + "description": "Language key (e.g. EN, FA)", + "name": "language_code", + "in": "query" } ], "responses": { @@ -2730,6 +2980,15 @@ const docTemplate = `{ "home" ], "summary": "Application home endpoint", + "parameters": [ + { + "type": "string", + "default": "EN", + "description": "Language key (e.g. EN, FA)", + "name": "language", + "in": "query" + } + ], "responses": { "200": { "description": "OK", @@ -2747,6 +3006,36 @@ const docTemplate = `{ } } }, + "/languages": { + "get": { + "description": "retrieve all available languages", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "languages" + ], + "summary": "List languages", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_khanzadimahdi_testproject_application_language_getLanguages.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/nodes": { "get": { "description": "return a page of runner nodes", @@ -3097,12 +3386,18 @@ const docTemplate = `{ "body": { "type": "string" }, + "correlation_uuid": { + "type": "string" + }, "cover": { "type": "string" }, "excerpt": { "type": "string" }, + "language_code": { + "type": "string" + }, "published_at": { "type": "string" }, @@ -3156,6 +3451,28 @@ const docTemplate = `{ } } }, + "createlanguage.Request": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "createlanguage.Response": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "errors": { + "$ref": "#/definitions/domain.ValidationErrors" + } + } + }, "createrole.Request": { "type": "object", "properties": { @@ -3318,6 +3635,9 @@ const docTemplate = `{ "$ref": "#/definitions/getArticlesByAuthor.articleResponse" } }, + "language_code": { + "$ref": "#/definitions/getArticlesByAuthor.languageResponse" + }, "pagination": { "$ref": "#/definitions/getArticlesByAuthor.paginationResponse" } @@ -3326,6 +3646,12 @@ const docTemplate = `{ "getArticlesByAuthor.articleResponse": { "type": "object", "properties": { + "available_languages": { + "type": "array", + "items": { + "$ref": "#/definitions/getArticlesByAuthor.languageResponse" + } + }, "cover": { "type": "string" }, @@ -3366,6 +3692,17 @@ const docTemplate = `{ } } }, + "getArticlesByAuthor.languageResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "getArticlesByAuthor.paginationResponse": { "type": "object", "properties": { @@ -3389,6 +3726,9 @@ const docTemplate = `{ "$ref": "#/definitions/getArticlesByHashtag.articleResponse" } }, + "language_code": { + "$ref": "#/definitions/getArticlesByHashtag.languageResponse" + }, "pagination": { "$ref": "#/definitions/getArticlesByHashtag.paginationResponse" } @@ -3400,6 +3740,12 @@ const docTemplate = `{ "author": { "$ref": "#/definitions/getArticlesByHashtag.authorResponse" }, + "available_languages": { + "type": "array", + "items": { + "$ref": "#/definitions/getArticlesByHashtag.languageResponse" + } + }, "cover": { "type": "string" }, @@ -3437,6 +3783,17 @@ const docTemplate = `{ } } }, + "getArticlesByHashtag.languageResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "getArticlesByHashtag.paginationResponse": { "type": "object", "properties": { @@ -3497,6 +3854,9 @@ const docTemplate = `{ "getConfig.Response": { "type": "object", "properties": { + "default_language_code": { + "type": "string" + }, "revision": { "type": "integer" }, @@ -3792,6 +4152,17 @@ const docTemplate = `{ } } }, + "getarticle.languageResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "getarticles.author": { "type": "object", "properties": { @@ -3826,6 +4197,17 @@ const docTemplate = `{ } } }, + "getarticles.languageResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "getarticles.pagination": { "type": "object", "properties": { @@ -3914,6 +4296,28 @@ const docTemplate = `{ } } }, + "getlanguage.Response": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "getlanguages.pagination": { + "type": "object", + "properties": { + "current_page": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, "getpermissions.Response": { "type": "object", "properties": { @@ -3945,6 +4349,9 @@ const docTemplate = `{ "email": { "type": "string" }, + "language_code": { + "type": "string" + }, "name": { "type": "string" }, @@ -4118,6 +4525,9 @@ const docTemplate = `{ "email": { "type": "string" }, + "language_code": { + "type": "string" + }, "name": { "type": "string" }, @@ -4225,9 +4635,18 @@ const docTemplate = `{ "author": { "$ref": "#/definitions/getarticle.authorResponse" }, + "available_languages": { + "type": "array", + "items": { + "$ref": "#/definitions/getarticle.languageResponse" + } + }, "body": { "type": "string" }, + "correlation_uuid": { + "type": "string" + }, "cover": { "type": "string" }, @@ -4240,6 +4659,9 @@ const docTemplate = `{ "excerpt": { "type": "string" }, + "language_code": { + "$ref": "#/definitions/getarticle.languageResponse" + }, "published_at": { "type": "string" }, @@ -4252,8 +4674,11 @@ const docTemplate = `{ "title": { "type": "string" }, - "uuid": { - "type": "string" + "validation_errors": { + "type": "object", + "additionalProperties": { + "type": "string" + } }, "video": { "type": "string" @@ -4272,6 +4697,9 @@ const docTemplate = `{ "$ref": "#/definitions/github_com_khanzadimahdi_testproject_application_article_getArticles.articleResponse" } }, + "language_code": { + "$ref": "#/definitions/getarticles.languageResponse" + }, "pagination": { "$ref": "#/definitions/getarticles.paginationResponse" } @@ -4283,6 +4711,12 @@ const docTemplate = `{ "author": { "$ref": "#/definitions/getarticles.authorResponse" }, + "available_languages": { + "type": "array", + "items": { + "$ref": "#/definitions/getarticles.languageResponse" + } + }, "cover": { "type": "string" }, @@ -4400,6 +4834,9 @@ const docTemplate = `{ "excerpt": { "type": "string" }, + "language_code": { + "type": "string" + }, "published_at": { "type": "string" }, @@ -4412,6 +4849,9 @@ const docTemplate = `{ "title": { "type": "string" }, + "translation_correlation_id": { + "type": "string" + }, "uuid": { "type": "string" }, @@ -4554,6 +4994,56 @@ const docTemplate = `{ } } }, + "github_com_khanzadimahdi_testproject_application_dashboard_language_getLanguages.Response": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_khanzadimahdi_testproject_application_dashboard_language_getLanguages.languageResponse" + } + }, + "pagination": { + "$ref": "#/definitions/getlanguages.pagination" + } + } + }, + "github_com_khanzadimahdi_testproject_application_dashboard_language_getLanguages.languageResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "github_com_khanzadimahdi_testproject_application_language_getLanguages.Response": { + "type": "object", + "properties": { + "default_language": { + "$ref": "#/definitions/github_com_khanzadimahdi_testproject_application_language_getLanguages.languageResponse" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_khanzadimahdi_testproject_application_language_getLanguages.languageResponse" + } + } + } + }, + "github_com_khanzadimahdi_testproject_application_language_getLanguages.languageResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "github_com_khanzadimahdi_testproject_application_runner_manager_task_getTasks.Response": { "type": "object", "properties": { @@ -4880,6 +5370,9 @@ const docTemplate = `{ "$ref": "#/definitions/element.Response" } }, + "language_code": { + "$ref": "#/definitions/home.languageResponse" + }, "popular": { "type": "array", "items": { @@ -4934,6 +5427,17 @@ const docTemplate = `{ } } }, + "home.languageResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "login.Request": { "type": "object", "properties": { @@ -5046,6 +5550,9 @@ const docTemplate = `{ "updateConfig.Request": { "type": "object", "properties": { + "default_language_code": { + "type": "string" + }, "user_default_roles": { "type": "array", "items": { @@ -5071,12 +5578,18 @@ const docTemplate = `{ "body": { "type": "string" }, + "correlation_uuid": { + "type": "string" + }, "cover": { "type": "string" }, "excerpt": { "type": "string" }, + "language_code": { + "type": "string" + }, "published_at": { "type": "string" }, @@ -5100,6 +5613,17 @@ const docTemplate = `{ "updateelement.Request": { "type": "object" }, + "updatelanguage.Request": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "updateprofile.Request": { "type": "object", "properties": { @@ -5109,6 +5633,9 @@ const docTemplate = `{ "email": { "type": "string" }, + "language_code": { + "type": "string" + }, "name": { "type": "string" }, @@ -5177,6 +5704,9 @@ const docTemplate = `{ "verify.Request": { "type": "object", "properties": { + "language_code": { + "type": "string" + }, "name": { "type": "string" }, diff --git a/resources/docs/blog/openapi/swagger.json b/resources/docs/blog/openapi/swagger.json index 09c91155..4f86496a 100644 --- a/resources/docs/blog/openapi/swagger.json +++ b/resources/docs/blog/openapi/swagger.json @@ -37,6 +37,13 @@ "description": "Page number", "name": "page", "in": "query" + }, + { + "type": "string", + "default": "EN", + "description": "Language key (e.g. EN, FA)", + "name": "language_code", + "in": "query" } ], "responses": { @@ -76,6 +83,13 @@ "name": "uuid", "in": "path", "required": true + }, + { + "type": "string", + "default": "EN", + "description": "Language key (e.g. EN, FA)", + "name": "language_code", + "in": "query" } ], "responses": { @@ -421,6 +435,13 @@ "description": "Page number", "name": "page", "in": "query" + }, + { + "type": "string", + "default": "EN", + "description": "Language key (e.g. EN, FA)", + "name": "language_code", + "in": "query" } ], "responses": { @@ -1777,6 +1798,228 @@ } } }, + "/dashboard/languages": { + "get": { + "description": "paginated list of languages", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard languages" + ], + "summary": "List languages", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "Page", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_khanzadimahdi_testproject_application_dashboard_language_getLanguages.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "put": { + "description": "modify an existing language", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard languages" + ], + "summary": "Update language", + "parameters": [ + { + "description": "Language update", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/updatelanguage.Request" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "description": "add a new language via dashboard", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard languages" + ], + "summary": "Create language", + "parameters": [ + { + "description": "Language data", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/createlanguage.Request" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/createlanguage.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/dashboard/languages/{code}": { + "get": { + "description": "fetch language by code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard languages" + ], + "summary": "Get language", + "parameters": [ + { + "type": "string", + "description": "Language code", + "name": "code", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/getlanguage.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "delete": { + "description": "remove a language by code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard languages" + ], + "summary": "Delete language", + "parameters": [ + { + "type": "string", + "description": "Language code", + "name": "code", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/dashboard/my/bookmarks": { "get": { "description": "paginated list of bookmarks for authenticated user", @@ -2688,6 +2931,13 @@ "description": "Page number", "name": "page", "in": "query" + }, + { + "type": "string", + "default": "EN", + "description": "Language key (e.g. EN, FA)", + "name": "language_code", + "in": "query" } ], "responses": { @@ -2727,6 +2977,15 @@ "home" ], "summary": "Application home endpoint", + "parameters": [ + { + "type": "string", + "default": "EN", + "description": "Language key (e.g. EN, FA)", + "name": "language", + "in": "query" + } + ], "responses": { "200": { "description": "OK", @@ -2744,6 +3003,36 @@ } } }, + "/languages": { + "get": { + "description": "retrieve all available languages", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "languages" + ], + "summary": "List languages", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_khanzadimahdi_testproject_application_language_getLanguages.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/nodes": { "get": { "description": "return a page of runner nodes", @@ -3094,12 +3383,18 @@ "body": { "type": "string" }, + "correlation_uuid": { + "type": "string" + }, "cover": { "type": "string" }, "excerpt": { "type": "string" }, + "language_code": { + "type": "string" + }, "published_at": { "type": "string" }, @@ -3153,6 +3448,28 @@ } } }, + "createlanguage.Request": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "createlanguage.Response": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "errors": { + "$ref": "#/definitions/domain.ValidationErrors" + } + } + }, "createrole.Request": { "type": "object", "properties": { @@ -3315,6 +3632,9 @@ "$ref": "#/definitions/getArticlesByAuthor.articleResponse" } }, + "language_code": { + "$ref": "#/definitions/getArticlesByAuthor.languageResponse" + }, "pagination": { "$ref": "#/definitions/getArticlesByAuthor.paginationResponse" } @@ -3323,6 +3643,12 @@ "getArticlesByAuthor.articleResponse": { "type": "object", "properties": { + "available_languages": { + "type": "array", + "items": { + "$ref": "#/definitions/getArticlesByAuthor.languageResponse" + } + }, "cover": { "type": "string" }, @@ -3363,6 +3689,17 @@ } } }, + "getArticlesByAuthor.languageResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "getArticlesByAuthor.paginationResponse": { "type": "object", "properties": { @@ -3386,6 +3723,9 @@ "$ref": "#/definitions/getArticlesByHashtag.articleResponse" } }, + "language_code": { + "$ref": "#/definitions/getArticlesByHashtag.languageResponse" + }, "pagination": { "$ref": "#/definitions/getArticlesByHashtag.paginationResponse" } @@ -3397,6 +3737,12 @@ "author": { "$ref": "#/definitions/getArticlesByHashtag.authorResponse" }, + "available_languages": { + "type": "array", + "items": { + "$ref": "#/definitions/getArticlesByHashtag.languageResponse" + } + }, "cover": { "type": "string" }, @@ -3434,6 +3780,17 @@ } } }, + "getArticlesByHashtag.languageResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "getArticlesByHashtag.paginationResponse": { "type": "object", "properties": { @@ -3494,6 +3851,9 @@ "getConfig.Response": { "type": "object", "properties": { + "default_language_code": { + "type": "string" + }, "revision": { "type": "integer" }, @@ -3789,6 +4149,17 @@ } } }, + "getarticle.languageResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "getarticles.author": { "type": "object", "properties": { @@ -3823,6 +4194,17 @@ } } }, + "getarticles.languageResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "getarticles.pagination": { "type": "object", "properties": { @@ -3911,6 +4293,28 @@ } } }, + "getlanguage.Response": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "getlanguages.pagination": { + "type": "object", + "properties": { + "current_page": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, "getpermissions.Response": { "type": "object", "properties": { @@ -3942,6 +4346,9 @@ "email": { "type": "string" }, + "language_code": { + "type": "string" + }, "name": { "type": "string" }, @@ -4115,6 +4522,9 @@ "email": { "type": "string" }, + "language_code": { + "type": "string" + }, "name": { "type": "string" }, @@ -4222,9 +4632,18 @@ "author": { "$ref": "#/definitions/getarticle.authorResponse" }, + "available_languages": { + "type": "array", + "items": { + "$ref": "#/definitions/getarticle.languageResponse" + } + }, "body": { "type": "string" }, + "correlation_uuid": { + "type": "string" + }, "cover": { "type": "string" }, @@ -4237,6 +4656,9 @@ "excerpt": { "type": "string" }, + "language_code": { + "$ref": "#/definitions/getarticle.languageResponse" + }, "published_at": { "type": "string" }, @@ -4249,8 +4671,11 @@ "title": { "type": "string" }, - "uuid": { - "type": "string" + "validation_errors": { + "type": "object", + "additionalProperties": { + "type": "string" + } }, "video": { "type": "string" @@ -4269,6 +4694,9 @@ "$ref": "#/definitions/github_com_khanzadimahdi_testproject_application_article_getArticles.articleResponse" } }, + "language_code": { + "$ref": "#/definitions/getarticles.languageResponse" + }, "pagination": { "$ref": "#/definitions/getarticles.paginationResponse" } @@ -4280,6 +4708,12 @@ "author": { "$ref": "#/definitions/getarticles.authorResponse" }, + "available_languages": { + "type": "array", + "items": { + "$ref": "#/definitions/getarticles.languageResponse" + } + }, "cover": { "type": "string" }, @@ -4397,6 +4831,9 @@ "excerpt": { "type": "string" }, + "language_code": { + "type": "string" + }, "published_at": { "type": "string" }, @@ -4409,6 +4846,9 @@ "title": { "type": "string" }, + "translation_correlation_id": { + "type": "string" + }, "uuid": { "type": "string" }, @@ -4551,6 +4991,56 @@ } } }, + "github_com_khanzadimahdi_testproject_application_dashboard_language_getLanguages.Response": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_khanzadimahdi_testproject_application_dashboard_language_getLanguages.languageResponse" + } + }, + "pagination": { + "$ref": "#/definitions/getlanguages.pagination" + } + } + }, + "github_com_khanzadimahdi_testproject_application_dashboard_language_getLanguages.languageResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "github_com_khanzadimahdi_testproject_application_language_getLanguages.Response": { + "type": "object", + "properties": { + "default_language": { + "$ref": "#/definitions/github_com_khanzadimahdi_testproject_application_language_getLanguages.languageResponse" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_khanzadimahdi_testproject_application_language_getLanguages.languageResponse" + } + } + } + }, + "github_com_khanzadimahdi_testproject_application_language_getLanguages.languageResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "github_com_khanzadimahdi_testproject_application_runner_manager_task_getTasks.Response": { "type": "object", "properties": { @@ -4877,6 +5367,9 @@ "$ref": "#/definitions/element.Response" } }, + "language_code": { + "$ref": "#/definitions/home.languageResponse" + }, "popular": { "type": "array", "items": { @@ -4931,6 +5424,17 @@ } } }, + "home.languageResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "login.Request": { "type": "object", "properties": { @@ -5043,6 +5547,9 @@ "updateConfig.Request": { "type": "object", "properties": { + "default_language_code": { + "type": "string" + }, "user_default_roles": { "type": "array", "items": { @@ -5068,12 +5575,18 @@ "body": { "type": "string" }, + "correlation_uuid": { + "type": "string" + }, "cover": { "type": "string" }, "excerpt": { "type": "string" }, + "language_code": { + "type": "string" + }, "published_at": { "type": "string" }, @@ -5097,6 +5610,17 @@ "updateelement.Request": { "type": "object" }, + "updatelanguage.Request": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "updateprofile.Request": { "type": "object", "properties": { @@ -5106,6 +5630,9 @@ "email": { "type": "string" }, + "language_code": { + "type": "string" + }, "name": { "type": "string" }, @@ -5174,6 +5701,9 @@ "verify.Request": { "type": "object", "properties": { + "language_code": { + "type": "string" + }, "name": { "type": "string" }, diff --git a/resources/docs/blog/openapi/swagger.yaml b/resources/docs/blog/openapi/swagger.yaml index fa37de1d..56229fed 100644 --- a/resources/docs/blog/openapi/swagger.yaml +++ b/resources/docs/blog/openapi/swagger.yaml @@ -25,10 +25,14 @@ definitions: properties: body: type: string + correlation_uuid: + type: string cover: type: string excerpt: type: string + language_code: + type: string published_at: type: string tags: @@ -63,6 +67,20 @@ definitions: uuid: type: string type: object + createlanguage.Request: + properties: + code: + type: string + name: + type: string + type: object + createlanguage.Response: + properties: + code: + type: string + errors: + $ref: '#/definitions/domain.ValidationErrors' + type: object createrole.Request: properties: description: @@ -168,11 +186,17 @@ definitions: items: $ref: '#/definitions/getArticlesByAuthor.articleResponse' type: array + language_code: + $ref: '#/definitions/getArticlesByAuthor.languageResponse' pagination: $ref: '#/definitions/getArticlesByAuthor.paginationResponse' type: object getArticlesByAuthor.articleResponse: properties: + available_languages: + items: + $ref: '#/definitions/getArticlesByAuthor.languageResponse' + type: array cover: type: string excerpt: @@ -199,6 +223,13 @@ definitions: uuid: type: string type: object + getArticlesByAuthor.languageResponse: + properties: + code: + type: string + name: + type: string + type: object getArticlesByAuthor.paginationResponse: properties: current_page: @@ -214,6 +245,8 @@ definitions: items: $ref: '#/definitions/getArticlesByHashtag.articleResponse' type: array + language_code: + $ref: '#/definitions/getArticlesByHashtag.languageResponse' pagination: $ref: '#/definitions/getArticlesByHashtag.paginationResponse' type: object @@ -221,6 +254,10 @@ definitions: properties: author: $ref: '#/definitions/getArticlesByHashtag.authorResponse' + available_languages: + items: + $ref: '#/definitions/getArticlesByHashtag.languageResponse' + type: array cover: type: string excerpt: @@ -245,6 +282,13 @@ definitions: uuid: type: string type: object + getArticlesByHashtag.languageResponse: + properties: + code: + type: string + name: + type: string + type: object getArticlesByHashtag.paginationResponse: properties: current_page: @@ -284,6 +328,8 @@ definitions: type: object getConfig.Response: properties: + default_language_code: + type: string revision: type: integer user_default_roles: @@ -475,6 +521,13 @@ definitions: uuid: type: string type: object + getarticle.languageResponse: + properties: + code: + type: string + name: + type: string + type: object getarticles.author: properties: avatar: @@ -497,6 +550,13 @@ definitions: uuid: type: string type: object + getarticles.languageResponse: + properties: + code: + type: string + name: + type: string + type: object getarticles.pagination: properties: current_page: @@ -554,6 +614,20 @@ definitions: total_pages: type: integer type: object + getlanguage.Response: + properties: + code: + type: string + name: + type: string + type: object + getlanguages.pagination: + properties: + current_page: + type: integer + total_pages: + type: integer + type: object getpermissions.Response: properties: items: @@ -574,6 +648,8 @@ definitions: type: string email: type: string + language_code: + type: string name: type: string username: @@ -687,6 +763,8 @@ definitions: type: string email: type: string + language_code: + type: string name: type: string username: @@ -756,8 +834,14 @@ definitions: properties: author: $ref: '#/definitions/getarticle.authorResponse' + available_languages: + items: + $ref: '#/definitions/getarticle.languageResponse' + type: array body: type: string + correlation_uuid: + type: string cover: type: string elements: @@ -766,6 +850,8 @@ definitions: type: array excerpt: type: string + language_code: + $ref: '#/definitions/getarticle.languageResponse' published_at: type: string tags: @@ -774,8 +860,10 @@ definitions: type: array title: type: string - uuid: - type: string + validation_errors: + additionalProperties: + type: string + type: object video: type: string view_count: @@ -787,6 +875,8 @@ definitions: items: $ref: '#/definitions/github_com_khanzadimahdi_testproject_application_article_getArticles.articleResponse' type: array + language_code: + $ref: '#/definitions/getarticles.languageResponse' pagination: $ref: '#/definitions/getarticles.paginationResponse' type: object @@ -794,6 +884,10 @@ definitions: properties: author: $ref: '#/definitions/getarticles.authorResponse' + available_languages: + items: + $ref: '#/definitions/getarticles.languageResponse' + type: array cover: type: string excerpt: @@ -870,6 +964,8 @@ definitions: type: string excerpt: type: string + language_code: + type: string published_at: type: string tags: @@ -878,6 +974,8 @@ definitions: type: array title: type: string + translation_correlation_id: + type: string uuid: type: string video: @@ -970,6 +1068,38 @@ definitions: total_pages: type: integer type: object + github_com_khanzadimahdi_testproject_application_dashboard_language_getLanguages.Response: + properties: + items: + items: + $ref: '#/definitions/github_com_khanzadimahdi_testproject_application_dashboard_language_getLanguages.languageResponse' + type: array + pagination: + $ref: '#/definitions/getlanguages.pagination' + type: object + github_com_khanzadimahdi_testproject_application_dashboard_language_getLanguages.languageResponse: + properties: + code: + type: string + name: + type: string + type: object + github_com_khanzadimahdi_testproject_application_language_getLanguages.Response: + properties: + default_language: + $ref: '#/definitions/github_com_khanzadimahdi_testproject_application_language_getLanguages.languageResponse' + items: + items: + $ref: '#/definitions/github_com_khanzadimahdi_testproject_application_language_getLanguages.languageResponse' + type: array + type: object + github_com_khanzadimahdi_testproject_application_language_getLanguages.languageResponse: + properties: + code: + type: string + name: + type: string + type: object github_com_khanzadimahdi_testproject_application_runner_manager_task_getTasks.Response: properties: items: @@ -1183,6 +1313,8 @@ definitions: items: $ref: '#/definitions/element.Response' type: array + language_code: + $ref: '#/definitions/home.languageResponse' popular: items: $ref: '#/definitions/home.articleResponse' @@ -1218,6 +1350,13 @@ definitions: uuid: type: string type: object + home.languageResponse: + properties: + code: + type: string + name: + type: string + type: object login.Request: properties: identity: @@ -1290,6 +1429,8 @@ definitions: type: object updateConfig.Request: properties: + default_language_code: + type: string user_default_roles: items: type: string @@ -1306,10 +1447,14 @@ definitions: properties: body: type: string + correlation_uuid: + type: string cover: type: string excerpt: type: string + language_code: + type: string published_at: type: string tags: @@ -1325,12 +1470,21 @@ definitions: type: object updateelement.Request: type: object + updatelanguage.Request: + properties: + code: + type: string + name: + type: string + type: object updateprofile.Request: properties: avatar: type: string email: type: string + language_code: + type: string name: type: string username: @@ -1375,6 +1529,8 @@ definitions: type: object verify.Request: properties: + language_code: + type: string name: type: string password: @@ -1407,6 +1563,11 @@ paths: in: query name: page type: integer + - default: EN + description: Language key (e.g. EN, FA) + in: query + name: language_code + type: string produces: - application/json responses: @@ -1433,6 +1594,11 @@ paths: name: uuid required: true type: string + - default: EN + description: Language key (e.g. EN, FA) + in: query + name: language_code + type: string produces: - application/json responses: @@ -1666,6 +1832,11 @@ paths: in: query name: page type: integer + - default: EN + description: Language key (e.g. EN, FA) + in: query + name: language_code + type: string produces: - application/json responses: @@ -2584,6 +2755,156 @@ paths: summary: List own files tags: - dashboard files + /dashboard/languages: + get: + consumes: + - application/json + description: paginated list of languages + parameters: + - default: 1 + description: Page + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_khanzadimahdi_testproject_application_dashboard_language_getLanguages.Response' + "500": + description: Internal Server Error + schema: + additionalProperties: true + type: object + summary: List languages + tags: + - dashboard languages + post: + consumes: + - application/json + description: add a new language via dashboard + parameters: + - description: Language data + in: body + name: body + required: true + schema: + $ref: '#/definitions/createlanguage.Request' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/createlanguage.Response' + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: true + type: object + summary: Create language + tags: + - dashboard languages + put: + consumes: + - application/json + description: modify an existing language + parameters: + - description: Language update + in: body + name: body + required: true + schema: + $ref: '#/definitions/updatelanguage.Request' + produces: + - application/json + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + "404": + description: Not Found + schema: + additionalProperties: true + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: true + type: object + summary: Update language + tags: + - dashboard languages + /dashboard/languages/{code}: + delete: + consumes: + - application/json + description: remove a language by code + parameters: + - description: Language code + in: path + name: code + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "404": + description: Not Found + schema: + additionalProperties: true + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: true + type: object + summary: Delete language + tags: + - dashboard languages + get: + consumes: + - application/json + description: fetch language by code + parameters: + - description: Language code + in: path + name: code + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/getlanguage.Response' + "404": + description: Not Found + schema: + additionalProperties: true + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: true + type: object + summary: Get language + tags: + - dashboard languages /dashboard/my/bookmarks: delete: consumes: @@ -3198,6 +3519,11 @@ paths: in: query name: page type: integer + - default: EN + description: Language key (e.g. EN, FA) + in: query + name: language_code + type: string produces: - application/json responses: @@ -3223,6 +3549,12 @@ paths: consumes: - application/json description: returns the contents used for home page + parameters: + - default: EN + description: Language key (e.g. EN, FA) + in: query + name: language + type: string produces: - application/json responses: @@ -3238,6 +3570,26 @@ paths: summary: Application home endpoint tags: - home + /languages: + get: + consumes: + - application/json + description: retrieve all available languages + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_khanzadimahdi_testproject_application_language_getLanguages.Response' + "500": + description: Internal Server Error + schema: + additionalProperties: true + type: object + summary: List languages + tags: + - languages /nodes: get: consumes: