package systemtime import ( "context" "errors" "strings" "testing" "time" ) type fakeApplier struct { timezone string appliedTime time.Time setTimeErr error rollbackCalled bool } type fakeNTPApplier struct { servers []string result time.Time err error } func (a *fakeNTPApplier) SyncNTP(_ context.Context, servers []string) (time.Time, error) { a.servers = append([]string(nil), servers...) return a.result, a.err } func (a *fakeApplier) ApplyTimezone(_ context.Context, timezone string) (func() error, error) { a.timezone = timezone return func() error { a.rollbackCalled = true a.timezone = "before" return nil }, nil } func (a *fakeApplier) SetTime(_ context.Context, value time.Time) error { if a.setTimeErr != nil { return a.setTimeErr } a.appliedTime = value return nil } func TestTransactionalControllerSyncsNormalizedTime(t *testing.T) { applier := &fakeApplier{timezone: "before"} controller := NewTransactionalController(applier) requested := time.Date(2026, 7, 23, 16, 30, 0, 0, time.FixedZone("KST", 9*60*60)) result, err := controller.Sync(context.Background(), Request{BrowserTime: requested, Timezone: " Asia/Seoul "}) if err != nil { t.Fatalf("Sync() error = %v", err) } if result.AppliedTime != requested.UTC() || applier.appliedTime != requested.UTC() { t.Fatalf("applied time = %v / %v, want %v", result.AppliedTime, applier.appliedTime, requested.UTC()) } if result.Timezone != "Asia/Seoul" || applier.timezone != "Asia/Seoul" { t.Fatalf("timezone = %q / %q, want Asia/Seoul", result.Timezone, applier.timezone) } if applier.rollbackCalled { t.Fatal("rollback was called after successful sync") } } func TestTransactionalControllerRollsBackTimezoneWhenClockFails(t *testing.T) { applier := &fakeApplier{timezone: "before", setTimeErr: errors.New("permission denied")} controller := NewTransactionalController(applier) _, err := controller.Sync(context.Background(), Request{BrowserTime: time.Date(2026, 7, 23, 0, 0, 0, 0, time.UTC), Timezone: "UTC"}) if err == nil || !strings.Contains(err.Error(), "permission denied") { t.Fatalf("Sync() error = %v, want clock failure", err) } if !applier.rollbackCalled || applier.timezone != "before" { t.Fatalf("timezone rollback = %t / %q, want true / before", applier.rollbackCalled, applier.timezone) } } func TestTransactionalControllerSynchronizesNTP(t *testing.T) { timeApplier := &fakeApplier{timezone: "before"} appliedAt := time.Date(2026, 7, 30, 7, 1, 2, 0, time.UTC) ntpApplier := &fakeNTPApplier{result: appliedAt} controller := NewTransactionalController(timeApplier, ntpApplier) result, err := controller.SyncNTP(context.Background(), NTPRequest{ Servers: []string{" NTP.KRISS.RE.KR ", "time.bora.net"}, Timezone: "Asia/Seoul", }) if err != nil { t.Fatalf("SyncNTP() error = %v", err) } if result.AppliedTime != appliedAt || result.Timezone != "Asia/Seoul" { t.Fatalf("SyncNTP() = %+v", result) } if len(ntpApplier.servers) != 2 || ntpApplier.servers[0] != "ntp.kriss.re.kr" { t.Fatalf("normalized servers = %#v", ntpApplier.servers) } } func TestTransactionalControllerRollsBackTimezoneWhenNTPSyncFails(t *testing.T) { timeApplier := &fakeApplier{timezone: "before"} controller := NewTransactionalController(timeApplier, &fakeNTPApplier{err: errors.New("no route")}) _, err := controller.SyncNTP(context.Background(), NTPRequest{Servers: []string{"ntp.kriss.re.kr"}, Timezone: "Asia/Seoul"}) if err == nil || !strings.Contains(err.Error(), "no route") { t.Fatalf("SyncNTP() error = %v, want route failure", err) } if !timeApplier.rollbackCalled || timeApplier.timezone != "before" { t.Fatalf("timezone rollback = %t / %q", timeApplier.rollbackCalled, timeApplier.timezone) } } func TestNormalizeRequestRejectsUnsafeValues(t *testing.T) { tests := []Request{ {Timezone: "UTC"}, {BrowserTime: time.Date(1999, 1, 1, 0, 0, 0, 0, time.UTC), Timezone: "UTC"}, {BrowserTime: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), Timezone: "../../etc/passwd"}, {BrowserTime: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), Timezone: "/etc/localtime"}, } for _, request := range tests { if _, err := NormalizeRequest(request); err == nil { t.Fatalf("NormalizeRequest(%+v) succeeded, want error", request) } } } func TestUnavailableControllerReturnsTypedError(t *testing.T) { _, err := (UnavailableController{}).Sync(context.Background(), Request{}) if !IsUnavailable(err) { t.Fatalf("IsUnavailable(%v) = false", err) } }