From 23c29aba1de3015aca5833ddc7d5f7d734d2dfe2 Mon Sep 17 00:00:00 2001 From: Aleksandr Trushkin Date: Sat, 27 Jun 2026 23:54:07 +0000 Subject: [PATCH 1/7] fix(cluster-1): data corruption fixes (C4/C5/C8) - synchandler: combine course date with clock time via time.Date instead of adding two absolute Unix epochs, which produced corrupt start times - tracing: make DeploymentEnvironment configurable via config.Trace (defaults to development instead of hardcoded production) - http: align course handler tracer name to 'kuriweb.http' to match the request middleware instrument so spans share the same tracer --- cmd/kuriweb/trace.go | 9 ++++++--- internal/common/config/trace.go | 9 +++++---- internal/kurious/ports/background/synchandler.go | 8 ++++++-- internal/kurious/ports/http/course.go | 2 +- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/cmd/kuriweb/trace.go b/cmd/kuriweb/trace.go index e11e62d..14838ef 100644 --- a/cmd/kuriweb/trace.go +++ b/cmd/kuriweb/trace.go @@ -50,7 +50,7 @@ func setupOtelSDK(ctx context.Context, cfg config.Trace) (shutdown shutdownFunc, return err } - resource, err := makeServiceResource(ctx) + resource, err := makeServiceResource(ctx, cfg.Environment) if err != nil { return shutdown, fmt.Errorf("making service resource: %w", err) } @@ -102,7 +102,10 @@ type TraceProviderParams struct { Type config.TraceClientType } -func makeServiceResource(ctx context.Context) (*resource.Resource, error) { +func makeServiceResource(ctx context.Context, environment string) (*resource.Resource, error) { + if environment == "" { + environment = "development" + } r, err := resource.New( ctx, resource.WithDetectors( @@ -113,7 +116,7 @@ func makeServiceResource(ctx context.Context) (*resource.Resource, error) { resource.WithHost(), resource.WithAttributes( semconv.ServiceName("bigstats:kuriweb"), - semconv.DeploymentEnvironment("production"), + semconv.DeploymentEnvironment(environment), ), ) if err != nil { diff --git a/internal/common/config/trace.go b/internal/common/config/trace.go index 844e740..35865b3 100644 --- a/internal/common/config/trace.go +++ b/internal/common/config/trace.go @@ -29,10 +29,11 @@ func (t *TraceClientType) UnmarshalText(data []byte) error { } type Trace struct { - Endpoint string `json:"endpoint"` - APIKey string `json:"api_key"` - APIHeader string `json:"api_header"` - Type TraceClientType `json:"type"` + Endpoint string `json:"endpoint"` + APIKey string `json:"api_key"` + APIHeader string `json:"api_header"` + Type TraceClientType `json:"type"` + Environment string `json:"environment"` ShowMetrics bool `json:"show_metrics"` } diff --git a/internal/kurious/ports/background/synchandler.go b/internal/kurious/ports/background/synchandler.go index ca21da0..e5ac1b7 100644 --- a/internal/kurious/ports/background/synchandler.go +++ b/internal/kurious/ports/background/synchandler.go @@ -384,8 +384,12 @@ func courseAsCreateCourseParams(course sravni.Course) command.CreateCourse { startAt = *course.DateStart } if course.TimeStart != nil { - startAtUnix := startAt.Unix() + course.TimeStart.Unix() - startAt = time.Unix(startAtUnix, 0) + clock := *course.TimeStart + startAt = time.Date( + startAt.Year(), startAt.Month(), startAt.Day(), + clock.Hour(), clock.Minute(), clock.Second(), clock.Nanosecond(), + startAt.Location(), + ) } var courseDuration time.Duration diff --git a/internal/kurious/ports/http/course.go b/internal/kurious/ports/http/course.go index e818ed1..db06464 100644 --- a/internal/kurious/ports/http/course.go +++ b/internal/kurious/ports/http/course.go @@ -23,7 +23,7 @@ import ( var ( paramsAttr = attribute.Key("params") - webtracer = otel.Tracer("http") + webtracer = otel.Tracer("kuriweb.http") ) type courseTemplServer struct { -- 2.25.1 From 40e5621eb93449251ce90ef8dc85338e6ae7962a Mon Sep 17 00:00:00 2001 From: Aleksandr Trushkin Date: Sun, 28 Jun 2026 04:28:48 +0000 Subject: [PATCH 2/7] fix(cluster-2): error contract unification (C1/M10/M11/M12) - http: handleError now recognizes domain.ErrNotFound in addition to the common errors.ErrNotFound sentinel, so repo-not-found maps to 404 instead of 500 (the two packages use distinct error types) - sqlite_course_repository: propagate listCount errors instead of logging and swallowing them, which left callers with a silent Count=0 - synchandler: collect course/organization insert failures into a function-scoped error via errors.Join and return it; previously the loop-local err was overwritten and the handler always returned nil, hiding all insert failures --- .../adapters/sqlite_course_repository.go | 3 +-- .../kurious/ports/background/synchandler.go | 23 ++++++++++--------- internal/kurious/ports/http/server.go | 3 ++- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/internal/kurious/adapters/sqlite_course_repository.go b/internal/kurious/adapters/sqlite_course_repository.go index 0aea5f6..830f5c8 100644 --- a/internal/kurious/adapters/sqlite_course_repository.go +++ b/internal/kurious/adapters/sqlite_course_repository.go @@ -10,7 +10,6 @@ import ( "time" "git.loyso.art/frx/kurious/internal/common/nullable" - "git.loyso.art/frx/kurious/internal/common/xcontext" "git.loyso.art/frx/kurious/internal/common/xslices" "git.loyso.art/frx/kurious/internal/kurious/domain" @@ -112,7 +111,7 @@ func (r *sqliteCourseRepository) List( result.Count, err = r.listCount(ctx, params) if err != nil { - xcontext.LogWithWarnError(ctx, r.log, err, "unable to list count") + return result, fmt.Errorf("listing count: %w", err) } span.SetAttributes( diff --git a/internal/kurious/ports/background/synchandler.go b/internal/kurious/ports/background/synchandler.go index e5ac1b7..b1dd75e 100644 --- a/internal/kurious/ports/background/synchandler.go +++ b/internal/kurious/ports/background/synchandler.go @@ -85,6 +85,7 @@ func (h *syncSravniHandler) Handle(ctx context.Context) (err error) { courses := make([]sravni.Course, 0, 1024) buffer := make([]sravni.Course, 0, 512) organizations := make([]sravni.Organization, 0, 256) + var insertErr error for _, learningType := range learningTypes.Fields { select { case <-ctx.Done(): @@ -174,22 +175,22 @@ func (h *syncSravniHandler) Handle(ctx context.Context) (err error) { var insertCourseSuccess bool if len(courses) > 0 { - err = h.insertCourses(lctx, courses) - if err != nil { - xcontext.LogWithError(lctx, h.log, err, "unable to insert courses") + if cerr := h.insertCourses(lctx, courses); cerr != nil { + xcontext.LogWithError(lctx, h.log, cerr, "unable to insert courses") + insertErr = errors.Join(insertErr, cerr) + } else { + insertCourseSuccess = true } - - insertCourseSuccess = err == nil } var insertOrgsSuccess bool if len(organizations) > 0 { - err = h.insertOrganizations(lctx, organizations) - if err != nil { - xcontext.LogWithError(lctx, h.log, err, "unable to insert courses") + if oerr := h.insertOrganizations(lctx, organizations); oerr != nil { + xcontext.LogWithError(lctx, h.log, oerr, "unable to insert organizations") + insertErr = errors.Join(insertErr, oerr) + } else { + insertOrgsSuccess = true } - - insertOrgsSuccess = err == nil } elapsed = time.Since(start) - elapsed @@ -205,7 +206,7 @@ func (h *syncSravniHandler) Handle(ctx context.Context) (err error) { ) } - return nil + return insertErr } func (h *syncSravniHandler) loadEducationalProducts(ctx context.Context, learningType, courseThematic string, buf []sravni.Course) ([]sravni.Course, map[string]sravni.Organization, error) { diff --git a/internal/kurious/ports/http/server.go b/internal/kurious/ports/http/server.go index bce2523..9bde71f 100644 --- a/internal/kurious/ports/http/server.go +++ b/internal/kurious/ports/http/server.go @@ -9,6 +9,7 @@ import ( "git.loyso.art/frx/kurious/internal/common/errors" "git.loyso.art/frx/kurious/internal/common/xcontext" + "git.loyso.art/frx/kurious/internal/kurious/domain" "git.loyso.art/frx/kurious/internal/kurious/service" "git.loyso.art/frx/kurious/pkg/xdefault" @@ -49,7 +50,7 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slo case stderrors.As(err, &valErr): errorString = valErr.Error() code = http.StatusBadRequest - case stderrors.Is(err, errors.ErrNotFound): + case stderrors.Is(err, errors.ErrNotFound), stderrors.Is(err, domain.ErrNotFound): errorString = err.Error() code = http.StatusNotFound default: -- 2.25.1 From 4f89f5923256c6ac77b6afa592d207a2c7a4318b Mon Sep 17 00:00:00 2001 From: Aleksandr Trushkin Date: Sun, 28 Jun 2026 04:31:21 +0000 Subject: [PATCH 3/7] fix(cluster-3): http hardening (M15/M16/M14/C2) - pagination: clamp per_page to [1,100] and page to >=1 in the parser, guard the TotalPages division against per_page=0 (panic), and clamp the current page to [1,totalPages]; preserves cursor (next-token) mode - middleware: add panic-recovery as the outermost middleware so handler panics return a 500 instead of crashing the process; re-panics http.ErrAbortHandler to keep file serving intact - index: bound the index page query (Limit:200) so it no longer drains the entire courses table in 1000-row batches --- cmd/kuriweb/http.go | 25 +++++++++++++++++++++++++ internal/kurious/ports/http/course.go | 18 +++++++++++++++--- internal/kurious/ports/http/server.go | 10 +++++++++- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/cmd/kuriweb/http.go b/cmd/kuriweb/http.go index 6bad629..8f3ed42 100644 --- a/cmd/kuriweb/http.go +++ b/cmd/kuriweb/http.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "log/slog" "net/http" "strings" @@ -54,6 +55,7 @@ func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server router := mux.NewRouter() router.Use( + middlewareRecovery(log), middlewareCustomWriterInjector(), mux.CORSMethodMiddleware(router), middlewareLogger(log), @@ -101,6 +103,29 @@ func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server } } +func middlewareRecovery(log *slog.Logger) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + rec := recover() + if rec == nil { + return + } + if rec == http.ErrAbortHandler { + panic(rec) + } + xcontext.LogWithError( + r.Context(), log, fmt.Errorf("%v", rec), "recovered from panic", + slog.String("method", r.Method), + slog.String("path", r.URL.Path), + ) + http.Error(w, "internal server error", http.StatusInternalServerError) + }() + next.ServeHTTP(w, r) + }) + } +} + func middlewareCustomWriterInjector() mux.MiddlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/kurious/ports/http/course.go b/internal/kurious/ports/http/course.go index db06464..c76cb27 100644 --- a/internal/kurious/ports/http/course.go +++ b/internal/kurious/ports/http/course.go @@ -213,6 +213,15 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) { } }) + totalPages := 0 + if pathParams.PerPage > 0 { + totalPages = listCoursesResult.Count / pathParams.PerPage + } + currentPage := pathParams.Page + if currentPage > 0 && totalPages > 0 && currentPage > totalPages { + currentPage = totalPages + } + params = bootstrap.ListCoursesParams{ FilterForm: bootstrap.FilterFormParams{ Render: true, @@ -233,8 +242,8 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) { Courses: params.Courses, Categories: params.Categories, Pagination: bootstrap.Pagination{ - Page: pathParams.Page, - TotalPages: listCoursesResult.Count / pathParams.PerPage, + Page: currentPage, + TotalPages: totalPages, BaseURL: r.URL.Path, }, } @@ -285,7 +294,10 @@ func (c courseTemplServer) Index(w http.ResponseWriter, r *http.Request) { stats := bootstrap.MakeNewStats(1, 2, 3) - coursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{}) + const indexCoursesLimit = 200 + coursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{ + Limit: indexCoursesLimit, + }) if handleError(ctx, err, w, c.log, "unable to list courses") { return } diff --git a/internal/kurious/ports/http/server.go b/internal/kurious/ports/http/server.go index 9bde71f..10b5243 100644 --- a/internal/kurious/ports/http/server.go +++ b/internal/kurious/ports/http/server.go @@ -88,14 +88,22 @@ func parsePaginationFromQuery(r *http.Request) (out pagination, err error) { } else { out.PerPage = 20 } + if out.PerPage < 1 { + out.PerPage = 1 + } else if out.PerPage > 100 { + out.PerPage = 100 + } if query.Has("page") { out.Page, err = strconv.Atoi(query.Get("page")) if err != nil { - return out, errors.NewValidationError("page", "bad per_page value") + return out, errors.NewValidationError("page", "bad page value") } } else if !query.Has("next") { out.Page = 1 } + if out.Page < 1 && !query.Has("next") { + out.Page = 1 + } return out, nil } -- 2.25.1 From 9088caf6001a4e4bcceb3fbdf8450875aee78f4b Mon Sep 17 00:00:00 2001 From: Aleksandr Trushkin Date: Sun, 28 Jun 2026 07:56:13 +0000 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20unify=20ErrNotFound=20=E2=80=94=20re?= =?UTF-8?q?move=20domain.ErrNotFound,=20use=20errors.ErrNotFound=20everywh?= =?UTF-8?q?ere?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Responds to PR #5 review comment: consolidate on common/errors.ErrNotFound instead of having two separate error types (domain.PlainError and common/errors.SimpleError) for the same sentinel. - Remove domain.ErrNotFound and domain.PlainError type - Keep domain.ErrNotImplemented as alias to common/errors.ErrNotImplemented - Update sqlite_organization_repository to return cerrors.ErrNotFound - Update sqlite_learning_category_repository to return cerrors.ErrNotFound - Update server.go handleError to check only errors.ErrNotFound - Update test to assert errors.ErrNotFound - Remove unused domain import from server.go --- .../adapters/sqlite_learning_category_repository.go | 3 ++- .../sqlite_learning_category_repository_test.go | 3 ++- .../adapters/sqlite_organization_repository.go | 7 ++++--- internal/kurious/domain/error.go | 12 ++++-------- internal/kurious/ports/http/server.go | 3 +-- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/internal/kurious/adapters/sqlite_learning_category_repository.go b/internal/kurious/adapters/sqlite_learning_category_repository.go index b3e9572..f65e6db 100644 --- a/internal/kurious/adapters/sqlite_learning_category_repository.go +++ b/internal/kurious/adapters/sqlite_learning_category_repository.go @@ -8,6 +8,7 @@ import ( "log/slog" "strings" + cerrors "git.loyso.art/frx/kurious/internal/common/errors" "git.loyso.art/frx/kurious/internal/common/xslices" "git.loyso.art/frx/kurious/internal/kurious/domain" @@ -156,7 +157,7 @@ func (r *sqliteLearingCategoryRepository) Get(ctx context.Context, id string) (c err = r.db.GetContext(ctx, &cdb, query, id) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return domain.LearningCategory{}, domain.ErrNotFound + return domain.LearningCategory{}, cerrors.ErrNotFound } return domain.LearningCategory{}, fmt.Errorf("executing query: %w", err) } diff --git a/internal/kurious/adapters/sqlite_learning_category_repository_test.go b/internal/kurious/adapters/sqlite_learning_category_repository_test.go index 9038cc3..0e5c8c9 100644 --- a/internal/kurious/adapters/sqlite_learning_category_repository_test.go +++ b/internal/kurious/adapters/sqlite_learning_category_repository_test.go @@ -3,6 +3,7 @@ package adapters import ( "testing" + "git.loyso.art/frx/kurious/internal/common/errors" "git.loyso.art/frx/kurious/internal/common/nullable" "git.loyso.art/frx/kurious/internal/kurious/domain" @@ -75,7 +76,7 @@ func (s *sqliteLearningCategoriesRepositorySuite) TestUpsert() { const categoryID = "test-id-1" repo := s.connection.LearningCategory() gotCategory, err := repo.Get(s.ctx, categoryID) - s.ErrorIs(err, domain.ErrNotFound) + s.ErrorIs(err, errors.ErrNotFound) s.Empty(gotCategory) createdCategory := domain.LearningCategory{ diff --git a/internal/kurious/adapters/sqlite_organization_repository.go b/internal/kurious/adapters/sqlite_organization_repository.go index 3ab2e9c..68132e4 100644 --- a/internal/kurious/adapters/sqlite_organization_repository.go +++ b/internal/kurious/adapters/sqlite_organization_repository.go @@ -9,6 +9,7 @@ import ( "strings" "time" + cerrors "git.loyso.art/frx/kurious/internal/common/errors" "git.loyso.art/frx/kurious/internal/common/xslices" "git.loyso.art/frx/kurious/internal/kurious/domain" "go.opentelemetry.io/otel/attribute" @@ -228,7 +229,7 @@ func (r *sqliteOrganizationRepository) Get(ctx context.Context, params domain.Ge err = r.db.GetContext(ctx, &orgdb, query, args...) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return out, domain.ErrNotFound + return out, cerrors.ErrNotFound } return out, fmt.Errorf("executing query: %w", err) } @@ -306,14 +307,14 @@ func (r *sqliteOrganizationRepository) Delete(ctx context.Context, id string) (e result, err := r.db.ExecContext(ctx, query, id) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return domain.ErrNotFound + return cerrors.ErrNotFound } return fmt.Errorf("executing query: %w", err) } affected, _ := result.RowsAffected() if affected == 0 { - return domain.ErrNotFound + return cerrors.ErrNotFound } return nil diff --git a/internal/kurious/domain/error.go b/internal/kurious/domain/error.go index ce3f41c..75fcac5 100644 --- a/internal/kurious/domain/error.go +++ b/internal/kurious/domain/error.go @@ -1,12 +1,8 @@ package domain -const ( - ErrNotFound PlainError = "not found" - ErrNotImplemented PlainError = "not implemented" +import ( + cerrors "git.loyso.art/frx/kurious/internal/common/errors" ) -type PlainError string - -func (err PlainError) Error() string { - return string(err) -} +// ErrNotImplemented is delegated to common/errors.ErrNotImplemented for consistency. +var ErrNotImplemented = cerrors.ErrNotImplemented diff --git a/internal/kurious/ports/http/server.go b/internal/kurious/ports/http/server.go index 10b5243..5b91266 100644 --- a/internal/kurious/ports/http/server.go +++ b/internal/kurious/ports/http/server.go @@ -9,7 +9,6 @@ import ( "git.loyso.art/frx/kurious/internal/common/errors" "git.loyso.art/frx/kurious/internal/common/xcontext" - "git.loyso.art/frx/kurious/internal/kurious/domain" "git.loyso.art/frx/kurious/internal/kurious/service" "git.loyso.art/frx/kurious/pkg/xdefault" @@ -50,7 +49,7 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slo case stderrors.As(err, &valErr): errorString = valErr.Error() code = http.StatusBadRequest - case stderrors.Is(err, errors.ErrNotFound), stderrors.Is(err, domain.ErrNotFound): + case stderrors.Is(err, errors.ErrNotFound): errorString = err.Error() code = http.StatusNotFound default: -- 2.25.1 From bd4a066d101ee0011a1850690dffdbb0b8564a69 Mon Sep 17 00:00:00 2001 From: Aleksandr Trushkin Date: Sun, 28 Jun 2026 10:02:35 +0000 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20remove=20domain->common/errors=20imp?= =?UTF-8?q?ort=20=E2=80=94=20domain=20must=20be=20dependency-free?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review #3 comment: domain package shouldn't depend on any other package. Replace common/errors.ErrNotImplemented alias with stdlib errors.New so domain has zero internal dependencies. --- internal/kurious/domain/error.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/internal/kurious/domain/error.go b/internal/kurious/domain/error.go index 75fcac5..f78426b 100644 --- a/internal/kurious/domain/error.go +++ b/internal/kurious/domain/error.go @@ -1,8 +1,6 @@ package domain -import ( - cerrors "git.loyso.art/frx/kurious/internal/common/errors" -) +import "errors" -// ErrNotImplemented is delegated to common/errors.ErrNotImplemented for consistency. -var ErrNotImplemented = cerrors.ErrNotImplemented +// ErrNotImplemented is returned by interface stubs that have not been implemented yet. +var ErrNotImplemented = errors.New("not implemented") -- 2.25.1 From 0d2c4a7c3a32c3f387fdb5e3c1fd7e32a5bfe158 Mon Sep 17 00:00:00 2001 From: Aleksandr Trushkin Date: Sun, 28 Jun 2026 10:19:33 +0000 Subject: [PATCH 6/7] fix: remove error types from domain package completely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit domain/error.go deleted — errors do not belong in domain. All ErrNotImplemented stubs in repository.go now reference common/errors.ErrNotImplemented via cerrors alias. --- internal/kurious/domain/error.go | 6 ------ internal/kurious/domain/repository.go | 17 +++++++++-------- 2 files changed, 9 insertions(+), 14 deletions(-) delete mode 100644 internal/kurious/domain/error.go diff --git a/internal/kurious/domain/error.go b/internal/kurious/domain/error.go deleted file mode 100644 index f78426b..0000000 --- a/internal/kurious/domain/error.go +++ /dev/null @@ -1,6 +0,0 @@ -package domain - -import "errors" - -// ErrNotImplemented is returned by interface stubs that have not been implemented yet. -var ErrNotImplemented = errors.New("not implemented") diff --git a/internal/kurious/domain/repository.go b/internal/kurious/domain/repository.go index e9e473c..9d33e34 100644 --- a/internal/kurious/domain/repository.go +++ b/internal/kurious/domain/repository.go @@ -4,6 +4,7 @@ import ( "context" "time" + cerrors "git.loyso.art/frx/kurious/internal/common/errors" "git.loyso.art/frx/kurious/internal/common/nullable" ) @@ -141,20 +142,20 @@ func (NotImplementedOrganizationRepository) ListStats( context.Context, ListOrganizationsParams, ) ([]OrganizationStat, error) { - return nil, ErrNotImplemented + return nil, cerrors.ErrNotImplemented } func (NotImplementedOrganizationRepository) List(context.Context, ListOrganizationsParams) ([]Organization, error) { - return nil, ErrNotImplemented + return nil, cerrors.ErrNotImplemented } func (NotImplementedOrganizationRepository) Get(context.Context, GetOrganizationParams) (Organization, error) { - return Organization{}, ErrNotImplemented + return Organization{}, cerrors.ErrNotImplemented } func (NotImplementedOrganizationRepository) Create(context.Context, CreateOrganizationParams) (Organization, error) { - return Organization{}, ErrNotImplemented + return Organization{}, cerrors.ErrNotImplemented } func (NotImplementedOrganizationRepository) Delete(ctx context.Context, id string) error { - return ErrNotImplemented + return cerrors.ErrNotImplemented } //go:generate mockery --name LearningCategoryRepository @@ -168,11 +169,11 @@ type LearningCategoryRepository interface { type NotImplementedLearningCategory struct{} func (NotImplementedLearningCategory) Upsert(context.Context, LearningCategory) error { - return ErrNotImplemented + return cerrors.ErrNotImplemented } func (NotImplementedLearningCategory) List(context.Context) ([]LearningCategory, error) { - return nil, ErrNotImplemented + return nil, cerrors.ErrNotImplemented } func (NotImplementedLearningCategory) Get(context.Context, string) (LearningCategory, error) { - return LearningCategory{}, ErrNotImplemented + return LearningCategory{}, cerrors.ErrNotImplemented } -- 2.25.1 From ca63e04e7d413589ec770499630d76dccd3d23c6 Mon Sep 17 00:00:00 2001 From: Aleksandr Trushkin Date: Sun, 28 Jun 2026 12:57:17 +0000 Subject: [PATCH 7/7] refactor: move mocks and NotImplemented stubs out of domain package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move domain/mocks/ → adapters/mocks/ (update imports + mockery directives) - Extract NotImplementedOrganizationRepository + NotImplementedLearningCategory from domain/repository.go into adapters/not_implemented.go - Remove cerrors import from domain/repository.go (domain now has no dependency on internal/common/errors) - nullable stays in domain: used by entity structs and interface Params Addresses review comments #2 (domain deps) and #3 (mocks in domain) on PR #5. --- .../kurious/adapters/memory_mapper_test.go | 2 +- .../mocks/CourseRepository.go | 0 .../mocks/LearningCategoryRepository.go | 0 .../mocks/OrganizationRepository.go | 0 internal/kurious/adapters/not_implemented.go | 42 +++++++++++++++++++ .../kurious/adapters/ydb_course_repository.go | 4 +- internal/kurious/app/command/command_test.go | 2 +- internal/kurious/app/query/query_test.go | 2 +- internal/kurious/domain/repository.go | 41 ++---------------- 9 files changed, 50 insertions(+), 43 deletions(-) rename internal/kurious/{domain => adapters}/mocks/CourseRepository.go (100%) rename internal/kurious/{domain => adapters}/mocks/LearningCategoryRepository.go (100%) rename internal/kurious/{domain => adapters}/mocks/OrganizationRepository.go (100%) create mode 100644 internal/kurious/adapters/not_implemented.go diff --git a/internal/kurious/adapters/memory_mapper_test.go b/internal/kurious/adapters/memory_mapper_test.go index c405c49..fdb0897 100644 --- a/internal/kurious/adapters/memory_mapper_test.go +++ b/internal/kurious/adapters/memory_mapper_test.go @@ -5,7 +5,7 @@ import ( "testing" "git.loyso.art/frx/kurious/internal/kurious/domain" - mockrepo "git.loyso.art/frx/kurious/internal/kurious/domain/mocks" + mockrepo "git.loyso.art/frx/kurious/internal/kurious/adapters/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" diff --git a/internal/kurious/domain/mocks/CourseRepository.go b/internal/kurious/adapters/mocks/CourseRepository.go similarity index 100% rename from internal/kurious/domain/mocks/CourseRepository.go rename to internal/kurious/adapters/mocks/CourseRepository.go diff --git a/internal/kurious/domain/mocks/LearningCategoryRepository.go b/internal/kurious/adapters/mocks/LearningCategoryRepository.go similarity index 100% rename from internal/kurious/domain/mocks/LearningCategoryRepository.go rename to internal/kurious/adapters/mocks/LearningCategoryRepository.go diff --git a/internal/kurious/domain/mocks/OrganizationRepository.go b/internal/kurious/adapters/mocks/OrganizationRepository.go similarity index 100% rename from internal/kurious/domain/mocks/OrganizationRepository.go rename to internal/kurious/adapters/mocks/OrganizationRepository.go diff --git a/internal/kurious/adapters/not_implemented.go b/internal/kurious/adapters/not_implemented.go new file mode 100644 index 0000000..031cd95 --- /dev/null +++ b/internal/kurious/adapters/not_implemented.go @@ -0,0 +1,42 @@ +package adapters + +import ( + "context" + + cerrors "git.loyso.art/frx/kurious/internal/common/errors" + "git.loyso.art/frx/kurious/internal/kurious/domain" +) + +type NotImplementedOrganizationRepository struct{} + +func (NotImplementedOrganizationRepository) ListStats( + context.Context, + domain.ListOrganizationsParams, +) ([]domain.OrganizationStat, error) { + return nil, cerrors.ErrNotImplemented +} + +func (NotImplementedOrganizationRepository) List(context.Context, domain.ListOrganizationsParams) ([]domain.Organization, error) { + return nil, cerrors.ErrNotImplemented +} +func (NotImplementedOrganizationRepository) Get(context.Context, domain.GetOrganizationParams) (domain.Organization, error) { + return domain.Organization{}, cerrors.ErrNotImplemented +} +func (NotImplementedOrganizationRepository) Create(context.Context, domain.CreateOrganizationParams) (domain.Organization, error) { + return domain.Organization{}, cerrors.ErrNotImplemented +} +func (NotImplementedOrganizationRepository) Delete(ctx context.Context, id string) error { + return cerrors.ErrNotImplemented +} + +type NotImplementedLearningCategory struct{} + +func (NotImplementedLearningCategory) Upsert(context.Context, domain.LearningCategory) error { + return cerrors.ErrNotImplemented +} +func (NotImplementedLearningCategory) List(context.Context) ([]domain.LearningCategory, error) { + return nil, cerrors.ErrNotImplemented +} +func (NotImplementedLearningCategory) Get(context.Context, string) (domain.LearningCategory, error) { + return domain.LearningCategory{}, cerrors.ErrNotImplemented +} diff --git a/internal/kurious/adapters/ydb_course_repository.go b/internal/kurious/adapters/ydb_course_repository.go index 46c6e4b..d6f1ec0 100644 --- a/internal/kurious/adapters/ydb_course_repository.go +++ b/internal/kurious/adapters/ydb_course_repository.go @@ -110,11 +110,11 @@ func (conn *YDBConnection) Close() error { } func (conn *YDBConnection) Organization() domain.OrganizationRepository { - return domain.NotImplementedOrganizationRepository{} + return NotImplementedOrganizationRepository{} } func (conn *YDBConnection) LearningCategory() domain.LearningCategoryRepository { - return domain.NotImplementedLearningCategory{} + return NotImplementedLearningCategory{} } func (conn *YDBConnection) CourseRepository() *ydbCourseRepository { diff --git a/internal/kurious/app/command/command_test.go b/internal/kurious/app/command/command_test.go index 4fbe012..e4fd189 100644 --- a/internal/kurious/app/command/command_test.go +++ b/internal/kurious/app/command/command_test.go @@ -9,7 +9,7 @@ import ( "git.loyso.art/frx/kurious/internal/common/nullable" "git.loyso.art/frx/kurious/internal/kurious/domain" - mockrepo "git.loyso.art/frx/kurious/internal/kurious/domain/mocks" + mockrepo "git.loyso.art/frx/kurious/internal/kurious/adapters/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" diff --git a/internal/kurious/app/query/query_test.go b/internal/kurious/app/query/query_test.go index ecb89dd..17bf53e 100644 --- a/internal/kurious/app/query/query_test.go +++ b/internal/kurious/app/query/query_test.go @@ -8,7 +8,7 @@ import ( "testing" "git.loyso.art/frx/kurious/internal/kurious/domain" - mockrepo "git.loyso.art/frx/kurious/internal/kurious/domain/mocks" + mockrepo "git.loyso.art/frx/kurious/internal/kurious/adapters/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" diff --git a/internal/kurious/domain/repository.go b/internal/kurious/domain/repository.go index 9d33e34..e844c0d 100644 --- a/internal/kurious/domain/repository.go +++ b/internal/kurious/domain/repository.go @@ -4,7 +4,6 @@ import ( "context" "time" - cerrors "git.loyso.art/frx/kurious/internal/common/errors" "git.loyso.art/frx/kurious/internal/common/nullable" ) @@ -81,7 +80,7 @@ type ListStatisticsResult struct { LearningTypeStatistics []StatisticUnit } -//go:generate mockery --name CourseRepository +//go:generate mockery --name CourseRepository --output ../adapters/mocks type CourseRepository interface { // List courses by specifid parameters. List(context.Context, ListCoursesParams) (ListCoursesResult, error) @@ -127,7 +126,7 @@ type ListOrganizationsParams struct { IDs []string } -//go:generate mockery --name OrganizationRepository +//go:generate mockery --name OrganizationRepository --output ../adapters/mocks type OrganizationRepository interface { ListStats(context.Context, ListOrganizationsParams) ([]OrganizationStat, error) List(context.Context, ListOrganizationsParams) ([]Organization, error) @@ -136,44 +135,10 @@ type OrganizationRepository interface { Delete(ctx context.Context, id string) error } -type NotImplementedOrganizationRepository struct{} - -func (NotImplementedOrganizationRepository) ListStats( - context.Context, - ListOrganizationsParams, -) ([]OrganizationStat, error) { - return nil, cerrors.ErrNotImplemented -} - -func (NotImplementedOrganizationRepository) List(context.Context, ListOrganizationsParams) ([]Organization, error) { - return nil, cerrors.ErrNotImplemented -} -func (NotImplementedOrganizationRepository) Get(context.Context, GetOrganizationParams) (Organization, error) { - return Organization{}, cerrors.ErrNotImplemented -} -func (NotImplementedOrganizationRepository) Create(context.Context, CreateOrganizationParams) (Organization, error) { - return Organization{}, cerrors.ErrNotImplemented -} -func (NotImplementedOrganizationRepository) Delete(ctx context.Context, id string) error { - return cerrors.ErrNotImplemented -} - -//go:generate mockery --name LearningCategoryRepository +//go:generate mockery --name LearningCategoryRepository --output ../adapters/mocks type LearningCategoryRepository interface { Upsert(context.Context, LearningCategory) error List(context.Context) ([]LearningCategory, error) Get(context.Context, string) (LearningCategory, error) } - -type NotImplementedLearningCategory struct{} - -func (NotImplementedLearningCategory) Upsert(context.Context, LearningCategory) error { - return cerrors.ErrNotImplemented -} -func (NotImplementedLearningCategory) List(context.Context) ([]LearningCategory, error) { - return nil, cerrors.ErrNotImplemented -} -func (NotImplementedLearningCategory) Get(context.Context, string) (LearningCategory, error) { - return LearningCategory{}, cerrors.ErrNotImplemented -} -- 2.25.1