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.
289 lines
12 KiB
289 lines
12 KiB
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: 9600, 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{CRProbeEnabled: true, CRProbeUp: true, CRProbeEverUp: true}, want: "up"},
|
|
{name: "no response is not cable down", runtime: &domain.PortRuntime{CRProbeEnabled: true, CRProbeEverUp: true, CRProbeState: serial.CRProbeStateNoResponse}, want: "unknown"},
|
|
{name: "no RX timestamp", runtime: &domain.PortRuntime{RXAgeMillis: 0}, want: "unknown"},
|
|
{name: "confirmed hardware down", runtime: &domain.PortRuntime{LinkAvailable: true, LinkEverUp: true, LinkState: "down", CRProbeEnabled: true, CRProbeUp: true}, 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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|