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.
132 lines
4.4 KiB
132 lines
4.4 KiB
|
2 months ago
|
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)
|
||
|
|
}
|
||
|
|
}
|