You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

131 lines
4.4 KiB

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)
}
}