package api import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" "tera-clone/services/tera-api/internal/adapters/mock" "tera-clone/services/tera-api/internal/directstream" "tera-clone/services/tera-api/internal/domain" "tera-clone/services/tera-api/internal/portservice" "tera-clone/services/tera-api/internal/serial" "tera-clone/services/tera-api/internal/sessionhealth" ) type runtimeSerialController struct{} func (runtimeSerialController) States(context.Context) ([]serial.State, error) { 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: fmt.Sprintf("CH%02d", index), Device: fmt.Sprintf("/dev/ttyWCH%d", index+3), HardwareProfile: "fet3568j-c", MappingVersion: "test-v1", Available: true, ProfileApplied: true, LineSettings: settings, }) } return states, nil } func (runtimeSerialController) LinkStates(context.Context) ([]serial.LinkState, error) { links := make([]serial.LinkState, 0, serial.ChannelCount) for index := 1; index <= serial.ChannelCount; index++ { links = append(links, serial.LinkState{ Channel: fmt.Sprintf("CH%02d", index), State: "unknown", Signal: "unknown", Message: "physical DCD/DSR signal is not board-validated", }) } return links, nil } func (runtimeSerialController) CRProbeStates(context.Context) ([]serial.CRProbeState, error) { confirmedAt := fixedClock().UTC() return []serial.CRProbeState{{ Channel: "CH01", Enabled: true, State: serial.CRProbeStateResponded, Method: "cr", ResponseBytes: 7, ConsecutiveResponses: 1, Up: true, EverUp: true, LastConfirmedAt: &confirmedAt, Message: "remote serial endpoint response confirmed", }, { Channel: "CH08", Enabled: true, State: serial.CRProbeStateResponded, Method: "cr", ResponseBytes: 7, ConsecutiveResponses: 2, Up: true, EverUp: true, LastConfirmedAt: &confirmedAt, Message: "remote serial endpoint response confirmed", }, { Channel: "CH20", Enabled: true, State: serial.CRProbeStateResponded, Method: "cr", ResponseBytes: 7, ConsecutiveResponses: 1, Up: true, EverUp: true, LastConfirmedAt: &confirmedAt, Message: "remote serial endpoint response confirmed", }}, nil } func (runtimeSerialController) Apply(context.Context, []serial.ChannelConfig) ([]serial.State, error) { return nil, nil } type runtimeSessionController struct{} func (runtimeSessionController) Status(context.Context) (sessionhealth.Status, error) { return sessionhealth.Status{ Status: "ok", Broker: directstream.BrokerSnapshot{ TotalAttachments: 2, ActiveChannels: 1, Channels: []directstream.ChannelSnapshot{{ Channel: "CH01", AttachmentCount: 2, MaxAttachments: 4, WriterSessionID: "session-real", QueuedBytes: 7, }}, }, Serial: serial.StreamSnapshot{ ActiveLeases: 1, Channels: []serial.StreamChannelSnapshot{{Channel: "CH01", Active: true, RXBytes: 11, TXBytes: 13}}, }, }, nil } type runtimePortServiceController struct{} func (runtimePortServiceController) Apply(context.Context, []domain.Port) error { return nil } func (runtimePortServiceController) Snapshot() portservice.Snapshot { return portservice.Snapshot{Services: []portservice.ServiceSnapshot{{ Channel: "CH20", Operation: 1, Mode: "reverse_ssh", Port: 2423, State: "listening", ActiveClients: 1, }}} } type countingRuntimeSerialController struct { runtimeSerialController mu sync.Mutex stateCalls int } func (c *countingRuntimeSerialController) States(ctx context.Context) ([]serial.State, error) { c.mu.Lock() c.stateCalls++ c.mu.Unlock() return c.runtimeSerialController.States(ctx) } func (c *countingRuntimeSerialController) calls() int { c.mu.Lock() defer c.mu.Unlock() return c.stateCalls } func TestPortAndSystemStatusUseActualSerialRuntime(t *testing.T) { handler := NewRouterWithOptions(mock.NewStore(fixedClock), RouterOptions{ SerialController: runtimeSerialController{}, SessionController: runtimeSessionController{}, }) portsResponse := httptest.NewRecorder() portsRequest := httptest.NewRequest(http.MethodGet, "/api/v1/ports?limit=48", nil) portsRequest.Header.Set("X-Mock-Actor", "mock-admin") handler.ServeHTTP(portsResponse, portsRequest) if portsResponse.Code != http.StatusOK { t.Fatalf("ports status=%d body=%s", portsResponse.Code, portsResponse.Body.String()) } var ports []domain.Port if err := json.Unmarshal(portsResponse.Body.Bytes(), &ports); err != nil { t.Fatal(err) } if len(ports) != serial.ChannelCount || ports[0].Status != "session" || ports[0].ConnectedUsers != 2 || ports[0].Writer != "session-real" { t.Fatalf("CH01 = %#v", ports[0]) } if ports[0].Runtime == nil || !ports[0].Runtime.ProfileApplied || ports[0].Runtime.RXBytes != 11 || ports[0].Runtime.TXBytes != 13 || ports[0].Runtime.LinkAvailable { t.Fatalf("CH01 runtime = %#v", ports[0].Runtime) } if ports[1].Status != "unknown" { t.Fatalf("CH02 status = %q, want unknown", ports[1].Status) } if ports[7].Runtime == nil || ports[7].Runtime.CRProbeState != serial.CRProbeStateResponded || ports[7].Runtime.CRProbeBytes != 7 || !ports[7].Runtime.CRProbeUp || !ports[7].Runtime.CRProbeEverUp || ports[7].Runtime.CRProbeResponses != 2 || ports[7].Runtime.CRProbeConfirmedAt == nil { t.Fatalf("CH08 CR runtime = %#v", ports[7].Runtime) } statusResponse := httptest.NewRecorder() statusRequest := httptest.NewRequest(http.MethodGet, "/api/v1/system/status", nil) statusRequest.Header.Set("X-Mock-Actor", "mock-admin") handler.ServeHTTP(statusResponse, statusRequest) var status domain.SystemStatus if err := json.Unmarshal(statusResponse.Body.Bytes(), &status); err != nil { t.Fatal(err) } if status.SerialPortCount != serial.ChannelCount || status.ActiveSessions != 2 { t.Fatalf("system status = %#v", status) } servicesResponse := httptest.NewRecorder() servicesRequest := httptest.NewRequest(http.MethodGet, "/api/v1/system/services?limit=100", nil) servicesRequest.Header.Set("X-Mock-Actor", "mock-admin") handler.ServeHTTP(servicesResponse, servicesRequest) var services []domain.ServiceHealth if err := json.Unmarshal(servicesResponse.Body.Bytes(), &services); err != nil { t.Fatal(err) } statusByService := make(map[string]string, len(services)) for _, service := range services { statusByService[service.Service] = service.Status } if statusByService["tera-seriald"] != "ok" || statusByService["tera-sessiond"] != "ok" || statusByService["tera-hwd"] != "degraded" { t.Fatalf("service statuses = %#v", statusByService) } } func TestSerialRuntimeSnapshotIsSharedForShortBurst(t *testing.T) { controller := &countingRuntimeSerialController{} handler := NewRouterWithOptions(mock.NewStore(fixedClock), RouterOptions{ SerialController: controller, SessionController: runtimeSessionController{}, }) requestPorts := func() { t.Helper() response := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, "/api/v1/ports?limit=48", nil) request.Header.Set("X-Mock-Actor", "mock-admin") handler.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("ports status=%d body=%s", response.Code, response.Body.String()) } } requestPorts() requestPorts() if calls := controller.calls(); calls != 1 { t.Fatalf("serial States calls=%d, want 1 for a short request burst", calls) } time.Sleep(runtimeStatusCacheTTL + 50*time.Millisecond) requestPorts() if calls := controller.calls(); calls != 2 { t.Fatalf("serial States calls=%d, want 2 after cache expiry", calls) } } func TestReverseSSHClientMarksPortAsSession(t *testing.T) { handler := NewRouterWithOptions(mock.NewStore(fixedClock), RouterOptions{ SerialController: runtimeSerialController{}, SessionController: runtimeSessionController{}, PortServiceController: runtimePortServiceController{}, }) response := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, "/api/v1/ports?limit=48", nil) request.Header.Set("X-Mock-Actor", "mock-admin") handler.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("ports status=%d body=%s", response.Code, response.Body.String()) } var ports []domain.Port if err := json.Unmarshal(response.Body.Bytes(), &ports); err != nil { t.Fatal(err) } port := ports[19] if port.Channel != "CH20" || port.Status != "session" || port.ConnectedUsers != 1 { t.Fatalf("CH20 = %#v", port) } if port.Runtime == nil || !port.Runtime.SessionActive || len(port.Runtime.Operations) != 1 || port.Runtime.Operations[0].ActiveClients != 1 { t.Fatalf("CH20 runtime = %#v", port.Runtime) } } func TestPortRuntimeStatusEndpointReturnsCompactLiveState(t *testing.T) { handler := NewRouterWithOptions(mock.NewStore(fixedClock), RouterOptions{ SerialController: runtimeSerialController{}, SessionController: runtimeSessionController{}, PortServiceController: runtimePortServiceController{}, }) response := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, "/api/v1/ports/runtime-status", nil) request.Header.Set("X-Mock-Actor", "mock-admin") handler.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("runtime status=%d body=%s", response.Code, response.Body.String()) } var ports []compactPortRuntimeStatus if err := json.Unmarshal(response.Body.Bytes(), &ports); err != nil { t.Fatal(err) } if len(ports) != serial.ChannelCount { t.Fatalf("ports=%d, want %d", len(ports), serial.ChannelCount) } if ports[0].Channel != "CH01" || ports[0].Status != "session" || !ports[0].Runtime.SessionActive { t.Fatalf("CH01 = %#v", ports[0]) } if ports[7].Status != "up" || !ports[7].Runtime.CRProbeUp { t.Fatalf("CH08 = %#v", ports[7]) } if ports[19].Status != "session" || ports[19].ConnectedUsers != 1 { t.Fatalf("CH20 = %#v", ports[19]) } if bytes := response.Body.Bytes(); len(bytes) >= 30000 || string(bytes) == "" { t.Fatalf("compact response size=%d", len(bytes)) } if strings.Contains(response.Body.String(), `"operation_1"`) || strings.Contains(response.Body.String(), `"baud_rate"`) { t.Fatalf("compact response contains port configuration: %s", response.Body.String()) } } func TestRuntimePortStatusRequiresUpBeforeDownAndPrioritizesSession(t *testing.T) { tests := []struct { name string runtime *domain.PortRuntime want string }{ {name: "boot", runtime: &domain.PortRuntime{}, want: "unknown"}, {name: "unconfirmed miss", runtime: &domain.PortRuntime{CRProbeState: serial.CRProbeStateNoResponse}, want: "unknown"}, {name: "confirmed response", runtime: &domain.PortRuntime{CRProbeUp: true, CRProbeEverUp: true}, want: "up"}, {name: "lost after response", runtime: &domain.PortRuntime{CRProbeEverUp: true, CRProbeState: serial.CRProbeStateNoResponse}, want: "down"}, {name: "unknown session is not promoted", runtime: &domain.PortRuntime{SessionActive: true}, want: "unknown"}, {name: "active session", runtime: &domain.PortRuntime{SessionActive: true, CRProbeEverUp: true, CRProbeState: serial.CRProbeStateNoResponse}, want: "session"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if got := runtimePortStatus(test.runtime); got != test.want { t.Fatalf("runtimePortStatus()=%q, want %q", got, test.want) } }) } }