2 Commits

Author SHA1 Message Date
590fed23ca refactor: use map[string]struct{} for allowedOrderBy whitelist
Per review feedback on PR #17, comment #137: use map[string]struct{}
(set semantics) instead of map[string]bool for the ORDER BY field
whitelist. Updates the lookup to comma-ok idiom.
2026-07-20 16:48:13 +00:00
frx
7bf95f136f fix(issue-16): fix critical transaction bug and high-severity repository issues 2026-07-09 13:55:55 +00:00
4 changed files with 90 additions and 14 deletions

View File

@ -73,18 +73,24 @@ func (r *sqliteCourseRepository) List(
params.OrderBy = "id" params.OrderBy = "id"
} }
var allowedOrderFields = map[string]bool{
"id": true, "full_price": true, "name": true,
"discount": true, "duration": true, "starts_at": true,
}
if !allowedOrderFields[params.OrderBy] {
params.OrderBy = "id"
}
direction := "ASC" direction := "ASC"
if !params.Ascending { if !params.Ascending {
direction = "DESC" direction = "DESC"
} }
allowedOrderBy := map[string]struct{}{
"id": {},
"name": {},
"created_at": {},
"updated_at": {},
"full_price": {},
"discount": {},
"duration": {},
}
if _, ok := allowedOrderBy[params.OrderBy]; !ok {
return result, fmt.Errorf("invalid order by field: %s", params.OrderBy)
}
query += " ORDER BY " + params.OrderBy + " " + direction query += " ORDER BY " + params.OrderBy + " " + direction
if params.Limit > 0 { if params.Limit > 0 {
@ -340,10 +346,7 @@ func (r *sqliteCourseRepository) CreateBatch(ctx context.Context, params ...doma
func (r *sqliteCourseRepository) Create(ctx context.Context, params domain.CreateCourseParams) (domain.Course, error) { func (r *sqliteCourseRepository) Create(ctx context.Context, params domain.CreateCourseParams) (domain.Course, error) {
err := r.CreateBatch(ctx, params) err := r.CreateBatch(ctx, params)
if err != nil {
return domain.Course{}, err return domain.Course{}, err
}
return r.Get(ctx, params.ID)
} }
func (r *sqliteCourseRepository) UpdateCourseDescription(ctx context.Context, id, description string) error { func (r *sqliteCourseRepository) UpdateCourseDescription(ctx context.Context, id, description string) error {
@ -426,7 +429,7 @@ func scanRows(ctx context.Context, db *sqlx.DB, f func(rowsScanner) error, query
} }
func createCourseParamsAsValues(params domain.CreateCourseParams) []any { func createCourseParamsAsValues(params domain.CreateCourseParams) []any {
now := time.Now().UTC() now := time.Now()
return []any{ return []any{
params.ID, params.ID,

View File

@ -1,10 +1,12 @@
package adapters package adapters
import ( import (
"errors"
"strconv" "strconv"
"testing" "testing"
"time" "time"
cerrors "git.loyso.art/frx/kurious/internal/common/errors"
"git.loyso.art/frx/kurious/internal/common/nullable" "git.loyso.art/frx/kurious/internal/common/nullable"
"git.loyso.art/frx/kurious/internal/kurious/domain" "git.loyso.art/frx/kurious/internal/kurious/domain"
@ -105,3 +107,33 @@ func (s *sqliteCourseRepositorySuite) TestListLimitOffset() {
s.NoError(err) s.NoError(err)
s.Empty(result.Courses) s.Empty(result.Courses)
} }
func (s *sqliteCourseRepositorySuite) TestCreateBatchPartialFailure() {
cr := s.connection.CourseRepository()
baseParams := domain.CreateCourseParams{
SourceType: domain.SourceTypeManual,
}
first := baseParams
first.ID = "dup-id"
duplicate := baseParams
duplicate.ID = "dup-id"
err := cr.CreateBatch(s.ctx, first, duplicate)
s.Require().Error(err)
result, listErr := cr.List(s.ctx, domain.ListCoursesParams{})
s.Require().NoError(listErr)
s.Empty(result.Courses, "partial batch should be rolled back, not committed")
s.Zero(result.Count, "partial batch should be rolled back, not committed")
}
func (s *sqliteCourseRepositorySuite) TestGetNotFound() {
cr := s.connection.CourseRepository()
_, err := cr.Get(s.ctx, "does-not-exist")
s.Require().Error(err)
s.True(errors.Is(err, cerrors.ErrNotFound), "expected ErrNotFound, got %v", err)
}

View File

@ -262,7 +262,6 @@ func (r *sqliteOrganizationRepository) Create(ctx context.Context, params domain
if err != nil { if err != nil {
return out, fmt.Errorf("preparing statement: %w", err) return out, fmt.Errorf("preparing statement: %w", err)
} }
defer stmt.Close()
var orgdb organizationDB var orgdb organizationDB

View File

@ -126,3 +126,45 @@ func (s *sqliteOrganzationRepositorySuite) TestCreate() {
expectedOrganization.UpdatedAt = gotOrganization.UpdatedAt expectedOrganization.UpdatedAt = gotOrganization.UpdatedAt
s.Equal(expectedOrganization, gotOrganization) s.Equal(expectedOrganization, gotOrganization)
} }
func (s *sqliteOrganzationRepositorySuite) TestListByIDs() {
const itemscount = 3
baseOrg := domain.Organization{
Alias: "test-alias",
Name: "test-name",
Site: "test-site",
LogoLink: "test-logo",
}
ids := make([]string, 0, itemscount)
for i := 0; i < itemscount; i++ {
iStr := strconv.Itoa(i)
id := "test-id-" + iStr
ids = append(ids, id)
_, err := s.connection.Organization().Create(s.ctx, domain.CreateOrganizationParams{
ID: id,
ExternalID: nullable.NewValue("test-ext-id-" + iStr),
Alias: baseOrg.Alias,
Name: baseOrg.Name,
Site: baseOrg.Site,
LogoLink: baseOrg.LogoLink,
})
s.Require().NoError(err)
}
wantedIDs := ids[:2]
gotOrgs, err := s.connection.Organization().List(s.ctx, domain.ListOrganizationsParams{
IDs: wantedIDs,
})
s.Require().NoError(err)
gotIDs := make([]string, 0, len(gotOrgs))
for _, o := range gotOrgs {
gotIDs = append(gotIDs, o.ID)
}
s.Len(gotOrgs, len(wantedIDs))
s.ElementsMatch(wantedIDs, gotIDs)
}