package mysql import ( "context" "fmt" "strings" "testing" "time" "gorm.io/driver/sqlite" "gorm.io/gorm" "wucher/internal/domain/base" filemanager "wucher/internal/domain/file_manager" "wucher/internal/service" "wucher/internal/shared/pkg/uuidv7" ) func openBaseTestDB(t *testing.T) *gorm.DB { t.Helper() dsn := fmt.Sprintf("file:base_test_%d?mode=memory&cache=shared", time.Now().UnixNano()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{ NowFunc: func() time.Time { return time.Now().UTC() }, }) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate(&base.BaseCategory{}, &filemanager.Folder{}, &filemanager.File{}, &filemanager.Attachment{}, &base.Base{}, &base.BaseContactRole{}); err != nil { t.Fatalf("auto migrate: %v", err) } if err := db.AutoMigrate(&base.BaseOperationalShiftTime{}); err != nil { t.Fatalf("auto migrate base operational shift time: %v", err) } return db } func TestBaseRepositoryUpdateBase_ReplacesFotoAttachmentID(t *testing.T) { db := openBaseTestDB(t) repo := NewBaseRepository(db) now := time.Now().UTC() category := &base.BaseCategory{ ID: uuidv7.MustBytes(), Key: base.CategoryKeyRegular, Name: "Regular", CreatedAt: now, UpdatedAt: now, } if err := db.Create(category).Error; err != nil { t.Fatalf("create category: %v", err) } folder := &filemanager.Folder{ ID: uuidv7.MustBytes(), Name: "photos", NameNormalized: "photos", Depth: 0, PathCache: "/photos", NameSlot: "live", CreatedAt: now, UpdatedAt: now, } if err := db.Create(folder).Error; err != nil { t.Fatalf("create folder: %v", err) } makeFile := func(name, key string) *filemanager.File { return &filemanager.File{ ID: uuidv7.MustBytes(), FolderID: folder.ID, Name: name, NameNormalized: strings.ToLower(name), Extension: "webp", SizeBytes: 1234, MimeType: "image/webp", Bucket: "bucket", ObjectKey: key, Status: filemanager.FileStatusReady, NameSlot: "live", CreatedAt: now, UpdatedAt: now, } } oldFile := makeFile("old.webp", "objects/old.webp") newFile := makeFile("new.webp", "objects/new.webp") if err := db.Create(oldFile).Error; err != nil { t.Fatalf("create old file: %v", err) } if err := db.Create(newFile).Error; err != nil { t.Fatalf("create new file: %v", err) } oldAttachment := &filemanager.Attachment{ ID: uuidv7.MustBytes(), FileID: oldFile.ID, RefType: "base_photo", RefID: "base-1", IsPrimary: true, CreatedAt: now, UpdatedAt: now, } newAttachment := &filemanager.Attachment{ ID: uuidv7.MustBytes(), FileID: newFile.ID, RefType: "base_photo", RefID: "base-1", IsPrimary: true, CreatedAt: now, UpdatedAt: now, } if err := db.Create(oldAttachment).Error; err != nil { t.Fatalf("create old attachment: %v", err) } if err := db.Create(newAttachment).Error; err != nil { t.Fatalf("create new attachment: %v", err) } row := &base.Base{ ID: uuidv7.MustBytes(), BaseCategoryID: category.ID, BaseName: "Base Old", FotoAttachmentID: oldAttachment.ID, FotoAttachment: oldAttachment, CreatedAt: now, UpdatedAt: now, } if err := db.Create(row).Error; err != nil { t.Fatalf("seed create base: %v", err) } row.FotoAttachmentID = newAttachment.ID row.FotoAttachment = oldAttachment if err := repo.UpdateBase(context.Background(), row, base.CategoryKeyRegular); err != nil { t.Fatalf("update base: %v", err) } loaded, err := repo.GetBaseByID(context.Background(), row.ID, base.CategoryKeyRegular) if err != nil { t.Fatalf("get by id after update: %v", err) } if loaded == nil { t.Fatalf("expected updated row") } if string(loaded.FotoAttachmentID) != string(newAttachment.ID) { t.Fatalf("expected foto_attachment_id replaced") } } func TestBaseRepositoryListBases_DefaultOrderActiveSortkey(t *testing.T) { db := openBaseTestDB(t) repo := NewBaseRepository(db) now := time.Now().UTC() category := &base.BaseCategory{ ID: uuidv7.MustBytes(), Key: base.CategoryKeyRegular, Name: "Regular", CreatedAt: now, UpdatedAt: now, } if err := db.Create(category).Error; err != nil { t.Fatalf("create category: %v", err) } seed := []base.Base{ {ID: uuidv7.MustBytes(), BaseName: "Gamma", IsActive: true}, {ID: uuidv7.MustBytes(), BaseName: "Beta", SortKey: intPtrBaseRepo(0), IsActive: true}, {ID: uuidv7.MustBytes(), BaseName: "Charlie", SortKey: intPtrBaseRepo(2), IsActive: true}, {ID: uuidv7.MustBytes(), BaseName: "Alpha", SortKey: intPtrBaseRepo(1), IsActive: true}, {ID: uuidv7.MustBytes(), BaseName: "Zulu", IsActive: false}, {ID: uuidv7.MustBytes(), BaseName: "Bravo", SortKey: intPtrBaseRepo(9), IsActive: false}, } for i := range seed { if err := repo.CreateBase(context.Background(), &seed[i], base.CategoryKeyRegular); err != nil { t.Fatalf("seed create: %v", err) } } rows, total, err := repo.ListBases(context.Background(), "", "", 0, 0, "", nil) if err != nil { t.Fatalf("list bases: %v", err) } if total != 6 || len(rows) != 6 { t.Fatalf("unexpected total/len total=%d len=%d", total, len(rows)) } gotOrder := []string{rows[0].BaseName, rows[1].BaseName, rows[2].BaseName, rows[3].BaseName, rows[4].BaseName, rows[5].BaseName} wantOrder := []string{"Beta", "Alpha", "Charlie", "Gamma", "Bravo", "Zulu"} for i := range wantOrder { if gotOrder[i] != wantOrder[i] { t.Fatalf("unexpected default order: got=%v want=%v", gotOrder, wantOrder) } } } func TestBaseRepositoryCreateAndGetOperationalShiftTimes(t *testing.T) { db := openBaseTestDB(t) repo := NewBaseRepository(db) now := time.Now().UTC() category := &base.BaseCategory{ ID: uuidv7.MustBytes(), Key: base.CategoryKeyRegular, Name: "Regular", CreatedAt: now, UpdatedAt: now, } if err := db.Create(category).Error; err != nil { t.Fatalf("create category: %v", err) } start := time.Date(1900, 1, 1, 9, 0, 0, 0, time.UTC) end := time.Date(1900, 1, 1, 18, 0, 0, 0, time.UTC) date := time.Date(2026, 4, 12, 0, 0, 0, 0, time.UTC) row := &base.Base{ ID: uuidv7.MustBytes(), BaseCategoryID: category.ID, BaseName: "Base Ops", OperationalShiftTimes: []base.BaseOperationalShiftTime{ { DateStart: &date, DateEnd: &date, StartTimeType: base.ShiftTimeTypeFixed, EndTimeType: base.ShiftTimeTypeFixed, ShiftStart: start.Format("15:04:05"), ShiftEnd: end.Format("15:04:05"), }, }, CreatedAt: now, UpdatedAt: now, } if err := repo.CreateBase(context.Background(), row, base.CategoryKeyRegular); err != nil { t.Fatalf("create base: %v", err) } loaded, err := repo.GetBaseByID(context.Background(), row.ID, base.CategoryKeyRegular) if err != nil { t.Fatalf("get base: %v", err) } if loaded == nil || len(loaded.OperationalShiftTimes) != 1 { t.Fatalf("expected operational shift time to preload") } if got := loaded.OperationalShiftTimes[0].DateStart.Format("2006-01-02"); got != "2026-04-12" { t.Fatalf("unexpected date start: %s", got) } if got := loaded.OperationalShiftTimes[0].DateEnd.Format("2006-01-02"); got != "2026-04-12" { t.Fatalf("unexpected date end: %s", got) } if got := loaded.OperationalShiftTimes[0].ShiftStart; got != "09:00:00" { t.Fatalf("unexpected shift start: %#v", got) } if got := loaded.OperationalShiftTimes[0].StartTimeType; got != base.ShiftTimeTypeFixed { t.Fatalf("unexpected start_time_type: %#v", got) } } func TestBaseRepositoryCreateOperationalShiftTimes_AllowsNilShiftForNonFixedTypes(t *testing.T) { db := openBaseTestDB(t) repo := NewBaseRepository(db) now := time.Now().UTC() category := &base.BaseCategory{ ID: uuidv7.MustBytes(), Key: base.CategoryKeyRegular, Name: "Regular", CreatedAt: now, UpdatedAt: now, } if err := db.Create(category).Error; err != nil { t.Fatalf("create category: %v", err) } date := time.Date(2026, 4, 12, 0, 0, 0, 0, time.UTC) row := &base.Base{ ID: uuidv7.MustBytes(), BaseCategoryID: category.ID, BaseName: "Base Ops Empty Shift", OperationalShiftTimes: []base.BaseOperationalShiftTime{ { DateStart: &date, DateEnd: &date, StartTimeType: base.ShiftTimeTypeBMCT, EndTimeType: base.ShiftTimeTypeECET, }, }, CreatedAt: now, UpdatedAt: now, } if err := repo.CreateBase(context.Background(), row, base.CategoryKeyRegular); err != nil { t.Fatalf("create base: %v", err) } loaded, err := repo.GetBaseByID(context.Background(), row.ID, base.CategoryKeyRegular) if err != nil { t.Fatalf("get base: %v", err) } if loaded == nil || len(loaded.OperationalShiftTimes) != 1 { t.Fatalf("expected operational shift time to preload") } if got := loaded.OperationalShiftTimes[0].StartTimeType; got != base.ShiftTimeTypeBMCT { t.Fatalf("unexpected start_time_type: %#v", got) } if got := loaded.OperationalShiftTimes[0].EndTimeType; got != base.ShiftTimeTypeECET { t.Fatalf("unexpected end_time_type: %#v", got) } if got := loaded.OperationalShiftTimes[0].ShiftStart; got != "" { t.Fatalf("expected empty shift start for non-fixed type, got %#v", got) } if got := loaded.OperationalShiftTimes[0].ShiftEnd; got != "" { t.Fatalf("expected empty shift end for non-fixed type, got %#v", got) } } func TestBaseServiceCreateDetailed_PersistsFixedOperationalShiftTimes(t *testing.T) { db := openBaseTestDB(t) repo := NewBaseRepository(db) svc := service.NewBaseService(repo) now := time.Now().UTC() category := &base.BaseCategory{ ID: uuidv7.MustBytes(), Key: base.CategoryKeyRegular, Name: "Regular", CreatedAt: now, UpdatedAt: now, } if err := db.Create(category).Error; err != nil { t.Fatalf("create category: %v", err) } start := time.Date(2000, 1, 1, 3, 0, 0, 0, time.UTC) end := time.Date(2000, 1, 1, 5, 0, 0, 0, time.UTC) date := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC) res, err := svc.CreateDetailed(context.Background(), service.BaseCreateInput{ ID: uuidv7.MustBytes(), CategoryType: base.CategoryKeyRegular, BaseName: "Base Ops Fixed", Latitude: 47.3769, Longitude: 8.5417, DefaultStartTimeType: base.ShiftTimeTypeFixed, DefaultEndTimeType: base.ShiftTimeTypeFixed, DefaultShiftStart: &start, DefaultShiftEnd: &end, OperationalShiftTimes: []service.BaseOperationalShiftTimeInput{ { DateStart: date, DateEnd: date, StartTimeType: base.ShiftTimeTypeFixed, EndTimeType: base.ShiftTimeTypeFixed, ShiftStart: &start, ShiftEnd: &end, }, }, }) if err != nil { t.Fatalf("create detailed: %v", err) } if res == nil || res.Row == nil { t.Fatalf("expected create detailed result row") } loaded, err := repo.GetBaseByID(context.Background(), res.Row.ID, base.CategoryKeyRegular) if err != nil { t.Fatalf("get base: %v", err) } if loaded == nil || len(loaded.OperationalShiftTimes) != 1 { t.Fatalf("expected operational shift time to preload") } if got := loaded.OperationalShiftTimes[0].ShiftStart; got != "03:00:00" { t.Fatalf("unexpected shift start stored: %#v", got) } if got := loaded.OperationalShiftTimes[0].ShiftEnd; got != "05:00:00" { t.Fatalf("unexpected shift end stored: %#v", got) } } func intPtrBaseRepo(v int) *int { return &v }