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.

114 lines
3.4 KiB

package systemtime
import (
"context"
"errors"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
)
type recordedCommand struct {
name string
args []string
}
type fakeCommandRunner struct {
commands []recordedCommand
failAt int
}
func (r *fakeCommandRunner) Run(_ context.Context, name string, args ...string) error {
r.commands = append(r.commands, recordedCommand{name: name, args: append([]string(nil), args...)})
if r.failAt > 0 && len(r.commands) == r.failAt {
return errors.New("command failed")
}
return nil
}
func TestOSNTPApplierSynchronizesAndLeavesDaemonRunning(t *testing.T) {
root := t.TempDir()
configPath := filepath.Join(root, "etc", "ntp.conf")
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(configPath, []byte("server old.example iburst\n"), 0o640); err != nil {
t.Fatal(err)
}
applier, err := NewOSNTPApplier(configPath, filepath.Join(root, "S49ntp"), filepath.Join(root, "ntpd"))
if err != nil {
t.Fatal(err)
}
runner := &fakeCommandRunner{}
applier.runner = runner
appliedAt := time.Date(2026, 7, 30, 7, 1, 2, 0, time.UTC)
applier.clock = func() time.Time { return appliedAt }
got, err := applier.SyncNTP(context.Background(), []string{"ntp.kriss.re.kr", "time.bora.net"})
if err != nil {
t.Fatalf("SyncNTP() error = %v", err)
}
if got != appliedAt {
t.Fatalf("SyncNTP() time = %v, want %v", got, appliedAt)
}
wantCommands := []recordedCommand{
{name: filepath.Join(root, "S49ntp"), args: []string{"stop"}},
{name: filepath.Join(root, "ntpd"), args: []string{"-g", "-q", "-c", configPath}},
{name: filepath.Join(root, "S49ntp"), args: []string{"start"}},
}
if !reflect.DeepEqual(runner.commands, wantCommands) {
t.Fatalf("commands = %#v, want %#v", runner.commands, wantCommands)
}
content, err := os.ReadFile(configPath)
if err != nil {
t.Fatal(err)
}
text := string(content)
if !strings.Contains(text, "server ntp.kriss.re.kr iburst") || !strings.Contains(text, "server time.bora.net iburst") {
t.Fatalf("NTP config = %q", text)
}
}
func TestOSNTPApplierRestoresConfigurationWhenSyncFails(t *testing.T) {
root := t.TempDir()
configPath := filepath.Join(root, "etc", "ntp.conf")
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
t.Fatal(err)
}
before := []byte("server old.example iburst\n")
if err := os.WriteFile(configPath, before, 0o640); err != nil {
t.Fatal(err)
}
applier, err := NewOSNTPApplier(configPath, filepath.Join(root, "S49ntp"), filepath.Join(root, "ntpd"))
if err != nil {
t.Fatal(err)
}
runner := &fakeCommandRunner{failAt: 2}
applier.runner = runner
applier.clock = func() time.Time { return time.Date(2026, 7, 30, 7, 1, 2, 0, time.UTC) }
if _, err := applier.SyncNTP(context.Background(), []string{"ntp.kriss.re.kr"}); err == nil {
t.Fatal("SyncNTP() succeeded, want failure")
}
after, err := os.ReadFile(configPath)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(after, before) {
t.Fatalf("restored config = %q, want %q", after, before)
}
if len(runner.commands) != 3 || runner.commands[2].args[0] != "start" {
t.Fatalf("rollback commands = %#v", runner.commands)
}
}
func TestBuildNTPConfigCannotContainLineInjection(t *testing.T) {
_, err := NormalizeNTPRequest(NTPRequest{Servers: []string{"ntp.example\nserver evil"}, Timezone: "UTC"})
if err == nil {
t.Fatal("NormalizeNTPRequest() accepted a line break")
}
}