package main import ( "context" "errors" "fmt" "os" "path/filepath" "testing" "time" "tera-clone/services/tera-api/internal/diagnostics" "tera-clone/services/tera-api/internal/domain" "tera-clone/services/tera-api/internal/security/password" "tera-clone/services/tera-api/internal/serial" "tera-clone/services/tera-api/internal/supportbundle" ) type serialRuntimeStoreStub struct { snapshot domain.ConfigExport err error } func (s serialRuntimeStoreStub) ConfigExport(context.Context) (domain.ConfigExport, error) { return s.snapshot, s.err } type serialRuntimeControllerStub struct { line []serial.ChannelConfig capture []serial.CaptureConfig lineErr error } func (*serialRuntimeControllerStub) States(context.Context) ([]serial.State, error) { return nil, nil } func (s *serialRuntimeControllerStub) Apply(_ context.Context, configs []serial.ChannelConfig) ([]serial.State, error) { s.line = append([]serial.ChannelConfig(nil), configs...) return nil, s.lineErr } func (s *serialRuntimeControllerStub) ApplyCapture(_ context.Context, configs []serial.CaptureConfig) error { s.capture = append(s.capture, configs...) return nil } func TestReconcileSerialRuntimeAppliesAllLineAndCaptureProfiles(t *testing.T) { ports := make([]domain.Port, 0, serial.ChannelCount) for index := 1; index <= serial.ChannelCount; index++ { ports = append(ports, domain.Port{ Channel: serialChannel(index), BaudRate: 115200, DataBits: 8, Parity: "none", StopBits: 1, FlowControl: "none", DataLogMode: "rx_tx", }) } controller := &serialRuntimeControllerStub{} err := reconcileSerialRuntime(context.Background(), serialRuntimeStoreStub{ snapshot: domain.ConfigExport{Running: domain.ConfigExportState{Ports: ports}}, }, controller) if err != nil { t.Fatal(err) } if len(controller.line) != serial.ChannelCount || len(controller.capture) != serial.ChannelCount { t.Fatalf("line=%d capture=%d", len(controller.line), len(controller.capture)) } if controller.line[0].Channel != "CH01" || controller.line[0].BaudRate != 115200 || controller.capture[0].Mode != "rx_tx" { t.Fatalf("CH01 line=%#v capture=%#v", controller.line[0], controller.capture[0]) } } func TestReconcileSerialRuntimeRejectsIncompleteSnapshotAndPropagatesApplyFailure(t *testing.T) { if err := reconcileSerialRuntime(context.Background(), serialRuntimeStoreStub{}, &serialRuntimeControllerStub{}); err == nil { t.Fatal("incomplete serial snapshot must fail") } ports := make([]domain.Port, serial.ChannelCount) for index := range ports { ports[index] = domain.Port{Channel: serialChannel(index + 1), BaudRate: 9600, DataBits: 8, Parity: "none", StopBits: 1, FlowControl: "none", DataLogMode: "rx_tx"} } want := errors.New("line apply failed") err := reconcileSerialRuntime(context.Background(), serialRuntimeStoreStub{snapshot: domain.ConfigExport{Running: domain.ConfigExportState{Ports: ports}}}, &serialRuntimeControllerStub{lineErr: want}) if !errors.Is(err, want) { t.Fatalf("apply error = %v", err) } } func TestSerialRuntimeHealthyRequiresAllVerifiedProfiles(t *testing.T) { states := make([]serial.State, 0, serial.ChannelCount) for index := 1; index <= serial.ChannelCount; index++ { settings := &serial.LineSettings{BaudRate: 115200, DataBits: 8, Parity: "none", StopBits: 1, FlowControl: "none"} states = append(states, serial.State{Channel: serialChannel(index), Available: true, ProfileApplied: true, LineSettings: settings}) } if !serialRuntimeHealthy(states, nil) { t.Fatal("complete verified profiles should be healthy") } states[0].ProfileApplied = false if serialRuntimeHealthy(states, nil) { t.Fatal("a missing applied profile must require reconciliation") } } func serialChannel(index int) string { return fmt.Sprintf("CH%02d", index) } func TestGetenvDurationSeconds(t *testing.T) { t.Setenv("TERA_TEST_TIMEOUT_SECONDS", "7") got := getenvDurationSeconds("TERA_TEST_TIMEOUT_SECONDS", 5*time.Second) if got != 7*time.Second { t.Fatalf("duration = %s, want 7s", got) } } func TestGetenvDurationSecondsFallsBackForInvalidValues(t *testing.T) { for _, value := range []string{"", "0", "-1", "abc"} { t.Setenv("TERA_TEST_TIMEOUT_SECONDS", value) got := getenvDurationSeconds("TERA_TEST_TIMEOUT_SECONDS", 5*time.Second) if got != 5*time.Second { t.Fatalf("duration for %q = %s, want 5s", value, got) } } } func TestMinimumPasswordLengthEnvironment(t *testing.T) { t.Setenv("TERA_API_MIN_PASSWORD_LENGTH", "8") if got := getenvInt("TERA_API_MIN_PASSWORD_LENGTH", 12); got != 8 { t.Fatalf("minimum password length = %d, want 8", got) } } func TestGetenvBool(t *testing.T) { for _, item := range []struct { value string want bool }{ {value: "true", want: true}, {value: "1", want: true}, {value: "yes", want: true}, {value: "on", want: true}, {value: "false", want: false}, {value: "0", want: false}, {value: "no", want: false}, {value: "off", want: false}, } { t.Setenv("TERA_TEST_BOOL", item.value) if got := getenvBool("TERA_TEST_BOOL", !item.want); got != item.want { t.Fatalf("bool for %q = %v, want %v", item.value, got, item.want) } } } func TestGetenvBoolFallsBackForMissingOrInvalidValues(t *testing.T) { for _, value := range []string{"", "maybe"} { t.Setenv("TERA_TEST_BOOL", value) if got := getenvBool("TERA_TEST_BOOL", true); !got { t.Fatalf("bool for %q = false, want fallback true", value) } } } func TestListenAddrConfigUsesTargetEnvironmentName(t *testing.T) { t.Setenv("TERA_API_ADDR", "") t.Setenv("TERA_API_LISTEN_ADDR", "0.0.0.0:8080") if got := listenAddrConfig(); got != "0.0.0.0:8080" { t.Fatalf("listen addr = %q, want target env value", got) } } func TestListenAddrConfigKeepsLegacyEnvironmentName(t *testing.T) { t.Setenv("TERA_API_ADDR", "127.0.0.1:18080") t.Setenv("TERA_API_LISTEN_ADDR", "0.0.0.0:8080") if got := listenAddrConfig(); got != "127.0.0.1:18080" { t.Fatalf("listen addr = %q, want legacy env value", got) } } func TestAllowMockActorsConfigDefaultsToDisabled(t *testing.T) { t.Setenv("TERA_API_ALLOW_MOCK_ACTORS", "") if allowMockActorsConfig() { t.Fatalf("mock actors should be disabled unless explicitly enabled") } } func TestAllowMockActorsConfigCanBeEnabled(t *testing.T) { t.Setenv("TERA_API_ALLOW_MOCK_ACTORS", "true") if !allowMockActorsConfig() { t.Fatalf("mock actors should be enabled when explicitly configured") } } func TestDiagnosticsRunnerConfigIsExplicitAndRejectsRelativePaths(t *testing.T) { runner, err := diagnosticsRunnerConfig(false) if err != nil || runner != nil { t.Fatalf("disabled diagnostics runner = %#v, %v", runner, err) } t.Setenv("TERA_API_DIAGNOSTICS_PING_PATH", "relative-ping") if _, err := diagnosticsRunnerConfig(true); err == nil { t.Fatal("diagnosticsRunnerConfig accepted a relative executable path") } t.Setenv("TERA_API_DIAGNOSTICS_PING_PATH", "/bin/ping") t.Setenv("TERA_API_DIAGNOSTICS_TRACEROUTE_PATH", "/usr/bin/traceroute") runner, err = diagnosticsRunnerConfig(true) if err != nil { t.Fatal(err) } if _, ok := runner.(*diagnostics.ProcessRunner); !ok { t.Fatalf("runner type = %T, want *diagnostics.ProcessRunner", runner) } } func TestSupportBundleServiceConfigIsExplicitAndBounded(t *testing.T) { service, err := supportBundleServiceConfig("") if err != nil || service != nil { t.Fatalf("disabled support bundle service = %#v, %v", service, err) } if _, err := supportBundleServiceConfig("relative/bundles"); err == nil { t.Fatal("support bundle service accepted a relative directory") } t.Setenv("TERA_API_SUPPORT_BUNDLE_MAX_ARTIFACTS", "2") t.Setenv("TERA_API_SUPPORT_BUNDLE_MAX_BYTES", "65536") t.Setenv("TERA_API_SUPPORT_BUNDLE_MAX_SECTION_BYTES", "4096") service, err = supportBundleServiceConfig(t.TempDir()) if err != nil { t.Fatal(err) } if _, ok := service.(*supportbundle.Manager); !ok { t.Fatalf("support bundle service type = %T, want *supportbundle.Manager", service) } } func TestPasswordKDFConfigDefaultsToArgon2id(t *testing.T) { kdf, registry, err := passwordKDFConfig("") if err != nil { t.Fatalf("password KDF config: %v", err) } if kdf.Algorithm() != password.AlgorithmArgon2id { t.Fatalf("kdf algorithm = %q, want argon2id", kdf.Algorithm()) } if !registry.Supports(password.AlgorithmArgon2id) { t.Fatalf("registry should support argon2id") } if registry.Supports(password.AlgorithmPBKDF2SHA256) { t.Fatalf("default registry should not enable pbkdf2") } } func TestPasswordKDFConfigSupportsFIPSCompatiblePBKDF2(t *testing.T) { for _, mode := range []string{password.AlgorithmPBKDF2SHA256, "pbkdf2", "fips"} { kdf, registry, err := passwordKDFConfig(mode) if err != nil { t.Fatalf("password KDF config for %q: %v", mode, err) } if kdf.Algorithm() != password.AlgorithmPBKDF2SHA256 { t.Fatalf("kdf algorithm for %q = %q, want pbkdf2-hmac-sha256", mode, kdf.Algorithm()) } if !registry.Supports(password.AlgorithmPBKDF2SHA256) { t.Fatalf("registry for %q should support pbkdf2", mode) } if registry.Supports(password.AlgorithmArgon2id) { t.Fatalf("fips-compatible registry for %q should not enable argon2id", mode) } } } func TestPasswordKDFConfigRejectsUnknownMode(t *testing.T) { if _, _, err := passwordKDFConfig("scrypt"); err == nil { t.Fatalf("unknown password KDF should fail") } } func TestUnnotifiedKernelBootOnlyClaimsNewBootID(t *testing.T) { directory := t.TempDir() bootPath := filepath.Join(directory, "boot-id") markerPath := filepath.Join(directory, "marker") if err := os.WriteFile(bootPath, []byte("boot-a\n"), 0o600); err != nil { t.Fatal(err) } bootID, unnotified, err := unnotifiedKernelBoot(bootPath, markerPath) if err != nil || bootID != "boot-a" || !unnotified { t.Fatalf("first detection = %q, %v, %v", bootID, unnotified, err) } if err := os.WriteFile(markerPath, []byte("boot-a\n"), 0o600); err != nil { t.Fatal(err) } _, unnotified, err = unnotifiedKernelBoot(bootPath, markerPath) if err != nil || unnotified { t.Fatalf("same-boot detection = %v, %v", unnotified, err) } if err := os.WriteFile(bootPath, []byte("boot-b\n"), 0o600); err != nil { t.Fatal(err) } bootID, unnotified, err = unnotifiedKernelBoot(bootPath, markerPath) if err != nil || bootID != "boot-b" || !unnotified { t.Fatalf("new-boot detection = %q, %v, %v", bootID, unnotified, err) } } func TestUnnotifiedKernelBootRejectsInvalidOrUnreadableState(t *testing.T) { directory := t.TempDir() bootPath := filepath.Join(directory, "boot-id") markerPath := filepath.Join(directory, "marker") if _, _, err := unnotifiedKernelBoot(bootPath, markerPath); err == nil { t.Fatal("missing boot id was accepted") } if err := os.WriteFile(bootPath, []byte(" \n"), 0o600); err != nil { t.Fatal(err) } if _, _, err := unnotifiedKernelBoot(bootPath, markerPath); err == nil { t.Fatal("empty boot id was accepted") } }