package mock
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/netip"
"os"
"path/filepath"
"reflect"
"slices"
"strings"
"testing"
"time"
"tera-clone/services/tera-api/internal/alarmdelivery"
"tera-clone/services/tera-api/internal/diagnostics"
"tera-clone/services/tera-api/internal/domain"
"tera-clone/services/tera-api/internal/externalauth"
"tera-clone/services/tera-api/internal/networkconfig"
"tera-clone/services/tera-api/internal/security/password"
"tera-clone/services/tera-api/internal/snmpagent"
"tera-clone/services/tera-api/internal/supportbundle"
"tera-clone/services/tera-api/internal/systemtime"
"tera-clone/services/tera-api/internal/tlscert"
)
type cacheableCountingKDF struct {
hashes int
}
type acceptingExternalAuthenticator struct { }
type healthCheckingSNMPController struct {
healthErr error
}
func ( c healthCheckingSNMPController ) Apply ( context . Context , snmpagent . Config ) error {
return nil
}
func ( c healthCheckingSNMPController ) Health ( context . Context ) error {
return c . healthErr
}
func ( acceptingExternalAuthenticator ) Authenticate ( _ context . Context , method string , server externalauth . Server , username , credential string , _ time . Duration ) ( bool , error ) {
return method == "radius" && server . Secret == "radius-test-secret" && username == "remote-user" && credential == "remote-password" , nil
}
func ( kdf * cacheableCountingKDF ) Algorithm ( ) string {
return "counting-test"
}
func ( kdf * cacheableCountingKDF ) Hash ( passwordValue , salt string ) ( password . Record , error ) {
kdf . hashes ++
sum := sha256 . Sum256 ( [ ] byte ( salt + ":" + passwordValue ) )
return password . Record {
Algorithm : kdf . Algorithm ( ) ,
Version : 1 ,
Salt : salt ,
Hash : base64 . StdEncoding . EncodeToString ( sum [ : ] ) ,
Params : password . Params {
KeyLength : 32 ,
} ,
} , nil
}
func TestBrowserTimeSyncRejectsUnavailableTimedAndAuditsFailure ( t * testing . T ) {
store , err := NewStoreWithOptions ( fixedClock , StoreOptions {
TimeController : systemtime . UnavailableController { Reason : "tera-timed test outage" } ,
} )
if err != nil {
t . Fatal ( err )
}
ctx := domain . WithActor ( context . Background ( ) , domain . Actor { Name : "mock-admin" } )
_ , err = store . SyncBrowserTime ( ctx , domain . BrowserTimeSyncRequest {
BrowserTime : time . Date ( 2026 , 7 , 23 , 0 , 0 , 0 , 0 , time . UTC ) ,
Timezone : "Asia/Seoul" ,
} )
if ! systemtime . IsUnavailable ( err ) {
t . Fatalf ( "SyncBrowserTime() error = %v, want unavailable" , err )
}
events , err := store . AuditEvents ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
if len ( events ) != 1 || events [ 0 ] . Actor != "mock-admin" || events [ 0 ] . Action != "time.sync_browser" || events [ 0 ] . Result != "rejected" {
t . Fatalf ( "audit events = %+v" , events )
}
}
func TestDefaultPortProfilesUse9600EightNOneWithoutFlowControl ( t * testing . T ) {
store := NewStore ( fixedClock )
ports , err := store . Ports ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read default ports: %v" , err )
}
if len ( ports ) != 48 {
t . Fatalf ( "default port count = %d, want 48" , len ( ports ) )
}
for _ , port := range ports {
if port . BaudRate != 9600 || port . DataBits != 8 || port . Parity != "none" || port . StopBits != 1 || port . FlowControl != "none" {
t . Errorf ( "%s default line profile = %d/%d/%s/%d/%s, want 9600/8/none/1/none" , port . Channel , port . BaudRate , port . DataBits , port . Parity , port . StopBits , port . FlowControl )
}
if port . Mode != "disabled" || port . ListenerPort != 0 || port . Operation1 . Mode != "disabled" || port . Operation2 . Mode != "disabled" || port . Operation1 . MaxClients != 4 || port . Operation2 . MaxClients != 4 {
t . Errorf ( "%s default operation profile = mode %q op1 %#v op2 %#v, want both Disabled with four-client ceiling" , port . Channel , port . Mode , port . Operation1 , port . Operation2 )
}
}
}
func ( kdf * cacheableCountingKDF ) Verify ( record password . Record , passwordValue string ) ( bool , error ) {
next , err := kdf . Hash ( passwordValue , record . Salt )
if err != nil {
return false , err
}
return next . Hash == record . Hash , nil
}
func TestNormalizeMinimumPasswordLength ( t * testing . T ) {
tests := [ ] struct {
name string
value int
want int
} {
{ name : "default" , value : 0 , want : 8 } ,
{ name : "lower bound" , value : 7 , want : 8 } ,
{ name : "configured" , value : 10 , want : 10 } ,
{ name : "upper bound" , value : 129 , want : 128 } ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
if got := normalizeMinimumPasswordLength ( test . value ) ; got != test . want {
t . Fatalf ( "normalizeMinimumPasswordLength(%d) = %d, want %d" , test . value , got , test . want )
}
} )
}
}
func ( kdf * cacheableCountingKDF ) DefaultAuthSeedCacheKey ( ) string {
return fmt . Sprintf ( "%s-v1" , kdf . Algorithm ( ) )
}
func TestDefaultAuthUsersCachesStableKDFSeed ( t * testing . T ) {
kdf := & cacheableCountingKDF { }
first , err := defaultAuthUsers ( kdf )
if err != nil {
t . Fatalf ( "first default auth users: %v" , err )
}
second , err := defaultAuthUsers ( kdf )
if err != nil {
t . Fatalf ( "second default auth users: %v" , err )
}
if kdf . hashes != len ( first ) {
t . Fatalf ( "hash count = %d, want %d" , kdf . hashes , len ( first ) )
}
first [ "admin" ] = authUserRecord { }
if second [ "admin" ] . Username != "admin" {
t . Fatalf ( "cached default auth users share mutable state" )
}
}
func TestAuditLogPersistsAcrossStoreInstances ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "audit.jsonl" )
first , err := NewStoreWithAuditLog ( fixedClock , path , 10 )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
_ , err = first . RecordAudit ( context . Background ( ) , domain . AuditEvent {
Actor : "mock-admin" ,
Action : "config.save" ,
Target : "config" ,
Result : "succeeded" ,
Message : "persistent config saved" ,
} )
if err != nil {
t . Fatalf ( "record audit: %v" , err )
}
second , err := NewStoreWithAuditLog ( fixedClock , path , 10 )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
events , err := second . AuditEvents ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read audit events: %v" , err )
}
if len ( events ) != 1 {
t . Fatalf ( "audit event count = %d, want 1" , len ( events ) )
}
if events [ 0 ] . Action != "config.save" {
t . Fatalf ( "action = %q, want config.save" , events [ 0 ] . Action )
}
if events [ 0 ] . Actor != "mock-admin" {
t . Fatalf ( "actor = %q, want mock-admin" , events [ 0 ] . Actor )
}
}
func TestAuditLogRejectsInvalidEvents ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "audit.jsonl" )
content , err := json . Marshal ( domain . AuditEvent {
AuditID : "audit_20260618_000001" ,
Timestamp : fixedClock ( ) ,
Actor : "mock-admin" ,
Target : "config" ,
Result : "succeeded" ,
Message : "missing action" ,
} )
if err != nil {
t . Fatalf ( "encode audit event: %v" , err )
}
if err := os . WriteFile ( path , append ( content , '\n' ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write audit log: %v" , err )
}
if _ , err := NewStoreWithAuditLog ( fixedClock , path , 10 ) ; err == nil {
t . Fatalf ( "audit log with invalid event should be rejected" )
}
}
func TestSystemAndPortLogsLoadFromFileBackedPrototypeStores ( t * testing . T ) {
dir := t . TempDir ( )
systemPath := filepath . Join ( dir , "system.jsonl" )
portPath := filepath . Join ( dir , "port.jsonl" )
writeJSONLines ( t , systemPath ,
domain . SystemLogEvent {
Timestamp : fixedClock ( ) ,
Severity : "info" ,
Source : "tera-api" ,
Message : "boot complete" ,
} ,
domain . SystemLogEvent {
Timestamp : fixedClock ( ) . Add ( time . Minute ) ,
Severity : "warning" ,
Source : "network" ,
Message : "secondary link degraded" ,
} ,
)
writeJSONLines ( t , portPath ,
domain . PortLogEvent {
Timestamp : fixedClock ( ) ,
Channel : "CH01" ,
Direction : "RX" ,
Actor : "admin/session-1" ,
Message : "login ok" ,
Match : "port" ,
} ,
domain . PortLogEvent {
Timestamp : fixedClock ( ) . Add ( time . Minute ) ,
Channel : "CH02" ,
Direction : "TX" ,
Actor : "ops/session-2" ,
Message : "panic output captured" ,
Match : "keyword" ,
} ,
)
store , err := NewStoreWithOptions ( fixedClock , StoreOptions {
SystemLogPath : systemPath ,
PortLogPath : portPath ,
} )
if err != nil {
t . Fatalf ( "create store: %v" , err )
}
systemEvents , err := store . SystemLogEvents ( context . Background ( ) , domain . SystemLogFilter {
Severity : "warning" ,
Keyword : "link" ,
Limit : 10 ,
} )
if err != nil {
t . Fatalf ( "system log events: %v" , err )
}
if len ( systemEvents ) != 1 {
t . Fatalf ( "system log count = %d, want 1" , len ( systemEvents ) )
}
if systemEvents [ 0 ] . Source != "network" {
t . Fatalf ( "system log source = %q, want network" , systemEvents [ 0 ] . Source )
}
portEvents , err := store . PortLogEvents ( context . Background ( ) , domain . PortLogFilter {
Channel : "CH02" ,
Keyword : "panic" ,
Limit : 10 ,
} )
if err != nil {
t . Fatalf ( "port log events: %v" , err )
}
if len ( portEvents ) != 1 {
t . Fatalf ( "port log count = %d, want 1" , len ( portEvents ) )
}
if portEvents [ 0 ] . Actor != "ops/session-2" {
t . Fatalf ( "port log actor = %q, want ops/session-2" , portEvents [ 0 ] . Actor )
}
}
func TestProductionStoreDoesNotReturnSampleLogs ( t * testing . T ) {
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { } )
if err != nil {
t . Fatal ( err )
}
systemEvents , err := store . SystemLogEvents ( context . Background ( ) , domain . SystemLogFilter { Limit : 100 } )
if err != nil {
t . Fatal ( err )
}
portEvents , err := store . PortLogEvents ( context . Background ( ) , domain . PortLogFilter { Limit : 100 } )
if err != nil {
t . Fatal ( err )
}
if len ( systemEvents ) != 0 || len ( portEvents ) != 0 {
t . Fatalf ( "production store returned sample logs: system=%+v port=%+v" , systemEvents , portEvents )
}
}
func TestSystemLogSearchIncludesAppliedConfigurationAuditEvents ( t * testing . T ) {
dir := t . TempDir ( )
store , err := NewStoreWithOptions ( fixedClock , StoreOptions {
AuditPath : filepath . Join ( dir , "audit.jsonl" ) ,
SystemLogPath : filepath . Join ( dir , "system.jsonl" ) ,
} )
if err != nil {
t . Fatal ( err )
}
ctx := domain . WithActor ( context . Background ( ) , domain . Actor { Name : "admin" } )
hostName := "tera"
if _ , err := store . UpdateNetwork ( ctx , domain . NetworkUpdate { HostName : & hostName } ) ; err != nil {
t . Fatalf ( "stage network configuration: %v" , err )
}
if _ , err := store . ApplyPendingNetwork ( ctx ) ; err != nil {
t . Fatalf ( "apply network configuration: %v" , err )
}
events , err := store . SystemLogEvents ( ctx , domain . SystemLogFilter {
Category : "configuration" ,
Keyword : "network.apply" ,
Limit : 100 ,
} )
if err != nil {
t . Fatal ( err )
}
if len ( events ) != 1 {
t . Fatalf ( "applied configuration log count = %d, want 1: %+v" , len ( events ) , events )
}
if events [ 0 ] . Action != "network.apply" || events [ 0 ] . Actor != "admin" || events [ 0 ] . Target != "network" {
t . Fatalf ( "applied configuration log = %+v" , events [ 0 ] )
}
if events [ 0 ] . AuditID == "" || events [ 0 ] . Source != "network" {
t . Fatalf ( "applied configuration log identity = %+v" , events [ 0 ] )
}
}
func TestSystemLogSearchIncludesLoginFailureEvent ( t * testing . T ) {
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { } )
if err != nil {
t . Fatal ( err )
}
if _ , err := store . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "admin" ,
Password : "wrong-password" ,
} ) ; err == nil {
t . Fatal ( "login with wrong password succeeded" )
}
events , err := store . SystemLogEvents ( context . Background ( ) , domain . SystemLogFilter {
Category : "system" ,
Keyword : "auth.login" ,
Limit : 100 ,
} )
if err != nil {
t . Fatal ( err )
}
if len ( events ) != 1 {
t . Fatalf ( "login failure log count = %d, want 1: %+v" , len ( events ) , events )
}
if events [ 0 ] . Severity != "warning" || events [ 0 ] . Source != "authentication" || events [ 0 ] . Action != "auth.login" {
t . Fatalf ( "login failure log = %+v" , events [ 0 ] )
}
}
func TestFileBackedLogStoresRejectInvalidRecords ( t * testing . T ) {
dir := t . TempDir ( )
systemPath := filepath . Join ( dir , "system-invalid.jsonl" )
portPath := filepath . Join ( dir , "port-invalid.jsonl" )
writeJSONLines ( t , systemPath , domain . SystemLogEvent {
Timestamp : fixedClock ( ) ,
Severity : "debug" ,
Source : "tera-api" ,
Message : "bad severity" ,
} )
if _ , err := NewStoreWithOptions ( fixedClock , StoreOptions { SystemLogPath : systemPath } ) ; err == nil {
t . Fatalf ( "system log store with invalid event should be rejected" )
}
writeJSONLines ( t , portPath , domain . PortLogEvent {
Timestamp : fixedClock ( ) ,
Channel : "CH99" ,
Direction : "RX" ,
Actor : "admin/session-1" ,
Message : "bad channel" ,
Match : "port" ,
} )
if _ , err := NewStoreWithOptions ( fixedClock , StoreOptions { PortLogPath : portPath } ) ; err == nil {
t . Fatalf ( "port log store with invalid event should be rejected" )
}
}
func TestAuditLogSequenceContinuesAfterReload ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "audit.jsonl" )
first , err := NewStoreWithAuditLog ( fixedClock , path , 10 )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
firstAuditID , err := first . RecordAudit ( context . Background ( ) , domain . AuditEvent {
Actor : "mock-admin" ,
Action : "first" ,
Target : "config" ,
Result : "succeeded" ,
Message : "first" ,
} )
if err != nil {
t . Fatalf ( "record first audit: %v" , err )
}
second , err := NewStoreWithAuditLog ( fixedClock , path , 10 )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
secondAuditID , err := second . RecordAudit ( context . Background ( ) , domain . AuditEvent {
Actor : "mock-admin" ,
Action : "second" ,
Target : "config" ,
Result : "succeeded" ,
Message : "second" ,
} )
if err != nil {
t . Fatalf ( "record second audit: %v" , err )
}
if firstAuditID != "audit_20260618_000001" {
t . Fatalf ( "first audit id = %q, want audit_20260618_000001" , firstAuditID )
}
if secondAuditID != "audit_20260618_000002" {
t . Fatalf ( "second audit id = %q, want audit_20260618_000002" , secondAuditID )
}
events , err := second . AuditEvents ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read audit events: %v" , err )
}
if len ( events ) != 2 {
t . Fatalf ( "audit event count = %d, want 2" , len ( events ) )
}
if events [ 0 ] . AuditID != firstAuditID || events [ 1 ] . AuditID != secondAuditID {
t . Fatalf ( "audit ids = %q, %q; want %q, %q" , events [ 0 ] . AuditID , events [ 1 ] . AuditID , firstAuditID , secondAuditID )
}
}
func writeJSONLines ( t * testing . T , path string , values ... any ) {
t . Helper ( )
var builder strings . Builder
encoder := json . NewEncoder ( & builder )
for _ , value := range values {
if err := encoder . Encode ( value ) ; err != nil {
t . Fatalf ( "encode JSON line: %v" , err )
}
}
if err := os . WriteFile ( path , [ ] byte ( builder . String ( ) ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write JSON lines: %v" , err )
}
}
func TestAuditLogKeepsBoundedRecentEvents ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "audit.jsonl" )
store , err := NewStoreWithAuditLog ( fixedClock , path , 2 )
if err != nil {
t . Fatalf ( "create store: %v" , err )
}
for _ , action := range [ ] string { "first" , "second" , "third" } {
_ , err := store . RecordAudit ( context . Background ( ) , domain . AuditEvent {
Actor : "mock-admin" ,
Action : action ,
Target : "config" ,
Result : "succeeded" ,
Message : action ,
} )
if err != nil {
t . Fatalf ( "record %s: %v" , action , err )
}
}
reloaded , err := NewStoreWithAuditLog ( fixedClock , path , 2 )
if err != nil {
t . Fatalf ( "reload store: %v" , err )
}
events , err := reloaded . AuditEvents ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read audit events: %v" , err )
}
if len ( events ) != 2 {
t . Fatalf ( "audit event count = %d, want 2" , len ( events ) )
}
if events [ 0 ] . Action != "second" || events [ 1 ] . Action != "third" {
t . Fatalf ( "actions = %q, %q; want second, third" , events [ 0 ] . Action , events [ 1 ] . Action )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read audit log file: %v" , err )
}
text := string ( content )
trimmed := strings . TrimSpace ( text )
if strings . Count ( trimmed , "\n" ) != 1 {
t . Fatalf ( "audit log file = %q, want exactly 2 jsonl entries" , text )
}
if strings . Contains ( text , ` "action":"first" ` ) {
t . Fatalf ( "audit log file still contains trimmed first event: %s" , text )
}
if ! strings . Contains ( text , ` "action":"second" ` ) || ! strings . Contains ( text , ` "action":"third" ` ) {
t . Fatalf ( "audit log file = %s, want second and third events" , text )
}
}
func TestServiceHealthIncludesRuntimeDaemons ( t * testing . T ) {
store := NewStore ( fixedClock )
health , err := store . ServiceHealth ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read service health: %v" , err )
}
byService := make ( map [ string ] domain . ServiceHealth , len ( health ) )
for _ , item := range health {
byService [ item . Service ] = item
}
for _ , service := range [ ] string {
"tera-api" ,
"tera-web" ,
"tera-authd" ,
"tera-sessiond" ,
"tera-logd" ,
"tera-alarmd" ,
"tera-netd" ,
"tera-timed" ,
"tera-seriald" ,
} {
item , ok := byService [ service ]
if ! ok {
t . Fatalf ( "service health missing %s: %#v" , service , health )
}
if item . Status == "" || item . Detail == "" || item . LastCheckedAt . IsZero ( ) {
t . Fatalf ( "service health for %s = %#v, want status/detail/timestamp" , service , item )
}
}
}
func TestServiceHealthDegradesUnreachableSNMPController ( t * testing . T ) {
store , err := NewStoreWithOptions ( fixedClock , StoreOptions {
SNMPAgentController : healthCheckingSNMPController {
healthErr : & snmpagent . UnavailableError { Reason : "missing socket" } ,
} ,
} )
if err != nil {
t . Fatalf ( "create store: %v" , err )
}
health , err := store . ServiceHealth ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read service health: %v" , err )
}
for _ , item := range health {
if item . Service != "tera-snmpd" {
continue
}
if item . Status != "degraded" || item . Detail != "SNMP agent control service is not reachable" {
t . Fatalf ( "SNMP service health = %#v, want unreachable degraded status" , item )
}
return
}
t . Fatal ( "SNMP service health is missing" )
}
func TestNetworkStateIncludesStableInterfaceIdentity ( t * testing . T ) {
store := NewStore ( fixedClock )
state , err := store . NetworkState ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read network state: %v" , err )
}
if len ( state . Interfaces ) != 2 {
t . Fatalf ( "interfaces = %d, want 2" , len ( state . Interfaces ) )
}
first := state . Interfaces [ 0 ]
if first . Name != "Interface-1" || first . Device != "eth0" || first . DeviceTreeNode != "fe010000.ethernet" || first . ActiveMedium != "RJ45" || first . PHY != "YT8531S" || first . IdentitySource != "board-profile" {
t . Fatalf ( "interface identity = %#v, want stable board-profile metadata" , first )
}
}
func TestPortOperationUsesFixedKeepaliveAndTimeouts ( t * testing . T ) {
operation , err := normalizePortOperation ( domain . PortOperation {
Mode : "reverse_ssh" , Port : 10001 , KeepAliveSeconds : 99 ,
TimeoutMilliseconds : 1 , InactivityTimeoutMilliseconds : 5000 ,
MaxClients : 4 , Authentication : "local" , Newline : "cr" ,
} )
if err != nil {
t . Fatal ( err )
}
if operation . KeepAliveSeconds != 3 || operation . TimeoutMilliseconds != 10000 || operation . InactivityTimeoutMilliseconds != 0 {
t . Fatalf ( "fixed operation timers = %+v" , operation )
}
}
func TestNetworkExtendedSettingsUseConfigTransactionAndPersist ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
store , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatal ( err )
}
hostName := "console-gateway"
operatingMode := "oob"
addressMode := "static"
address := "192.168.10.223/24"
gateway := "192.168.10.1"
ipv6Mode := "static"
ipv6Address := "2001:db8::223/64"
ipv6Gateway := "2001:db8::1"
dns := [ ] string { "8.8.8.8" , "1.1.1.1" }
ipv6DNS := [ ] string { "2001:4860:4860::8888" }
primary := "Interface-2"
fallback := "wwan0"
target := "9.9.9.9"
interval := 15
threshold := 4
result , err := store . UpdateNetwork ( context . Background ( ) , domain . NetworkUpdate {
HostName : & hostName , OperatingMode : & operatingMode ,
OOB : & domain . OOBSettingsUpdate {
PrimaryInterface : & primary , FallbackInterface : & fallback , HealthCheckTarget : & target ,
HealthCheckIntervalSecond : & interval , FailureThreshold : & threshold ,
} ,
Interfaces : [ ] domain . NetworkInterfaceUpdate { {
Name : "Interface-1" , AddressMode : & addressMode , Address : & address , Gateway : & gateway ,
DNSServers : & dns , IPv6Mode : & ipv6Mode , IPv6Address : & ipv6Address ,
IPv6Gateway : & ipv6Gateway , IPv6DNSServers : & ipv6DNS ,
} } ,
} )
if err != nil {
t . Fatal ( err )
}
if result . Config == nil || result . Config . PendingChanges != 1 {
t . Fatalf ( "pending summary = %#v, want one atomic network section" , result . Config )
}
before , _ := store . NetworkState ( context . Background ( ) )
if before . HostName != "iconsole-server" || before . OperatingMode != "single" {
t . Fatalf ( "running network changed before Apply: %#v" , before )
}
if _ , err := store . SaveAndApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatal ( err )
}
reloaded , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatal ( err )
}
after , err := reloaded . NetworkState ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
if after . HostName != hostName || after . OperatingMode != operatingMode || after . OOB . HealthCheckTarget != target || after . OOB . HealthCheckIntervalSecond != interval {
t . Fatalf ( "extended network settings were not persisted: %#v" , after )
}
if after . OOB . PrimaryInterface != "Interface-1" || after . OOB . FallbackInterface != "Interface-2" {
t . Fatalf ( "OOB topology = %+v, want Interface-1/eth0 primary and Interface-2/eth1 fallback" , after . OOB )
}
first := after . Interfaces [ 0 ]
if first . AddressMode != addressMode || first . Address != address || first . Gateway != gateway || first . IPv6Address != ipv6Address || len ( first . DNSServers ) != 2 || len ( first . IPv6DNSServers ) != 1 {
t . Fatalf ( "extended interface settings were not persisted: %#v" , first )
}
}
func TestMigrateNetworkDefaultsCanonicalizesLegacyUnsafeOOB ( t * testing . T ) {
configs := baseNetworkConfig ( )
primary := configs [ networkconfig . Interface1Name ]
primary . OperatingMode = networkconfig . OperatingModeOOB
primary . OOBPrimaryInterface = networkconfig . Interface2Name
primary . OOBFallbackInterface = "wwan0"
primary . AddressMode = networkconfig . AddressModeDHCP
primary . Address = ""
primary . Gateway = ""
configs [ networkconfig . Interface1Name ] = primary
migrateNetworkDefaults ( configs , baseNetworkConfig ( ) )
primary = configs [ networkconfig . Interface1Name ]
fallback := configs [ networkconfig . Interface2Name ]
if primary . OperatingMode != networkconfig . OperatingModeSingle {
t . Fatalf ( "operating mode = %q, want safe Single migration" , primary . OperatingMode )
}
if primary . OOBPrimaryInterface != networkconfig . Interface1Name || primary . OOBFallbackInterface != networkconfig . Interface2Name {
t . Fatalf ( "OOB mapping = %q/%q, want Interface-1/Interface-2" , primary . OOBPrimaryInterface , primary . OOBFallbackInterface )
}
if primary . Role != networkconfig . RoleInternal || fallback . Role != networkconfig . RoleExternal {
t . Fatalf ( "Single roles = %q/%q, want internal/external" , primary . Role , fallback . Role )
}
}
func TestPortOperationCredentialUsesProtectedStoreAndTransactionPhases ( t * testing . T ) {
directory := t . TempDir ( )
configPath := filepath . Join ( directory , "config.json" )
secretPath := filepath . Join ( directory , "secrets.json" )
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : configPath , SecretPath : secretPath } )
if err != nil {
t . Fatal ( err )
}
operation := domain . PortOperation {
Mode : "reverse_ssh" , Port : 12001 , KeepAliveSeconds : 3 , TimeoutMilliseconds : 10000 ,
MaxClients : 4 , Authentication : "local" , Newline : "cr" ,
}
if _ , err := store . UpdatePort ( context . Background ( ) , "CH01" , domain . PortUpdate { Operation1 : & operation } ) ; err != nil {
t . Fatal ( err )
}
exported , err := store . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
encoded , _ := json . Marshal ( exported )
if strings . Contains ( string ( encoded ) , ` "credential" ` ) {
t . Fatalf ( "configuration export leaked operation credential: %s" , encoded )
}
pending , err := store . ResolvePortServiceCredentials ( context . Background ( ) , "pending" , exported . Pending . Ports )
if err != nil || pending [ 0 ] . Operation1 . Credential != "" || pending [ 0 ] . Operation1 . PasswordStored {
t . Fatalf ( "pending credential = %#v, %v" , pending [ 0 ] . Operation1 , err )
}
if _ , err := store . SaveAndApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatal ( err )
}
reloaded , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : configPath , SecretPath : secretPath } )
if err != nil {
t . Fatal ( err )
}
runningExport , _ := reloaded . ConfigExport ( context . Background ( ) )
running , err := reloaded . ResolvePortServiceCredentials ( context . Background ( ) , "running" , runningExport . Running . Ports )
if err != nil || running [ 0 ] . Operation1 . Credential != "" || running [ 0 ] . Operation1 . Authentication != "local" {
t . Fatalf ( "reloaded running credential = %#v, %v" , running [ 0 ] . Operation1 , err )
}
}
func TestDiscardConfigClearsPendingStateAndSecretsWithoutChangingRunningOrPersistent ( t * testing . T ) {
directory := t . TempDir ( )
store , err := NewStoreWithOptions ( fixedClock , StoreOptions {
ConfigPath : filepath . Join ( directory , "config.json" ) ,
SecretPath : filepath . Join ( directory , "secrets.json" ) ,
} )
if err != nil {
t . Fatal ( err )
}
before , err := store . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
alias := "pending-only-alias"
if _ , err := store . UpdatePort ( context . Background ( ) , "CH01" , domain . PortUpdate { Alias : & alias } ) ; err != nil {
t . Fatal ( err )
}
result , err := store . DiscardConfig ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
if result . Result != "discard" || result . Config == nil || result . Config . PendingChanges != 0 {
t . Fatalf ( "discard result = %#v" , result )
}
after , err := store . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
if ! reflect . DeepEqual ( after . Running , before . Running ) || ! reflect . DeepEqual ( after . Persistent , before . Persistent ) {
t . Fatal ( "discard changed running or persistent configuration" )
}
if after . Summary . RunningRevision != before . Summary . RunningRevision || after . Summary . PersistentRevision != before . Summary . PersistentRevision {
t . Fatalf ( "discard changed revisions: before=%#v after=%#v" , before . Summary , after . Summary )
}
events , err := store . AuditEvents ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
if len ( events ) == 0 || events [ len ( events ) - 1 ] . Action != "config.discard" || events [ len ( events ) - 1 ] . Result != "succeeded" {
t . Fatalf ( "discard audit event = %#v" , events )
}
}
func TestConfigStorePersistsPendingRunningAndPersistentState ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
first , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
mode := "Reverse Telnet"
baudRate := 38400
dataBits := 7
parity := "Even"
stopBits := 2
flowControl := "RTSCTS"
result , err := first . UpdatePort ( context . Background ( ) , "CH01" , domain . PortUpdate {
Mode : & mode , BaudRate : & baudRate , DataBits : & dataBits , Parity : & parity ,
StopBits : & stopBits , FlowControl : & flowControl ,
} )
if err != nil {
t . Fatalf ( "stage port update: %v" , err )
}
if result . Config == nil || result . Config . PendingChanges != 1 {
t . Fatalf ( "pending changes = %#v, want 1" , result . Config )
}
second , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
status , err := second . ConfigStatus ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read config status: %v" , err )
}
if status . PendingChanges != 1 || status . RunningRevision != 1 || status . PersistentRevision != 1 {
t . Fatalf ( "status = %#v, want pending 1 running r1 persistent r1" , status )
}
beforeApply , err := second . Port ( context . Background ( ) , "CH01" )
if err != nil {
t . Fatalf ( "read port before apply: %v" , err )
}
if beforeApply . Mode != "disabled" || beforeApply . BaudRate != 9600 || beforeApply . DataBits != 8 || beforeApply . Parity != "none" || beforeApply . StopBits != 1 || beforeApply . FlowControl != "none" {
t . Fatalf ( "running profile before apply = %#v, want unchanged disabled 9600 8N1 none" , beforeApply )
}
if _ , err := second . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply config: %v" , err )
}
third , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
afterApply , err := third . Port ( context . Background ( ) , "CH01" )
if err != nil {
t . Fatalf ( "read port after apply: %v" , err )
}
if afterApply . Mode != "reverse_telnet" || afterApply . BaudRate != 38400 || afterApply . DataBits != 7 || afterApply . Parity != "even" || afterApply . StopBits != 2 || afterApply . FlowControl != "rtscts" {
t . Fatalf ( "running profile after apply = %#v, want reverse_telnet 38400 7E2 rtscts" , afterApply )
}
reloadedStatus , err := third . ConfigStatus ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read reloaded config status: %v" , err )
}
if reloadedStatus . PendingChanges != 0 || reloadedStatus . RunningRevision != 2 || reloadedStatus . PersistentRevision != 1 {
t . Fatalf ( "reloaded status = %#v, want pending 0 running r2 persistent r1" , reloadedStatus )
}
}
func TestConfigStoreMigratesLegacyPortLineDefaults ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
if _ , err := NewStoreWithConfigFile ( fixedClock , path ) ; err != nil {
t . Fatalf ( "create initial store: %v" , err )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read initial config store: %v" , err )
}
var raw map [ string ] any
if err := json . Unmarshal ( content , & raw ) ; err != nil {
t . Fatalf ( "decode initial config store: %v" , err )
}
for _ , section := range [ ] string { "running" , "persistent" } {
ports , ok := raw [ section ] . ( map [ string ] any )
if ! ok {
t . Fatalf ( "%s shape = %T, want object" , section , raw [ section ] )
}
for _ , value := range ports {
port , ok := value . ( map [ string ] any )
if ! ok {
t . Fatalf ( "%s port shape = %T, want object" , section , value )
}
delete ( port , "DataBits" )
delete ( port , "Parity" )
delete ( port , "StopBits" )
delete ( port , "FlowControl" )
}
}
content , err = json . Marshal ( raw )
if err != nil {
t . Fatalf ( "encode legacy config store: %v" , err )
}
if err := os . WriteFile ( path , append ( content , '\n' ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write legacy config store: %v" , err )
}
reloaded , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "reload legacy config store: %v" , err )
}
port , err := reloaded . Port ( context . Background ( ) , "CH01" )
if err != nil {
t . Fatalf ( "read migrated CH01: %v" , err )
}
if port . DataBits != 8 || port . Parity != "none" || port . StopBits != 1 || port . FlowControl != "none" {
t . Fatalf ( "migrated profile = %#v, want 8N1 none" , port )
}
}
func TestConfigStoreMigratesRemovedPortModesWithoutBlockingStartup ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
if _ , err := NewStoreWithConfigFile ( fixedClock , path ) ; err != nil {
t . Fatalf ( "create initial store: %v" , err )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read initial config store: %v" , err )
}
var raw map [ string ] any
if err := json . Unmarshal ( content , & raw ) ; err != nil {
t . Fatalf ( "decode initial config store: %v" , err )
}
for _ , section := range [ ] string { "running" , "persistent" } {
ports := raw [ section ] . ( map [ string ] any )
ch01 := ports [ "CH01" ] . ( map [ string ] any )
ch01 [ "Mode" ] = "realcom"
ch01 [ "ListenerPort" ] = float64 ( 10001 )
ch01 [ "Operation1" ] = map [ string ] any {
"mode" : "realcom" , "port" : float64 ( 10001 ) , "max_clients" : float64 ( 16 ) ,
"authentication" : "none" , "newline" : "none" ,
}
ch01 [ "Operation2" ] = map [ string ] any {
"mode" : "tcp_server" , "port" : float64 ( 20001 ) , "max_clients" : float64 ( 8 ) ,
"authentication" : "none" , "newline" : "none" ,
}
ch02 := ports [ "CH02" ] . ( map [ string ] any )
ch02 [ "Mode" ] = "reverse_ssh"
ch02 [ "ListenerPort" ] = float64 ( 10002 )
ch02 [ "Operation1" ] = map [ string ] any {
"mode" : "reverse_ssh" , "port" : float64 ( 10002 ) , "max_clients" : float64 ( 16 ) ,
"authentication" : "local" , "newline" : "cr" ,
}
}
content , err = json . Marshal ( raw )
if err != nil {
t . Fatalf ( "encode legacy config store: %v" , err )
}
if err := os . WriteFile ( path , append ( content , '\n' ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write legacy config store: %v" , err )
}
reloaded , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "reload legacy operation modes: %v" , err )
}
ch01 , err := reloaded . Port ( context . Background ( ) , "CH01" )
if err != nil {
t . Fatalf ( "read migrated CH01: %v" , err )
}
if ch01 . Mode != "disabled" || ch01 . ListenerPort != 0 || ch01 . Operation1 . Mode != "disabled" || ch01 . Operation1 . Port != 0 || ch01 . Operation2 . Mode != "disabled" || ch01 . Operation2 . Port != 0 {
t . Fatalf ( "migrated removed-mode CH01 = %#v, want both operations disabled" , ch01 )
}
ch02 , err := reloaded . Port ( context . Background ( ) , "CH02" )
if err != nil {
t . Fatalf ( "read migrated CH02: %v" , err )
}
if ch02 . Mode != "reverse_ssh" || ch02 . ListenerPort != 10002 || ch02 . Operation1 . Mode != "reverse_ssh" || ch02 . Operation1 . MaxClients != 4 {
t . Fatalf ( "migrated supported CH02 = %#v, want reverse SSH with max_clients 4" , ch02 )
}
}
func TestConfigStoreMigratesMissingExternalAuthDefaultRoleWithoutBlockingStartup ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
if _ , err := NewStoreWithConfigFile ( fixedClock , path ) ; err != nil {
t . Fatalf ( "create initial store: %v" , err )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read initial config store: %v" , err )
}
var snapshot configSnapshot
if err := json . Unmarshal ( content , & snapshot ) ; err != nil {
t . Fatalf ( "decode initial config store: %v" , err )
}
snapshot . RunningExternalAuth . DefaultRole = ""
snapshot . PersistentExternalAuth . DefaultRole = ""
pending := cloneExternalAuth ( * snapshot . RunningExternalAuth )
pending . DefaultRole = ""
snapshot . PendingExternalAuth = & pending
content , err = json . Marshal ( snapshot )
if err != nil {
t . Fatalf ( "encode legacy config store: %v" , err )
}
if err := os . WriteFile ( path , append ( content , '\n' ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write legacy config store: %v" , err )
}
reloaded , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "reload legacy external auth: %v" , err )
}
running , err := reloaded . ExternalAuth ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read migrated external auth: %v" , err )
}
if running . DefaultRole != "viewer" {
t . Fatalf ( "migrated running default role = %q, want viewer" , running . DefaultRole )
}
exported , err := reloaded . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatalf ( "export migrated external auth: %v" , err )
}
if exported . Persistent . ExternalAuth . DefaultRole != "viewer" || exported . Pending . ExternalAuth == nil || exported . Pending . ExternalAuth . DefaultRole != "viewer" {
t . Fatalf ( "migrated external auth export = %#v, want viewer in persistent and pending" , exported )
}
}
func TestConfigStoreRejectsUnknownExternalAuthDefaultRole ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
if _ , err := NewStoreWithConfigFile ( fixedClock , path ) ; err != nil {
t . Fatalf ( "create initial store: %v" , err )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read initial config store: %v" , err )
}
var snapshot configSnapshot
if err := json . Unmarshal ( content , & snapshot ) ; err != nil {
t . Fatalf ( "decode initial config store: %v" , err )
}
snapshot . RunningExternalAuth . DefaultRole = "owner"
content , err = json . Marshal ( snapshot )
if err != nil {
t . Fatalf ( "encode invalid config store: %v" , err )
}
if err := os . WriteFile ( path , append ( content , '\n' ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write invalid config store: %v" , err )
}
if _ , err := NewStoreWithConfigFile ( fixedClock , path ) ; err == nil || ! strings . Contains ( err . Error ( ) , "external auth default_role" ) {
t . Fatalf ( "reload error = %v, want external auth default_role rejection" , err )
}
}
func TestConfigStoreMigratesMissingDefaultAlarmRules ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
if _ , err := NewStoreWithConfigFile ( fixedClock , path ) ; err != nil {
t . Fatalf ( "create initial store: %v" , err )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read initial config store: %v" , err )
}
var raw map [ string ] any
if err := json . Unmarshal ( content , & raw ) ; err != nil {
t . Fatalf ( "decode initial config store: %v" , err )
}
for _ , section := range [ ] string { "running_alarms" , "persistent_alarms" } {
rules , ok := raw [ section ] . ( map [ string ] any )
if ! ok {
t . Fatalf ( "%s shape = %T, want object" , section , raw [ section ] )
}
delete ( rules , "serial_link_down" )
delete ( rules , "ethernet_link_down" )
}
content , err = json . Marshal ( raw )
if err != nil {
t . Fatalf ( "encode legacy config store: %v" , err )
}
if err := os . WriteFile ( path , append ( content , '\n' ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write legacy config store: %v" , err )
}
reloaded , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "reload legacy config store: %v" , err )
}
for ruleID , wantSource := range map [ string ] string {
"serial_link_down" : "serial_link" ,
"ethernet_link_down" : "ethernet_link" ,
} {
rule , err := reloaded . AlarmRule ( context . Background ( ) , ruleID )
if err != nil {
t . Fatalf ( "read migrated %s rule: %v" , ruleID , err )
}
if ! rule . Enabled || rule . Source != wantSource || rule . Trigger != "up to down" {
t . Fatalf ( "migrated %s rule = %#v, want enabled %s up-to-down default" , ruleID , rule , wantSource )
}
}
persisted , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read migrated config store: %v" , err )
}
var migrated map [ string ] any
if err := json . Unmarshal ( persisted , & migrated ) ; err != nil {
t . Fatalf ( "decode migrated config store: %v" , err )
}
for _ , section := range [ ] string { "running_alarms" , "persistent_alarms" } {
rules , ok := migrated [ section ] . ( map [ string ] any )
if ! ok {
t . Fatalf ( "persisted %s shape = %T, want object" , section , migrated [ section ] )
}
for _ , ruleID := range [ ] string { "serial_link_down" , "ethernet_link_down" } {
if _ , ok := rules [ ruleID ] ; ! ok {
t . Fatalf ( "persisted %s missing migrated rule %s" , section , ruleID )
}
}
}
}
func TestConfigStorePersistsAlarmRuleTransactionState ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
first , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
enabled := false
result , err := first . UpdateAlarmRule ( context . Background ( ) , "auth_failures" , domain . AlarmRuleUpdate { Enabled : & enabled , Delivery : [ ] string { "web" , "syslog" } } )
if err != nil {
t . Fatalf ( "stage alarm rule update: %v" , err )
}
if result . Config == nil || result . Config . PendingChanges != 1 {
t . Fatalf ( "pending changes = %#v, want 1" , result . Config )
}
second , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
status , err := second . ConfigStatus ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read config status: %v" , err )
}
if status . PendingChanges != 1 || status . RunningRevision != 1 || status . PersistentRevision != 1 {
t . Fatalf ( "status = %#v, want pending 1 running r1 persistent r1" , status )
}
beforeApply , err := second . AlarmRule ( context . Background ( ) , "auth_failures" )
if err != nil {
t . Fatalf ( "read alarm rule before apply: %v" , err )
}
if ! beforeApply . Enabled {
t . Fatalf ( "enabled before apply = false, want true" )
}
if _ , err := second . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply config: %v" , err )
}
third , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
afterApply , err := third . AlarmRule ( context . Background ( ) , "auth_failures" )
if err != nil {
t . Fatalf ( "read alarm rule after apply: %v" , err )
}
if afterApply . Enabled {
t . Fatalf ( "enabled after apply = true, want false" )
}
if len ( afterApply . Delivery ) != 2 || afterApply . Delivery [ 1 ] != "syslog" {
t . Fatalf ( "delivery after apply = %#v, want web/syslog" , afterApply . Delivery )
}
reloadedStatus , err := third . ConfigStatus ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read reloaded config status: %v" , err )
}
if reloadedStatus . PendingChanges != 0 || reloadedStatus . RunningRevision != 2 || reloadedStatus . PersistentRevision != 1 {
t . Fatalf ( "reloaded status = %#v, want pending 0 running r2 persistent r1" , reloadedStatus )
}
}
func TestConfigStorePersistsAlarmDeliveryPolicyTransactionState ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
first , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
retryCount := 5
retryInterval := 120
queueLimit := 250
result , err := first . UpdateAlarmDeliveryPolicy ( context . Background ( ) , domain . AlarmDeliveryPolicyUpdate {
RetryCount : & retryCount ,
RetryIntervalSeconds : & retryInterval ,
PendingQueueLimit : & queueLimit ,
} )
if err != nil {
t . Fatalf ( "stage alarm delivery policy update: %v" , err )
}
if result . Config == nil || result . Config . PendingChanges != 1 {
t . Fatalf ( "pending changes = %#v, want 1" , result . Config )
}
second , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
beforeApply , err := second . AlarmDeliveryPolicy ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read alarm delivery policy before apply: %v" , err )
}
if beforeApply . RetryCount != 3 || beforeApply . RetryIntervalSeconds != 60 || beforeApply . PendingQueueLimit != 100 {
t . Fatalf ( "alarm delivery policy before apply = %#v, want defaults" , beforeApply )
}
exported , err := second . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatalf ( "export config before apply: %v" , err )
}
if exported . Pending . AlarmDelivery == nil || exported . Pending . AlarmDelivery . RetryCount != retryCount || exported . Pending . AlarmDelivery . RetryIntervalSeconds != retryInterval || exported . Pending . AlarmDelivery . PendingQueueLimit != queueLimit {
t . Fatalf ( "pending alarm delivery export = %#v, want retry %d interval %d queue %d" , exported . Pending . AlarmDelivery , retryCount , retryInterval , queueLimit )
}
if _ , err := second . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply config: %v" , err )
}
third , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
afterApply , err := third . AlarmDeliveryPolicy ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read alarm delivery policy after apply: %v" , err )
}
if afterApply . RetryCount != retryCount || afterApply . RetryIntervalSeconds != retryInterval || afterApply . PendingQueueLimit != queueLimit {
t . Fatalf ( "alarm delivery policy after apply = %#v, want retry %d interval %d queue %d" , afterApply , retryCount , retryInterval , queueLimit )
}
}
func TestConfigStorePersistsEmailNotificationTransactionState ( t * testing . T ) {
directory := t . TempDir ( )
path := filepath . Join ( directory , "config.json" )
secretPath := filepath . Join ( directory , "secrets.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : path , SecretPath : secretPath } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
enabled := true
host := "smtp.local"
port := 587
authEnabled := true
username := "mailer"
from := "tera@example.local"
result , err := first . UpdateEmailNotification ( context . Background ( ) , domain . EmailNotificationUpdate {
Enabled : & enabled ,
SMTPHost : & host ,
SMTPPort : & port ,
AuthEnabled : & authEnabled ,
Username : & username ,
Password : "smtp-test-password" ,
From : & from ,
To : [ ] string { "ops@example.local" } ,
} )
if err != nil {
t . Fatalf ( "stage email notification update: %v" , err )
}
if result . Config == nil || result . Config . PendingChanges != 1 {
t . Fatalf ( "pending changes = %#v, want 1" , result . Config )
}
second , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : path , SecretPath : secretPath } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
beforeApply , err := second . EmailNotification ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read email notification before apply: %v" , err )
}
if beforeApply . SMTPHost != "smtp.example.local" || beforeApply . Enabled {
t . Fatalf ( "email notification before apply = %#v, want defaults" , beforeApply )
}
exported , err := second . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatalf ( "export config before apply: %v" , err )
}
if exported . Pending . EmailNotification == nil || exported . Pending . EmailNotification . SMTPHost != "smtp.local" || len ( exported . Pending . EmailNotification . To ) != 1 {
t . Fatalf ( "pending email notification export = %#v, want smtp.local" , exported . Pending . EmailNotification )
}
if _ , err := second . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply config: %v" , err )
}
third , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : path , SecretPath : secretPath } )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
afterApply , err := third . EmailNotification ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read email notification after apply: %v" , err )
}
if afterApply . SMTPHost != "smtp.local" || ! afterApply . Enabled || len ( afterApply . To ) != 1 || afterApply . To [ 0 ] != "ops@example.local" {
t . Fatalf ( "email notification after apply = %#v, want enabled smtp.local" , afterApply )
}
}
func TestConfigStorePersistsSNMPAgentTransactionState ( t * testing . T ) {
directory := t . TempDir ( )
path := filepath . Join ( directory , "config.json" )
secretPath := filepath . Join ( directory , "secrets.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : path , SecretPath : secretPath } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
enabled := true
location := "Rack 42"
v2cEnabled := false
v3Enabled := true
result , err := first . UpdateSNMPAgent ( context . Background ( ) , domain . SNMPAgentUpdate {
Enabled : & enabled ,
Location : & location ,
V2CEnabled : & v2cEnabled ,
V3Enabled : & v3Enabled ,
ReadOnlyCommunity : "tera-read-only" ,
V3Users : [ ] domain . SNMPV3User {
{ Username : "snmp-admin" , AuthProtocol : "sha256" , PrivacyProtocol : "aes" , AuthKey : "snmp-auth-passphrase" , PrivacyKey : "snmp-privacy-passphrase" , Enabled : true } ,
} ,
} )
if err != nil {
t . Fatalf ( "stage snmp agent update: %v" , err )
}
if result . Config == nil || result . Config . PendingChanges != 1 {
t . Fatalf ( "pending changes = %#v, want 1" , result . Config )
}
second , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : path , SecretPath : secretPath } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
beforeApply , err := second . SNMPAgent ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read snmp agent before apply: %v" , err )
}
if beforeApply . Location != "Data center" || beforeApply . Enabled {
t . Fatalf ( "snmp agent before apply = %#v, want defaults" , beforeApply )
}
exported , err := second . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatalf ( "export config before apply: %v" , err )
}
if exported . Pending . SNMPAgent == nil || exported . Pending . SNMPAgent . Location != "Rack 42" || len ( exported . Pending . SNMPAgent . V3Users ) != 1 {
t . Fatalf ( "pending snmp agent export = %#v, want Rack 42" , exported . Pending . SNMPAgent )
}
if _ , err := second . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply config: %v" , err )
}
third , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : path , SecretPath : secretPath } )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
afterApply , err := third . SNMPAgent ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read snmp agent after apply: %v" , err )
}
if afterApply . Location != "Rack 42" || ! afterApply . Enabled || ! afterApply . V3Enabled || len ( afterApply . V3Users ) != 1 || ! afterApply . V3Users [ 0 ] . Enabled {
t . Fatalf ( "snmp agent after apply = %#v, want enabled Rack 42 v3" , afterApply )
}
}
func TestApplySNMPAgentPassesResolvedSecretsOnlyToRuntimeController ( t * testing . T ) {
directory := t . TempDir ( )
var applied snmpagent . Config
controller := & snmpagent . MockController { ApplyFunc : func ( _ context . Context , config snmpagent . Config ) error {
applied = config
return snmpagent . Validate ( config )
} }
store , err := NewStoreWithOptions ( fixedClock , StoreOptions {
ConfigPath : filepath . Join ( directory , "config.json" ) , SecretPath : filepath . Join ( directory , "secrets.json" ) ,
SNMPAgentController : controller ,
} )
if err != nil {
t . Fatal ( err )
}
enabled := true
v2cEnabled := true
v3Enabled := true
if _ , err := store . UpdateSNMPAgent ( context . Background ( ) , domain . SNMPAgentUpdate {
Enabled : & enabled , V2CEnabled : & v2cEnabled , V3Enabled : & v3Enabled ,
ReadOnlyCommunity : "runtime-read-only" , ReadWriteCommunity : "runtime-read-write" ,
V3Users : [ ] domain . SNMPV3User { {
Username : "runtime-admin" , Access : "write" , AuthProtocol : "sha256" , PrivacyProtocol : "aes" ,
AuthKey : "runtime-auth-key" , PrivacyKey : "runtime-privacy-key" , Enabled : true ,
} } ,
} ) ; err != nil {
t . Fatal ( err )
}
if _ , err := store . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatal ( err )
}
if applied . ReadOnlyCommunity != "runtime-read-only" || applied . ReadWriteCommunity != "runtime-read-write" || len ( applied . V3Users ) != 1 || applied . V3Users [ 0 ] . AuthKey != "runtime-auth-key" || applied . V3Users [ 0 ] . PrivacyKey != "runtime-privacy-key" {
t . Fatalf ( "runtime SNMP config did not receive resolved secrets: %#v" , applied )
}
exported , err := store . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
encoded , err := json . Marshal ( exported )
if err != nil {
t . Fatal ( err )
}
for _ , secret := range [ ] string { "runtime-read-only" , "runtime-read-write" , "runtime-auth-key" , "runtime-privacy-key" } {
if strings . Contains ( string ( encoded ) , secret ) {
t . Fatalf ( "public config export leaked SNMP secret %q: %s" , secret , encoded )
}
}
}
func TestApplySNMPAgentRuntimeFailureKeepsPendingAndRunningState ( t * testing . T ) {
directory := t . TempDir ( )
controller := & snmpagent . MockController { ApplyFunc : func ( context . Context , snmpagent . Config ) error {
return fmt . Errorf ( "snmpd restart failed" )
} }
store , err := NewStoreWithOptions ( fixedClock , StoreOptions {
ConfigPath : filepath . Join ( directory , "config.json" ) , SecretPath : filepath . Join ( directory , "secrets.json" ) ,
SNMPAgentController : controller ,
} )
if err != nil {
t . Fatal ( err )
}
enabled := true
v2cEnabled := true
location := "Pending rack"
if _ , err := store . UpdateSNMPAgent ( context . Background ( ) , domain . SNMPAgentUpdate {
Enabled : & enabled , V2CEnabled : & v2cEnabled , Location : & location , ReadOnlyCommunity : "pending-community" ,
} ) ; err != nil {
t . Fatal ( err )
}
if _ , err := store . ApplyConfig ( context . Background ( ) ) ; err == nil || ! strings . Contains ( err . Error ( ) , "snmpd restart failed" ) {
t . Fatalf ( "ApplyConfig() error = %v, want runtime failure" , err )
}
running , err := store . SNMPAgent ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
if running . Enabled || running . Location != "Data center" {
t . Fatalf ( "running SNMP state changed after failed apply: %#v" , running )
}
exported , err := store . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
if exported . Pending . SNMPAgent == nil || exported . Pending . SNMPAgent . Location != location {
t . Fatalf ( "pending SNMP state was lost after failed apply: %#v" , exported . Pending . SNMPAgent )
}
audits , err := store . AuditEvents ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
if ! slices . ContainsFunc ( audits , func ( event domain . AuditEvent ) bool {
return event . Action == "snmp.agent.apply" && event . Result == "rejected"
} ) {
t . Fatalf ( "SNMP runtime failure audit missing: %#v" , audits )
}
}
func TestConfigStorePersistsSNMPTrapTargetTransactionState ( t * testing . T ) {
directory := t . TempDir ( )
path := filepath . Join ( directory , "config.json" )
secretPath := filepath . Join ( directory , "secrets.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : path , SecretPath : secretPath } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
result , err := first . UpdateSNMPTrapTargets ( context . Background ( ) , domain . SNMPTrapTargetUpdate {
Targets : [ ] domain . SNMPTrapTarget {
{ TargetID : "snmp-primary" , Host : "snmp.local" , Port : 162 , Version : "v3" , Username : "trap-user" , AuthProtocol : "sha1" , PrivacyProtocol : "aes" , EngineID : "tera-core-x48" , Credential : "snmp-trap-passphrase" , PrivacyKey : "snmp-privacy-passphrase" , Enabled : true } ,
} ,
} )
if err != nil {
t . Fatalf ( "stage snmp trap target update: %v" , err )
}
if result . Config == nil || result . Config . PendingChanges != 1 {
t . Fatalf ( "pending changes = %#v, want 1" , result . Config )
}
second , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : path , SecretPath : secretPath } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
beforeApply , err := second . SNMPTrapTargets ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read snmp trap targets before apply: %v" , err )
}
if len ( beforeApply . Targets ) != 1 || beforeApply . Targets [ 0 ] . Host != "snmp.example.local" {
t . Fatalf ( "snmp trap targets before apply = %#v, want defaults" , beforeApply . Targets )
}
exported , err := second . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatalf ( "export config before apply: %v" , err )
}
if exported . Pending . SNMPTrapTargets == nil || len ( exported . Pending . SNMPTrapTargets . Targets ) != 1 || exported . Pending . SNMPTrapTargets . Targets [ 0 ] . Host != "snmp.local" {
t . Fatalf ( "pending snmp trap export = %#v, want snmp.local" , exported . Pending . SNMPTrapTargets )
}
if _ , err := second . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply config: %v" , err )
}
third , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : path , SecretPath : secretPath } )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
afterApply , err := third . SNMPTrapTargets ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read snmp trap targets after apply: %v" , err )
}
if len ( afterApply . Targets ) != 1 || afterApply . Targets [ 0 ] . Host != "snmp.local" || ! afterApply . Targets [ 0 ] . Enabled {
t . Fatalf ( "snmp trap targets after apply = %#v, want enabled snmp.local" , afterApply . Targets )
}
}
func TestConfigExportContainsRunningPersistentAndPendingConfig ( t * testing . T ) {
store := NewStore ( fixedClock )
mode := "Reverse Telnet"
if _ , err := store . UpdatePort ( context . Background ( ) , "CH01" , domain . PortUpdate { Mode : & mode } ) ; err != nil {
t . Fatalf ( "stage port update: %v" , err )
}
enabled := false
if _ , err := store . UpdateAlarmRule ( context . Background ( ) , "auth_failures" , domain . AlarmRuleUpdate { Enabled : & enabled } ) ; err != nil {
t . Fatalf ( "stage alarm update: %v" , err )
}
address := "192.168.10.233/24"
gateway := "192.168.10.1"
if _ , err := store . UpdateNetwork ( context . Background ( ) , domain . NetworkUpdate {
Interfaces : [ ] domain . NetworkInterfaceUpdate { { Name : "Interface-1" , Address : & address , Gateway : & gateway } } ,
} ) ; err != nil {
t . Fatalf ( "stage network update: %v" , err )
}
exported , err := store . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatalf ( "export config: %v" , err )
}
if exported . FormatVersion != 1 {
t . Fatalf ( "format version = %d, want 1" , exported . FormatVersion )
}
if ! exported . GeneratedAt . Equal ( fixedClock ( ) ) {
t . Fatalf ( "generated at = %s, want %s" , exported . GeneratedAt , fixedClock ( ) )
}
if exported . Summary . PendingChanges != 3 || exported . Summary . RunningRevision != 1 || exported . Summary . PersistentRevision != 1 {
t . Fatalf ( "summary = %#v, want pending 3 running r1 persistent r1" , exported . Summary )
}
if len ( exported . Running . Ports ) != 48 || len ( exported . Persistent . Ports ) != 48 {
t . Fatalf ( "running/persistent port counts = %d/%d, want 48/48" , len ( exported . Running . Ports ) , len ( exported . Persistent . Ports ) )
}
if exported . Running . Ports [ 0 ] . Mode != "disabled" || exported . Persistent . Ports [ 0 ] . Mode != "disabled" {
t . Fatalf ( "running/persistent CH01 modes = %q/%q, want disabled" , exported . Running . Ports [ 0 ] . Mode , exported . Persistent . Ports [ 0 ] . Mode )
}
if len ( exported . Pending . Ports ) != 1 || exported . Pending . Ports [ 0 ] . Mode != "reverse_telnet" {
t . Fatalf ( "pending ports = %#v, want CH01 reverse_telnet" , exported . Pending . Ports )
}
if len ( exported . Pending . AlarmRules ) != 1 || exported . Pending . AlarmRules [ 0 ] . RuleID != "auth_failures" || exported . Pending . AlarmRules [ 0 ] . Enabled {
t . Fatalf ( "pending alarm rules = %#v, want disabled auth_failures" , exported . Pending . AlarmRules )
}
if len ( exported . Pending . NetworkInterfaces ) != 2 || exported . Pending . NetworkInterfaces [ 0 ] . Address != address {
t . Fatalf ( "pending network interfaces = %#v, want address %q" , exported . Pending . NetworkInterfaces , address )
}
if exported . Running . Time . Timezone != "Asia/Seoul" || exported . Persistent . Time . Timezone != "Asia/Seoul" {
t . Fatalf ( "running/persistent time zones = %q/%q, want Asia/Seoul" , exported . Running . Time . Timezone , exported . Persistent . Time . Timezone )
}
if exported . Pending . Time != nil {
t . Fatalf ( "pending time = %#v, want nil" , exported . Pending . Time )
}
}
func TestConfigStorePersistsTimeSettingsTransactionState ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
first , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
timezone := "UTC"
result , err := first . UpdateTimeSettings ( context . Background ( ) , domain . TimeSettingsUpdate {
Timezone : & timezone ,
} )
if err != nil {
t . Fatalf ( "stage time settings update: %v" , err )
}
if result . Config == nil || result . Config . PendingChanges != 1 {
t . Fatalf ( "pending changes = %#v, want 1" , result . Config )
}
second , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
beforeApply , err := second . TimeSettings ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read time settings before apply: %v" , err )
}
if beforeApply . Timezone != "Asia/Seoul" {
t . Fatalf ( "timezone before apply = %q, want Asia/Seoul" , beforeApply . Timezone )
}
exported , err := second . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatalf ( "export config before apply: %v" , err )
}
if exported . Pending . Time == nil || exported . Pending . Time . Timezone != timezone {
t . Fatalf ( "pending time export = %#v, want timezone %q" , exported . Pending . Time , timezone )
}
if _ , err := second . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply config: %v" , err )
}
third , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
afterApply , err := third . TimeSettings ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read time settings after apply: %v" , err )
}
if afterApply . Timezone != timezone {
t . Fatalf ( "timezone after apply = %q, want %q" , afterApply . Timezone , timezone )
}
}
func TestConfigStorePersistsLogRetentionTransactionState ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
first , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
portDays := 180
warningPct := 90
result , err := first . UpdateLogRetention ( context . Background ( ) , domain . LogRetentionUpdate {
PortDays : & portDays ,
ProtectedUsageWarningPct : & warningPct ,
} )
if err != nil {
t . Fatalf ( "stage log retention update: %v" , err )
}
if result . Config == nil || result . Config . PendingChanges != 1 {
t . Fatalf ( "pending changes = %#v, want 1" , result . Config )
}
second , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
beforeApply , err := second . LogRetention ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read log retention before apply: %v" , err )
}
if beforeApply . PortDays != 365 {
t . Fatalf ( "port days before apply = %d, want 365" , beforeApply . PortDays )
}
exported , err := second . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatalf ( "export config before apply: %v" , err )
}
if exported . Pending . Logs == nil || exported . Pending . Logs . PortDays != portDays || exported . Pending . Logs . ProtectedUsageWarningPct != warningPct {
t . Fatalf ( "pending log retention export = %#v, want port days %d pct %d" , exported . Pending . Logs , portDays , warningPct )
}
if _ , err := second . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply config: %v" , err )
}
third , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
afterApply , err := third . LogRetention ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read log retention after apply: %v" , err )
}
if afterApply . PortDays != portDays || afterApply . ProtectedUsageWarningPct != warningPct {
t . Fatalf ( "log retention after apply = %#v, want port days %d pct %d" , afterApply , portDays , warningPct )
}
}
func TestConfigStorePersistsManagementAccessTransactionState ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
first , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
httpPort := 8081
httpsPort := 8443
idleTimeout := 30
captchaEnabled := true
loginDetection := 9
loginHits := 4
result , err := first . UpdateManagementAccess ( context . Background ( ) , domain . ManagementAccessUpdate {
HTTPPort : & httpPort ,
HTTPSPort : & httpsPort ,
IdleTimeoutMinutes : & idleTimeout ,
CaptchaEnabled : & captchaEnabled ,
LoginDetectionMinutes : & loginDetection ,
LoginSecurityHitCount : & loginHits ,
} )
if err != nil {
t . Fatalf ( "stage management access update: %v" , err )
}
if result . Config == nil || result . Config . PendingChanges != 1 {
t . Fatalf ( "pending changes = %#v, want 1" , result . Config )
}
second , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
beforeApply , err := second . ManagementAccess ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read management access before apply: %v" , err )
}
if beforeApply . TelnetEnabled || beforeApply . IdleTimeoutMinutes != 15 {
t . Fatalf ( "management access before apply = %#v, want default" , beforeApply )
}
exported , err := second . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatalf ( "export config before apply: %v" , err )
}
if exported . Pending . Management == nil || exported . Pending . Management . SSHEnabled || exported . Pending . Management . TelnetEnabled || exported . Pending . Management . HTTPPort != httpPort || exported . Pending . Management . HTTPSPort != httpsPort || exported . Pending . Management . SSHPort != 22 || exported . Pending . Management . TelnetPort != 23 || exported . Pending . Management . IdleTimeoutMinutes != idleTimeout || ! exported . Pending . Management . CaptchaEnabled || exported . Pending . Management . LoginDetectionMinutes != loginDetection || exported . Pending . Management . LoginSecurityHitCount != loginHits {
t . Fatalf ( "pending management export = %#v, want custom ports and timeout %d" , exported . Pending . Management , idleTimeout )
}
if _ , err := second . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply config: %v" , err )
}
third , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
afterApply , err := third . ManagementAccess ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read management access after apply: %v" , err )
}
if afterApply . SSHEnabled || afterApply . TelnetEnabled || afterApply . HTTPPort != httpPort || afterApply . HTTPSPort != httpsPort || afterApply . SSHPort != 22 || afterApply . TelnetPort != 23 || afterApply . IdleTimeoutMinutes != idleTimeout || ! afterApply . CaptchaEnabled || afterApply . LoginDetectionMinutes != loginDetection || afterApply . LoginSecurityHitCount != loginHits {
t . Fatalf ( "management access after apply = %#v, want custom ports and timeout %d" , afterApply , idleTimeout )
}
}
func TestConfigStorePersistsRemoteSyslogTransactionState ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
first , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
enabled := true
host := "syslog.local"
port := 1514
protocol := "tcp"
facility := "local4"
result , err := first . UpdateRemoteSyslog ( context . Background ( ) , domain . RemoteSyslogUpdate {
Enabled : & enabled ,
Host : & host ,
Port : & port ,
Protocol : & protocol ,
Facility : & facility ,
} )
if err != nil {
t . Fatalf ( "stage remote syslog update: %v" , err )
}
if result . Config == nil || result . Config . PendingChanges != 1 {
t . Fatalf ( "pending changes = %#v, want 1" , result . Config )
}
second , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
beforeApply , err := second . RemoteSyslog ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read remote syslog before apply: %v" , err )
}
if beforeApply . Enabled || beforeApply . Protocol != "udp" {
t . Fatalf ( "remote syslog before apply = %#v, want default disabled udp" , beforeApply )
}
exported , err := second . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatalf ( "export config before apply: %v" , err )
}
if exported . Pending . RemoteSyslog == nil || ! exported . Pending . RemoteSyslog . Enabled || exported . Pending . RemoteSyslog . Host != host || exported . Pending . RemoteSyslog . Protocol != protocol {
t . Fatalf ( "pending remote syslog export = %#v, want enabled host %q protocol %q" , exported . Pending . RemoteSyslog , host , protocol )
}
if _ , err := second . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply config: %v" , err )
}
third , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
afterApply , err := third . RemoteSyslog ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read remote syslog after apply: %v" , err )
}
if ! afterApply . Enabled || afterApply . Host != host || afterApply . Port != port || afterApply . Protocol != protocol || afterApply . Facility != facility {
t . Fatalf ( "remote syslog after apply = %#v, want configured syslog" , afterApply )
}
}
func TestConfigStorePersistsNetworkTransactionState ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
first , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
address := "192.168.10.233/24"
gateway := "192.168.10.1"
result , err := first . UpdateNetwork ( context . Background ( ) , domain . NetworkUpdate {
Interfaces : [ ] domain . NetworkInterfaceUpdate { {
Name : "Interface-1" ,
Address : & address ,
Gateway : & gateway ,
} } ,
} )
if err != nil {
t . Fatalf ( "stage network update: %v" , err )
}
if result . Config == nil || result . Config . PendingChanges != 1 {
t . Fatalf ( "pending changes = %#v, want 1" , result . Config )
}
second , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
status , err := second . ConfigStatus ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read config status: %v" , err )
}
if status . PendingChanges != 1 || status . RunningRevision != 1 || status . PersistentRevision != 1 {
t . Fatalf ( "status = %#v, want pending 1 running r1 persistent r1" , status )
}
beforeApply , err := second . NetworkState ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read network before apply: %v" , err )
}
if beforeApply . Interfaces [ 0 ] . Address != "192.168.0.223/24" {
t . Fatalf ( "running address before apply = %q, want default" , beforeApply . Interfaces [ 0 ] . Address )
}
if _ , err := second . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply config: %v" , err )
}
third , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
afterApply , err := third . NetworkState ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read network after apply: %v" , err )
}
if afterApply . Interfaces [ 0 ] . Address != address || afterApply . Interfaces [ 0 ] . Gateway != gateway {
t . Fatalf ( "network after apply = %#v, want updated address/gateway" , afterApply . Interfaces [ 0 ] )
}
reloadedStatus , err := third . ConfigStatus ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read reloaded config status: %v" , err )
}
if reloadedStatus . PendingChanges != 0 || reloadedStatus . RunningRevision != 2 || reloadedStatus . PersistentRevision != 1 {
t . Fatalf ( "reloaded status = %#v, want pending 0 running r2 persistent r1" , reloadedStatus )
}
}
func TestConfigStorePersistsTLSCertificateTransactionState ( t * testing . T ) {
root := t . TempDir ( )
path := filepath . Join ( root , "config.json" )
tlsRoot := filepath . Join ( root , "tls" )
openTLSStore := func ( ) * tlscert . Repository {
repository , openErr := tlscert . Open ( tlscert . Options {
Root : tlsRoot ,
ActiveCert : filepath . Join ( root , "active.crt" ) ,
ActiveKey : filepath . Join ( root , "active.key" ) ,
Clock : fixedClock ,
} )
if openErr != nil {
t . Fatalf ( "open TLS certificate store: %v" , openErr )
}
return repository
}
certificateID := "operator-imported-2026"
firstTLS := openTLSStore ( )
if _ , err := firstTLS . Generate ( certificateID , tlscert . Subject { CommonName : "console.example.local" } ) ; err != nil {
t . Fatalf ( "generate TLS certificate: %v" , err )
}
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : path , TLSCertificateStore : firstTLS } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
result , err := first . UpdateTLSCertificate ( context . Background ( ) , domain . TLSCertificateUpdate {
ActiveCertificateID : & certificateID ,
} )
if err != nil {
t . Fatalf ( "stage tls certificate update: %v" , err )
}
if result . Config == nil || result . Config . PendingChanges != 1 {
t . Fatalf ( "pending changes = %#v, want 1" , result . Config )
}
second , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : path , TLSCertificateStore : openTLSStore ( ) } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
beforeApply , err := second . TLSCertificate ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read tls certificate before apply: %v" , err )
}
if beforeApply . ActiveCertificateID != "factory-default" {
t . Fatalf ( "active certificate before apply = %q, want factory-default" , beforeApply . ActiveCertificateID )
}
exported , err := second . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatalf ( "export config before apply: %v" , err )
}
if exported . Pending . TLSCertificate == nil || exported . Pending . TLSCertificate . ActiveCertificateID != certificateID {
t . Fatalf ( "pending tls certificate export = %#v, want %q" , exported . Pending . TLSCertificate , certificateID )
}
if exported . Pending . TLSCertificate . PrivateKeyStored != true {
t . Fatalf ( "pending tls private key storage flag = false, want true" )
}
if _ , err := second . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply config: %v" , err )
}
third , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : path , TLSCertificateStore : openTLSStore ( ) } )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
afterApply , err := third . TLSCertificate ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read tls certificate after apply: %v" , err )
}
if afterApply . ActiveCertificateID != certificateID || afterApply . Source != "generated" {
t . Fatalf ( "tls certificate after apply = %#v, want %q generated" , afterApply , certificateID )
}
}
func TestConfigStorePersistsExternalAuthTransactionState ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
// External credentials persist separately from the exported configuration.
// Reopening only the configuration previously hid a lost-secret failure.
options := StoreOptions { ConfigPath : path , SecretPath : filepath . Join ( filepath . Dir ( path ) , "secrets.json" ) }
first , err := NewStoreWithOptions ( fixedClock , options )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
enabled := true
method := "radius"
timeoutSeconds := 10
servers := [ ] domain . ExternalAuthServer {
{ ServerID : "radius-primary" , Host : "radius.local" , Port : 1812 , Secret : "radius-test-shared-secret" , Enabled : true } ,
}
result , err := first . UpdateExternalAuth ( context . Background ( ) , domain . ExternalAuthUpdate {
Enabled : & enabled ,
PrimaryMethod : & method ,
TimeoutSeconds : & timeoutSeconds ,
RadiusServers : servers ,
} )
if err != nil {
t . Fatalf ( "stage external auth update: %v" , err )
}
if result . Config == nil || result . Config . PendingChanges != 1 {
t . Fatalf ( "pending changes = %#v, want 1" , result . Config )
}
second , err := NewStoreWithOptions ( fixedClock , options )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
beforeApply , err := second . ExternalAuth ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read external auth before apply: %v" , err )
}
if beforeApply . PrimaryMethod != "local" || beforeApply . Enabled {
t . Fatalf ( "external auth before apply = %#v, want disabled local" , beforeApply )
}
exported , err := second . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatalf ( "export config before apply: %v" , err )
}
if exported . Pending . ExternalAuth == nil || ! exported . Pending . ExternalAuth . Enabled || exported . Pending . ExternalAuth . PrimaryMethod != method {
t . Fatalf ( "pending external auth export = %#v, want enabled %q" , exported . Pending . ExternalAuth , method )
}
if len ( exported . Pending . ExternalAuth . RadiusServers ) != 1 || ! exported . Pending . ExternalAuth . RadiusServers [ 0 ] . SecretStored {
t . Fatalf ( "pending radius servers = %#v, want one protected-secret server" , exported . Pending . ExternalAuth . RadiusServers )
}
if _ , err := second . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply config: %v" , err )
}
third , err := NewStoreWithOptions ( fixedClock , options )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
afterApply , err := third . ExternalAuth ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read external auth after apply: %v" , err )
}
if ! afterApply . Enabled || afterApply . PrimaryMethod != method || afterApply . TimeoutSeconds != timeoutSeconds {
t . Fatalf ( "external auth after apply = %#v, want enabled %q timeout %d" , afterApply , method , timeoutSeconds )
}
if ! afterApply . RadiusServers [ 0 ] . SecretStored {
t . Fatal ( "protected radius secret was not preserved across store reloads" )
}
}
func TestCustomGroupAndUserPortPolicyIsEnforced ( t * testing . T ) {
store := NewStore ( fixedClock )
group , err := store . CreateAuthGroup ( context . Background ( ) , domain . AuthGroupCreateRequest { GroupID : "rack-a" , Name : "Rack A" , Role : "viewer" , PortPermissions : [ ] domain . PortPermission { { Channel : "CH01" , Access : "write" } } } )
if err != nil || group . System {
t . Fatalf ( "create group=%#v err=%v" , group , err )
}
user , err := store . CreateAuthUser ( context . Background ( ) , domain . AuthUserCreateRequest { Username : "rack-user" , Password : "rack-user-secret" , Role : "viewer" , Groups : [ ] string { "rack-a" } } )
if err != nil || ! user . MustChangePassword {
t . Fatalf ( "create user=%#v err=%v" , user , err )
}
forceChange := false
if _ , err := store . UpdateAuthUserPassword ( context . Background ( ) , "rack-user" , domain . AuthUserPasswordUpdateRequest { Password : "rack-user-secret-2" , ForceChange : & forceChange } ) ; err != nil {
t . Fatal ( err )
}
session , err := store . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "rack-user" , Password : "rack-user-secret-2" } )
if err != nil {
t . Fatal ( err )
}
actor , err := store . ActorBySession ( context . Background ( ) , session . SessionToken )
if err != nil {
t . Fatal ( err )
}
if err := store . AuthorizePort ( context . Background ( ) , actor , "CH01" , "write" ) ; err != nil {
t . Fatalf ( "CH01 write denied: %v" , err )
}
if err := store . AuthorizePort ( context . Background ( ) , actor , "CH02" , "read" ) ; err == nil {
t . Fatal ( "CH02 read unexpectedly allowed" )
}
}
func TestAddressFiltersEnforceIPv4AndMACRules ( t * testing . T ) {
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { MACResolver : func ( context . Context , netip . Addr ) ( string , error ) { return "00:11:22:33:44:55" , nil } } )
if err != nil {
t . Fatal ( err )
}
settings := domain . AddressFilterSettings { Enabled : true , Mode : "whitelist" , Rules : [ ] domain . AddressFilterRule { { RuleID : "office" , AddressType : "ipv4" , Address : "192.168.10.0/24" , Protocol : "https" , DestinationPort : 443 , Enabled : true } , { RuleID : "service-laptop" , AddressType : "mac" , Address : "00:11:22:33:44:55" , Protocol : "ssh" , DestinationPort : 22 , Enabled : true } } }
if _ , err := store . UpdateAddressFilters ( context . Background ( ) , settings ) ; err != nil {
t . Fatal ( err )
}
if err := store . AuthorizeClient ( context . Background ( ) , "192.168.10.5" , "https" , 443 ) ; err != nil {
t . Fatal ( err )
}
if err := store . AuthorizeClient ( context . Background ( ) , "192.168.20.5" , "ssh" , 22 ) ; err != nil {
t . Fatal ( err )
}
if err := store . AuthorizeClient ( context . Background ( ) , "192.168.20.5" , "https" , 443 ) ; err == nil {
t . Fatal ( "unmatched client unexpectedly allowed" )
}
}
func TestAddressFiltersEnforceIPv6Rules ( t * testing . T ) {
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { } )
if err != nil {
t . Fatal ( err )
}
settings := domain . AddressFilterSettings { Enabled : true , Mode : "whitelist" , Rules : [ ] domain . AddressFilterRule { { RuleID : "office-v6" , AddressType : "ipv6" , Address : "2001:db8:42::/64" , Protocol : "https" , DestinationPort : 443 , Enabled : true } } }
if _ , err := store . UpdateAddressFilters ( context . Background ( ) , settings ) ; err != nil {
t . Fatal ( err )
}
if err := store . AuthorizeClient ( context . Background ( ) , "2001:db8:42::10" , "https" , 443 ) ; err != nil {
t . Fatal ( err )
}
if err := store . AuthorizeClient ( context . Background ( ) , "2001:db8:99::10" , "https" , 443 ) ; err == nil {
t . Fatal ( "unmatched IPv6 client unexpectedly allowed" )
}
}
func TestExternalRadiusLoginUsesProtectedRunningSecretAndLocalFallback ( t * testing . T ) {
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { SecretPath : filepath . Join ( t . TempDir ( ) , "secrets.json" ) , ExternalAuthenticator : acceptingExternalAuthenticator { } } )
if err != nil {
t . Fatal ( err )
}
enabled , method , fallback , role := true , "radius" , true , "port_operator"
_ , err = store . UpdateExternalAuth ( context . Background ( ) , domain . ExternalAuthUpdate { Enabled : & enabled , PrimaryMethod : & method , LocalFallback : & fallback , DefaultRole : & role , RadiusServers : [ ] domain . ExternalAuthServer { { ServerID : "radius-primary" , Host : "radius.local" , Port : 1812 , Secret : "radius-test-secret" , Enabled : true } } } )
if err != nil {
t . Fatal ( err )
}
if _ , err := store . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatal ( err )
}
session , err := store . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "remote-user" , Password : "remote-password" } )
if err != nil {
t . Fatal ( err )
}
if session . Role != "port_operator" || session . Actor != "external-remote-user" {
t . Fatalf ( "external session=%#v" , session )
}
exported , err := store . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
encoded , _ := json . Marshal ( exported )
if strings . Contains ( string ( encoded ) , "radius-test-secret" ) {
t . Fatal ( "config export leaked external shared secret" )
}
if _ , err := store . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "admin" , Password : "mock-admin-secret" } ) ; err != nil {
t . Fatalf ( "local fallback failed: %v" , err )
}
}
func TestExternalAuthenticationCannotDisableLocalRecoveryFallback ( t * testing . T ) {
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { SecretPath : filepath . Join ( t . TempDir ( ) , "secrets.json" ) } )
if err != nil {
t . Fatal ( err )
}
enabled , method , fallback := true , "radius" , false
_ , err = store . UpdateExternalAuth ( context . Background ( ) , domain . ExternalAuthUpdate {
Enabled : & enabled ,
PrimaryMethod : & method ,
LocalFallback : & fallback ,
RadiusServers : [ ] domain . ExternalAuthServer { {
ServerID : "radius-primary" , Host : "radius.local" , Port : 1812 ,
Secret : "radius-test-secret" , Enabled : true ,
} } ,
} )
if err == nil || ! strings . Contains ( err . Error ( ) , "requires local fallback" ) {
t . Fatalf ( "external auth without local fallback error=%v" , err )
}
if store . secrets . Has ( externalAuthSecretKey ( "pending" , "radius" , "radius-primary" ) ) {
t . Fatal ( "rejected external auth request stored an orphaned pending secret" )
}
}
func TestConfigStorePersistsRestorePendingState ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
first , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
snapshot , err := first . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatalf ( "export config: %v" , err )
}
snapshot . Persistent . Ports [ 0 ] . Mode = "reverse_telnet"
snapshot . Persistent . Ports [ 0 ] . Operation1 = defaultPortOperation ( 1 , "reverse_telnet" )
snapshot . Persistent . Ports [ 0 ] . BaudRate = 38400
snapshot . Persistent . Ports [ 0 ] . AlarmKeyword = "RESTORED"
result , err := first . RestoreConfig ( context . Background ( ) , snapshot )
if err != nil {
t . Fatalf ( "restore config: %v" , err )
}
if result . Config == nil || result . Config . PendingChanges == 0 {
t . Fatalf ( "restore config summary = %#v, want pending changes" , result . Config )
}
second , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
beforeApply , err := second . Port ( context . Background ( ) , "CH01" )
if err != nil {
t . Fatalf ( "read port before apply: %v" , err )
}
if beforeApply . Mode != "disabled" {
t . Fatalf ( "running mode before apply = %q, want disabled" , beforeApply . Mode )
}
status , err := second . ConfigStatus ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read config status: %v" , err )
}
if status . PendingChanges == 0 {
t . Fatalf ( "pending changes after reload = 0, want restored pending state" )
}
if _ , err := second . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply restored config: %v" , err )
}
afterApply , err := second . Port ( context . Background ( ) , "CH01" )
if err != nil {
t . Fatalf ( "read port after apply: %v" , err )
}
if afterApply . Mode != "reverse_telnet" || afterApply . BaudRate != 38400 || afterApply . AlarmKeyword != "RESTORED" {
t . Fatalf ( "restored port = %#v, want reverse_telnet 38400 RESTORED" , afterApply )
}
}
func TestRestoreKeepsUnchangedSecretMarkerWithoutCreatingInvalidPendingConfig ( t * testing . T ) {
directory := t . TempDir ( )
configPath := filepath . Join ( directory , "config.json" )
secretPath := filepath . Join ( directory , "secrets.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : configPath , SecretPath : secretPath } )
if err != nil {
t . Fatal ( err )
}
// Model an appliance migrated from an older release: the configuration
// records that an SMTP password was stored, but the protected value is no
// longer present. An unrelated restore must not turn that marker false and
// persist an invalid pending email configuration.
first . mu . Lock ( )
email := first . runningEmail
email . AuthEnabled = true
email . Username = "alerts"
email . PasswordStored = true
first . runningEmail = email
first . persistentEmail = email
if err := first . persistConfigStoreLocked ( ) ; err != nil {
first . mu . Unlock ( )
t . Fatal ( err )
}
first . mu . Unlock ( )
snapshot , err := first . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
snapshot . Persistent . Ports [ 47 ] . AlarmKeyword = "RESTORED-CH48"
if _ , err := first . RestoreConfig ( context . Background ( ) , snapshot ) ; err != nil {
t . Fatalf ( "restore unrelated port setting: %v" , err )
}
first . mu . Lock ( )
if first . pendingEmail != nil {
first . mu . Unlock ( )
t . Fatalf ( "unchanged email configuration was staged: %#v" , first . pendingEmail )
}
first . mu . Unlock ( )
if _ , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : configPath , SecretPath : secretPath } ) ; err != nil {
t . Fatalf ( "reload after restore: %v" , err )
}
}
func TestRestorePreservesChangedSecretDependentConfigWithoutPersistingInvalidState ( t * testing . T ) {
directory := t . TempDir ( )
configPath := filepath . Join ( directory , "config.json" )
secretPath := filepath . Join ( directory , "secrets.json" )
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : configPath , SecretPath : secretPath } )
if err != nil {
t . Fatal ( err )
}
snapshot , err := store . ConfigExport ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
snapshot . Persistent . EmailNotification . AuthEnabled = true
snapshot . Persistent . EmailNotification . Username = "alerts"
snapshot . Persistent . EmailNotification . PasswordStored = true
snapshot . Persistent . EmailNotification . CustomTitle = "restored title"
result , err := store . RestoreConfig ( context . Background ( ) , snapshot )
if err != nil {
t . Fatalf ( "restore missing protected SMTP password: %v" , err )
}
if ! strings . Contains ( result . Message , "email notification" ) {
t . Fatalf ( "restore message=%q, want preserved email warning" , result . Message )
}
store . mu . Lock ( )
if store . pendingEmail != nil {
store . mu . Unlock ( )
t . Fatalf ( "secret-dependent email configuration was staged: %#v" , store . pendingEmail )
}
store . mu . Unlock ( )
if _ , err := NewStoreWithOptions ( fixedClock , StoreOptions { ConfigPath : configPath , SecretPath : secretPath } ) ; err != nil {
t . Fatalf ( "restore corrupted persisted configuration: %v" , err )
}
}
func TestConfigStorePersistsFactoryResetState ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.json" )
first , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
mode := "Reverse Telnet"
if _ , err := first . UpdatePort ( context . Background ( ) , "CH01" , domain . PortUpdate { Mode : & mode } ) ; err != nil {
t . Fatalf ( "stage port update: %v" , err )
}
if _ , err := first . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply port update: %v" , err )
}
changed , err := first . Port ( context . Background ( ) , "CH01" )
if err != nil {
t . Fatalf ( "read changed port: %v" , err )
}
if changed . Mode != "reverse_telnet" {
t . Fatalf ( "changed mode = %q, want reverse_telnet" , changed . Mode )
}
operation , err := first . FactoryReset ( context . Background ( ) )
if err != nil {
t . Fatalf ( "factory reset: %v" , err )
}
if operation . State != "succeeded" || operation . ProgressPercent != 100 {
t . Fatalf ( "operation = %#v, want succeeded progress 100" , operation )
}
second , err := NewStoreWithConfigFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
port , err := second . Port ( context . Background ( ) , "CH01" )
if err != nil {
t . Fatalf ( "read reset port: %v" , err )
}
if port . Mode != "disabled" || port . BaudRate != 9600 || port . Operation1 . Mode != "disabled" || port . Operation2 . Mode != "disabled" {
t . Fatalf ( "reset port = %#v, want default disabled operations and 9600" , port )
}
status , err := second . ConfigStatus ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read config status: %v" , err )
}
if status . PendingChanges != 0 || status . RunningDiffersFromPersistent {
t . Fatalf ( "reset status = %#v, want no pending and no drift" , status )
}
}
func TestOperationStorePersistsOperationsAcrossStoreInstances ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "operations.json" )
first , err := NewStoreWithOperationFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
operation , err := first . RunDiagnostics ( context . Background ( ) , "network-test" , domain . DiagnosticsRequest {
Target : "192.168.0.1" ,
TimeoutSeconds : 10 ,
MaxOutputBytes : 4096 ,
} )
if err != nil {
t . Fatalf ( "run diagnostics: %v" , err )
}
assertStoredOperationMetadata ( t , operation )
assertOperationResult ( t , operation , "kind" , "network_test" )
assertOperationResult ( t , operation , "target" , "192.168.0.1" )
firstList , err := first . Operations ( context . Background ( ) )
if err != nil {
t . Fatalf ( "list first operations: %v" , err )
}
if len ( firstList ) != 1 || firstList [ 0 ] . OperationID != operation . OperationID {
t . Fatalf ( "first operations = %#v, want single operation %q" , firstList , operation . OperationID )
}
assertStoredOperationMetadata ( t , firstList [ 0 ] )
assertOperationResult ( t , firstList [ 0 ] , "kind" , "network_test" )
second , err := NewStoreWithOperationFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
reloaded , err := second . Operation ( context . Background ( ) , operation . OperationID )
if err != nil {
t . Fatalf ( "read reloaded operation: %v" , err )
}
if reloaded . OperationID != operation . OperationID {
t . Fatalf ( "operation id = %q, want %q" , reloaded . OperationID , operation . OperationID )
}
if reloaded . AuditID == "" {
t . Fatalf ( "expected reloaded operation audit id" )
}
assertStoredOperationMetadata ( t , reloaded )
assertOperationResult ( t , reloaded , "kind" , "network_test" )
assertOperationResult ( t , reloaded , "target" , "192.168.0.1" )
reloadedList , err := second . Operations ( context . Background ( ) )
if err != nil {
t . Fatalf ( "list reloaded operations: %v" , err )
}
if len ( reloadedList ) != 1 || reloadedList [ 0 ] . OperationID != operation . OperationID {
t . Fatalf ( "reloaded operations = %#v, want single operation %q" , reloadedList , operation . OperationID )
}
assertStoredOperationMetadata ( t , reloadedList [ 0 ] )
assertOperationResult ( t , reloadedList [ 0 ] , "kind" , "network_test" )
next , err := second . RestartPort ( context . Background ( ) , "CH02" )
if err != nil {
t . Fatalf ( "restart second port: %v" , err )
}
if next . OperationID == operation . OperationID {
t . Fatalf ( "operation id duplicated: %q" , next . OperationID )
}
if next . OperationID != "op_20260618_000002" {
t . Fatalf ( "next operation id = %q, want op_20260618_000002" , next . OperationID )
}
assertStoredOperationMetadata ( t , next )
nextList , err := second . Operations ( context . Background ( ) )
if err != nil {
t . Fatalf ( "list next operations: %v" , err )
}
if len ( nextList ) != 2 || nextList [ 0 ] . OperationID != operation . OperationID || nextList [ 1 ] . OperationID != next . OperationID {
t . Fatalf ( "next operations = %#v, want ordered operation list" , nextList )
}
assertStoredOperationMetadata ( t , nextList [ 1 ] )
}
type blockingDiagnosticsRunner struct {
started chan diagnostics . Request
release chan struct { }
result diagnostics . Result
progress string
err error
}
func ( r * blockingDiagnosticsRunner ) Run ( ctx context . Context , request diagnostics . Request ) ( diagnostics . Result , error ) {
if r . progress != "" {
diagnostics . ReportOutput ( ctx , diagnostics . OutputProgress { Output : r . progress , Bytes : len ( r . progress ) } )
}
select {
case r . started <- request :
default :
}
select {
case <- ctx . Done ( ) :
return diagnostics . Result { } , ctx . Err ( )
case <- r . release :
return r . result , r . err
}
}
func TestAsyncDiagnosticsTransitionsAndAuditsFinalResult ( t * testing . T ) {
reachable := true
runner := & blockingDiagnosticsRunner {
started : make ( chan diagnostics . Request , 1 ) ,
release : make ( chan struct { } ) ,
result : diagnostics . Result {
Kind : diagnostics . KindNetworkTest , Target : "192.0.2.1" , ExitCode : 0 ,
Output : "reachable" , OutputBytes : 9 , Reachable : & reachable ,
} ,
progress : "PING 192.0.2.1\n64 bytes from 192.0.2.1\n" ,
}
store , err := NewStoreWithOptions ( fixedClock , StoreOptions {
DiagnosticsRunner : runner ,
DiagnosticsWorkers : 1 ,
DiagnosticsQueue : 2 ,
} )
if err != nil {
t . Fatal ( err )
}
defer store . Close ( )
ctx := domain . WithActor ( context . Background ( ) , domain . Actor { Name : "mock-admin" } )
operation , err := store . RunDiagnostics ( ctx , "network-test" , domain . DiagnosticsRequest {
Target : "192.0.2.1" , TimeoutSeconds : 5 , MaxOutputBytes : 4096 ,
} )
if err != nil {
t . Fatal ( err )
}
if operation . State != "queued" && operation . State != "running" {
t . Fatalf ( "initial state = %q, want queued or running" , operation . State )
}
select {
case request := <- runner . started :
if request . Target != "192.0.2.1" {
t . Fatalf ( "runner target = %q" , request . Target )
}
case <- time . After ( time . Second ) :
t . Fatal ( "diagnostic runner did not start" )
}
running := waitForOperationState ( t , store , operation . OperationID , "running" )
if running . Result [ "output" ] != runner . progress || running . Result [ "output_bytes" ] != len ( runner . progress ) {
t . Fatalf ( "running result = %+v, want live process output" , running . Result )
}
close ( runner . release )
completed := waitForOperationState ( t , store , operation . OperationID , "succeeded" )
if completed . ProgressPercent != 100 || completed . CompletedAt == nil {
t . Fatalf ( "completed operation = %+v" , completed )
}
if completed . Result [ "adapter" ] != "process" || completed . Result [ "reachable" ] != true || completed . Result [ "output" ] != "reachable" {
t . Fatalf ( "completed result = %+v" , completed . Result )
}
events , err := store . AuditEvents ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
if len ( events ) != 2 || events [ 0 ] . Result != "accepted" || events [ 1 ] . Result != "succeeded" || events [ 1 ] . Actor != "mock-admin" {
t . Fatalf ( "audit events = %+v" , events )
}
}
func TestSupportBundleServiceCreatesAndReadsArchive ( t * testing . T ) {
manager , err := supportbundle . NewManager ( supportbundle . Options { Directory : filepath . Join ( t . TempDir ( ) , "bundles" ) } )
if err != nil {
t . Fatal ( err )
}
store , err := NewStoreWithOptions ( fixedClock , StoreOptions {
SupportBundleService : manager ,
DiagnosticsWorkers : 1 ,
DiagnosticsQueue : 2 ,
} )
if err != nil {
t . Fatal ( err )
}
defer store . Close ( )
ctx := domain . WithActor ( context . Background ( ) , domain . Actor { Name : "mock-admin" } )
operation , err := store . RunDiagnostics ( ctx , "support-bundle" , domain . DiagnosticsRequest { } )
if err != nil {
t . Fatal ( err )
}
if operation . State != "queued" && operation . State != "running" {
t . Fatalf ( "initial state = %q, want queued or running" , operation . State )
}
completed := waitForOperationState ( t , store , operation . OperationID , "succeeded" )
if completed . Result [ "adapter" ] != "support_bundle" || completed . Result [ "download_url" ] == "" || completed . Result [ "sha256" ] == "" {
t . Fatalf ( "support bundle result = %+v" , completed . Result )
}
bundleID , _ := completed . Result [ "bundle_id" ] . ( string )
artifact , err := store . SupportBundle ( ctx , bundleID )
if err != nil {
t . Fatal ( err )
}
if artifact . BundleID != bundleID || len ( artifact . Content ) < 4 || string ( artifact . Content [ : 2 ] ) != "PK" || len ( artifact . SHA256 ) != 64 {
t . Fatalf ( "artifact = %+v" , artifact )
}
events , err := store . AuditEvents ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
if len ( events ) != 3 || events [ 0 ] . Result != "accepted" || events [ 1 ] . Result != "succeeded" || events [ 2 ] . Action != "diagnostics.support_bundle.download" {
t . Fatalf ( "support bundle audit events = %+v" , events )
}
}
func TestAsyncDiagnosticsQueueIsBoundedAndShutdownFinalizesWork ( t * testing . T ) {
runner := & blockingDiagnosticsRunner { started : make ( chan diagnostics . Request , 1 ) , release : make ( chan struct { } ) }
store , err := NewStoreWithOptions ( fixedClock , StoreOptions {
DiagnosticsRunner : runner ,
DiagnosticsWorkers : 1 ,
DiagnosticsQueue : 1 ,
} )
if err != nil {
t . Fatal ( err )
}
first , err := store . RunDiagnostics ( context . Background ( ) , "network-test" , domain . DiagnosticsRequest { Target : "192.0.2.1" } )
if err != nil {
t . Fatal ( err )
}
select {
case <- runner . started :
case <- time . After ( time . Second ) :
t . Fatal ( "first diagnostic did not start" )
}
second , err := store . RunDiagnostics ( context . Background ( ) , "network-test" , domain . DiagnosticsRequest { Target : "192.0.2.2" } )
if err != nil {
t . Fatal ( err )
}
if _ , err := store . RunDiagnostics ( context . Background ( ) , "network-test" , domain . DiagnosticsRequest { Target : "192.0.2.3" } ) ; err == nil || ! strings . Contains ( err . Error ( ) , "queue is full" ) {
t . Fatalf ( "third diagnostic error = %v, want full queue" , err )
}
store . Close ( )
for _ , operationID := range [ ] string { first . OperationID , second . OperationID } {
operation , err := store . Operation ( context . Background ( ) , operationID )
if err != nil {
t . Fatal ( err )
}
if operation . State != "failed" || operation . CompletedAt == nil || ! strings . Contains ( operation . Message , "shutdown" ) {
t . Fatalf ( "shutdown operation = %+v" , operation )
}
}
}
func TestAsyncDiagnosticsRetentionPreservesActiveWork ( t * testing . T ) {
runner := & blockingDiagnosticsRunner { started : make ( chan diagnostics . Request , 1 ) , release : make ( chan struct { } ) }
store , err := NewStoreWithOptions ( fixedClock , StoreOptions {
OperationMaxEvents : 1 ,
DiagnosticsRunner : runner ,
DiagnosticsWorkers : 1 ,
DiagnosticsQueue : 2 ,
} )
if err != nil {
t . Fatal ( err )
}
first , err := store . RunDiagnostics ( context . Background ( ) , "network-test" , domain . DiagnosticsRequest { Target : "192.0.2.1" } )
if err != nil {
t . Fatal ( err )
}
select {
case <- runner . started :
case <- time . After ( time . Second ) :
t . Fatal ( "first diagnostic did not start" )
}
second , err := store . RunDiagnostics ( context . Background ( ) , "network-test" , domain . DiagnosticsRequest { Target : "192.0.2.2" } )
if err != nil {
t . Fatal ( err )
}
operations , err := store . Operations ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
if len ( operations ) != 2 {
t . Fatalf ( "active operations = %d, want 2" , len ( operations ) )
}
for _ , operationID := range [ ] string { first . OperationID , second . OperationID } {
if _ , err := store . Operation ( context . Background ( ) , operationID ) ; err != nil {
t . Fatalf ( "active operation %s was trimmed: %v" , operationID , err )
}
}
store . Close ( )
operations , err = store . Operations ( context . Background ( ) )
if err != nil {
t . Fatal ( err )
}
if len ( operations ) != 1 || operations [ 0 ] . State != "failed" {
t . Fatalf ( "operations after shutdown = %+v, want one retained failed operation" , operations )
}
}
func waitForOperationState ( t * testing . T , store * Store , operationID , state string ) domain . Operation {
t . Helper ( )
deadline := time . Now ( ) . Add ( 2 * time . Second )
for time . Now ( ) . Before ( deadline ) {
operation , err := store . Operation ( context . Background ( ) , operationID )
if err != nil {
t . Fatal ( err )
}
if operation . State == state {
return operation
}
time . Sleep ( 10 * time . Millisecond )
}
operation , _ := store . Operation ( context . Background ( ) , operationID )
t . Fatalf ( "operation state = %q, want %q" , operation . State , state )
return domain . Operation { }
}
func TestOperationResultClonesDoNotShareSlices ( t * testing . T ) {
store := NewStore ( fixedClock )
operation , err := store . RunDiagnostics ( context . Background ( ) , "support-bundle" , domain . DiagnosticsRequest { } )
if err != nil {
t . Fatalf ( "run support bundle: %v" , err )
}
sections , ok := operation . Result [ "included_sections" ] . ( [ ] string )
if ! ok || len ( sections ) == 0 {
t . Fatalf ( "included_sections shape = %T length %d, want non-empty []string" , operation . Result [ "included_sections" ] , len ( sections ) )
}
sections [ 0 ] = "mutated"
reloaded , err := store . Operation ( context . Background ( ) , operation . OperationID )
if err != nil {
t . Fatalf ( "read operation: %v" , err )
}
reloadedSections , ok := reloaded . Result [ "included_sections" ] . ( [ ] string )
if ! ok || len ( reloadedSections ) == 0 {
t . Fatalf ( "reloaded included_sections shape = %T length %d, want non-empty []string" , reloaded . Result [ "included_sections" ] , len ( reloadedSections ) )
}
if reloadedSections [ 0 ] != "system_status" {
t . Fatalf ( "stored included_sections[0] = %q, want system_status" , reloadedSections [ 0 ] )
}
}
func TestOperationStoreRejectsInvalidOperationProgress ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "operations.json" )
first , err := NewStoreWithOperationFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
operation , err := first . RestartPort ( context . Background ( ) , "CH01" )
if err != nil {
t . Fatalf ( "restart port: %v" , err )
}
var snapshot operationSnapshot
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read operation store: %v" , err )
}
if err := json . Unmarshal ( content , & snapshot ) ; err != nil {
t . Fatalf ( "decode operation store: %v" , err )
}
tampered := snapshot . Operations [ operation . OperationID ]
tampered . ProgressPercent = 101
snapshot . Operations [ operation . OperationID ] = tampered
content , err = json . Marshal ( snapshot )
if err != nil {
t . Fatalf ( "encode tampered operation store: %v" , err )
}
if err := os . WriteFile ( path , append ( content , '\n' ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write tampered operation store: %v" , err )
}
if _ , err := NewStoreWithOperationFile ( fixedClock , path ) ; err == nil {
t . Fatalf ( "operation store with invalid progress should be rejected" )
}
}
func TestSaveAndRestartPersistsConfigAndOperationAcrossStoreInstances ( t * testing . T ) {
tempDir := t . TempDir ( )
configPath := filepath . Join ( tempDir , "config.json" )
operationPath := filepath . Join ( tempDir , "operations.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions {
ConfigPath : configPath ,
OperationPath : operationPath ,
} )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
mode := "Reverse Telnet"
if _ , err := first . UpdatePort ( context . Background ( ) , "CH01" , domain . PortUpdate { Mode : & mode } ) ; err != nil {
t . Fatalf ( "stage port update: %v" , err )
}
operation , err := first . SaveAndRestart ( context . Background ( ) )
if err != nil {
t . Fatalf ( "save and restart: %v" , err )
}
assertStoredOperationMetadata ( t , operation )
if ! strings . Contains ( operation . Message , "saving 1 pending change" ) {
t . Fatalf ( "operation message = %q, want pending change count" , operation . Message )
}
second , err := NewStoreWithOptions ( fixedClock , StoreOptions {
ConfigPath : configPath ,
OperationPath : operationPath ,
} )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
reloadedOperation , err := second . Operation ( context . Background ( ) , operation . OperationID )
if err != nil {
t . Fatalf ( "read reloaded operation: %v" , err )
}
if reloadedOperation . OperationID != operation . OperationID || reloadedOperation . AuditID == "" {
t . Fatalf ( "reloaded operation = %#v, want operation %q with audit id" , reloadedOperation , operation . OperationID )
}
assertStoredOperationMetadata ( t , reloadedOperation )
port , err := second . Port ( context . Background ( ) , "CH01" )
if err != nil {
t . Fatalf ( "read reloaded port: %v" , err )
}
if port . Mode != "reverse_telnet" {
t . Fatalf ( "reloaded CH01 mode = %q, want reverse_telnet" , port . Mode )
}
status , err := second . ConfigStatus ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read reloaded config status: %v" , err )
}
if status . PendingChanges != 0 || status . RunningRevision != 2 || status . PersistentRevision != 2 || status . RunningDiffersFromPersistent {
t . Fatalf ( "reloaded config status = %#v, want saved/applied revision 2 with no drift" , status )
}
}
func TestAlarmDeliveryTestOperationPersistsAndValidatesTargets ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "operations.json" )
store , err := NewStoreWithOperationFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "create store: %v" , err )
}
operation , err := store . TestAlarmDelivery ( context . Background ( ) , domain . AlarmDeliveryTestRequest {
Delivery : [ ] string { "web" } ,
Message : "Operator verification" ,
} )
if err != nil {
t . Fatalf ( "test alarm delivery: %v" , err )
}
assertStoredOperationMetadata ( t , operation )
if operation . OperationID != "op_20260618_000001" {
t . Fatalf ( "operation id = %q, want op_20260618_000001" , operation . OperationID )
}
if ! strings . Contains ( operation . Message , "Operator verification" ) || ! strings . Contains ( operation . Message , "web mock delivery" ) {
t . Fatalf ( "operation message = %q, want verification through web mock delivery" , operation . Message )
}
if _ , err := store . TestAlarmDelivery ( context . Background ( ) , domain . AlarmDeliveryTestRequest { Delivery : [ ] string { "email" } } ) ; err == nil {
t . Fatalf ( "email delivery test should require enabled email notification" )
}
reloaded , err := NewStoreWithOperationFile ( fixedClock , path )
if err != nil {
t . Fatalf ( "reload store: %v" , err )
}
operations , err := reloaded . Operations ( context . Background ( ) )
if err != nil {
t . Fatalf ( "list reloaded operations: %v" , err )
}
if len ( operations ) != 1 || operations [ 0 ] . OperationID != operation . OperationID {
t . Fatalf ( "reloaded operations = %#v, want single alarm delivery operation %q" , operations , operation . OperationID )
}
assertStoredOperationMetadata ( t , operations [ 0 ] )
}
func TestAlarmDeliveryTestUsesEnabledRuleDelivery ( t * testing . T ) {
store := NewStore ( fixedClock )
update , err := store . UpdateAlarmRule ( context . Background ( ) , "auth_failures" , domain . AlarmRuleUpdate {
Delivery : [ ] string { "web" } ,
} )
if err != nil {
t . Fatalf ( "stage alarm rule update: %v" , err )
}
if update . Config == nil || update . Config . PendingChanges != 1 {
t . Fatalf ( "pending config = %#v, want one change" , update . Config )
}
if _ , err := store . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatalf ( "apply alarm rule update: %v" , err )
}
operation , err := store . TestAlarmDelivery ( context . Background ( ) , domain . AlarmDeliveryTestRequest {
RuleID : "auth_failures" ,
} )
if err != nil {
t . Fatalf ( "test rule alarm delivery: %v" , err )
}
assertStoredOperationMetadata ( t , operation )
if ! strings . Contains ( operation . Message , "for rule auth_failures" ) || ! strings . Contains ( operation . Message , "web mock delivery" ) {
t . Fatalf ( "operation message = %q, want rule delivery message" , operation . Message )
}
}
func TestAlarmDeliveryTestQueuesThroughDispatcher ( t * testing . T ) {
dispatcher , err := alarmdelivery . NewDispatcher ( map [ string ] alarmdelivery . Transport {
alarmdelivery . ChannelWeb : alarmdelivery . WebTransport { } ,
} , alarmdelivery . DispatcherOptions {
StatePath : filepath . Join ( t . TempDir ( ) , "deliveries.json" ) , PollInterval : 10 * time . Millisecond ,
} )
if err != nil {
t . Fatal ( err )
}
defer dispatcher . Close ( )
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AlarmDeliveryController : dispatcher } )
if err != nil {
t . Fatal ( err )
}
operation , err := store . TestAlarmDelivery ( context . Background ( ) , domain . AlarmDeliveryTestRequest {
Delivery : [ ] string { "web" } , Message : "Operator verification" ,
} )
if err != nil {
t . Fatal ( err )
}
if operation . Result [ "adapter" ] != "tera-alarmd" || operation . Result [ "state" ] != "queued" {
t . Fatalf ( "alarm delivery operation = %+v" , operation )
}
deliveryID , _ := operation . Result [ "delivery_id" ] . ( string )
deadline := time . Now ( ) . Add ( 2 * time . Second )
for time . Now ( ) . Before ( deadline ) {
status , err := dispatcher . Status ( context . Background ( ) , deliveryID )
if err != nil {
t . Fatal ( err )
}
if status . State == "delivered" {
if status . Attempts != 1 {
t . Fatalf ( "delivery status = %+v" , status )
}
return
}
time . Sleep ( 10 * time . Millisecond )
}
t . Fatal ( "alarm delivery did not complete" )
}
func TestOperationStoreCapsOperationHistory ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "operations.json" )
store , err := NewStoreWithOptions ( fixedClock , StoreOptions {
OperationPath : path ,
OperationMaxEvents : 2 ,
} )
if err != nil {
t . Fatalf ( "create store: %v" , err )
}
first , err := store . RestartPort ( context . Background ( ) , "CH01" )
if err != nil {
t . Fatalf ( "first operation: %v" , err )
}
second , err := store . RestartPort ( context . Background ( ) , "CH02" )
if err != nil {
t . Fatalf ( "second operation: %v" , err )
}
third , err := store . RestartPort ( context . Background ( ) , "CH03" )
if err != nil {
t . Fatalf ( "third operation: %v" , err )
}
if _ , err := store . Operation ( context . Background ( ) , first . OperationID ) ; err == nil {
t . Fatalf ( "first operation should have been trimmed" )
}
operations , err := store . Operations ( context . Background ( ) )
if err != nil {
t . Fatalf ( "list operations: %v" , err )
}
if len ( operations ) != 2 || operations [ 0 ] . OperationID != second . OperationID || operations [ 1 ] . OperationID != third . OperationID {
t . Fatalf ( "operations = %#v, want second and third only" , operations )
}
reloaded , err := NewStoreWithOptions ( fixedClock , StoreOptions {
OperationPath : path ,
OperationMaxEvents : 2 ,
} )
if err != nil {
t . Fatalf ( "reload store: %v" , err )
}
reloadedOperations , err := reloaded . Operations ( context . Background ( ) )
if err != nil {
t . Fatalf ( "list reloaded operations: %v" , err )
}
if len ( reloadedOperations ) != 2 || reloadedOperations [ 0 ] . OperationID != second . OperationID || reloadedOperations [ 1 ] . OperationID != third . OperationID {
t . Fatalf ( "reloaded operations = %#v, want second and third only" , reloadedOperations )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read operation store file: %v" , err )
}
text := string ( content )
if strings . Contains ( text , first . OperationID ) {
t . Fatalf ( "operation store file still contains trimmed first operation %q: %s" , first . OperationID , text )
}
if ! strings . Contains ( text , second . OperationID ) || ! strings . Contains ( text , third . OperationID ) {
t . Fatalf ( "operation store file = %s, want second and third operations" , text )
}
next , err := reloaded . RestartPort ( context . Background ( ) , "CH04" )
if err != nil {
t . Fatalf ( "next operation: %v" , err )
}
if next . OperationID != "op_20260618_000004" {
t . Fatalf ( "next operation id = %q, want op_20260618_000004" , next . OperationID )
}
content , err = os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read operation store file after next operation: %v" , err )
}
text = string ( content )
if strings . Contains ( text , second . OperationID ) {
t . Fatalf ( "operation store file still contains trimmed second operation %q: %s" , second . OperationID , text )
}
if ! strings . Contains ( text , third . OperationID ) || ! strings . Contains ( text , next . OperationID ) {
t . Fatalf ( "operation store file = %s, want third and next operations" , text )
}
}
func TestAuthStorePersistsHashesWithoutPlaintextPasswords ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "auth.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
session , err := first . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "admin" ,
Password : "mock-admin-secret" ,
} )
if err != nil {
t . Fatalf ( "login with default admin: %v" , err )
}
if session . SessionToken == "" || session . Actor != "mock-admin" {
t . Fatalf ( "session = %#v, want mock-admin with token" , session )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read auth store: %v" , err )
}
body := string ( content )
if strings . Contains ( body , "mock-admin-secret" ) {
t . Fatalf ( "auth store contains plaintext password" )
}
if ! strings . Contains ( body , ` "password" ` ) || ! strings . Contains ( body , ` "algorithm" ` ) || ! strings . Contains ( body , ` "salt" ` ) || ! strings . Contains ( body , ` "hash" ` ) {
t . Fatalf ( "auth store missing hash metadata: %s" , body )
}
if ! strings . Contains ( body , ` "algorithm": "argon2id" ` ) {
t . Fatalf ( "auth store algorithm metadata = %s, want argon2id" , body )
}
second , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
reloaded , err := second . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "admin" ,
Password : "mock-admin-secret" ,
} )
if err != nil {
t . Fatalf ( "login after reload: %v" , err )
}
if reloaded . Actor != "mock-admin" {
t . Fatalf ( "reloaded actor = %q, want mock-admin" , reloaded . Actor )
}
}
func TestAuthStoreRemovesLegacyPrototypeUsers ( t * testing . T ) {
authPath := filepath . Join ( t . TempDir ( ) , "auth.json" )
sessionPath := filepath . Join ( filepath . Dir ( authPath ) , "sessions.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : authPath , SessionPath : sessionPath } )
if err != nil {
t . Fatalf ( "create legacy store: %v" , err )
}
if _ , err := first . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "viewer" ,
Password : "mock-viewer-secret" ,
} ) ; err != nil {
t . Fatalf ( "create legacy viewer session: %v" , err )
}
first . Close ( )
migrated , err := NewStoreWithOptions ( fixedClock , StoreOptions {
AuthPath : authPath ,
SessionPath : sessionPath ,
RemoveLegacyAuthUsers : true ,
} )
if err != nil {
t . Fatalf ( "migrate legacy users: %v" , err )
}
defer migrated . Close ( )
users , err := migrated . AuthUsers ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read migrated users: %v" , err )
}
if len ( users ) != 1 || users [ 0 ] . Username != "admin" {
t . Fatalf ( "migrated users = %#v, want only admin" , users )
}
if _ , err := migrated . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "viewer" ,
Password : "mock-viewer-secret" ,
} ) ; err == nil {
t . Fatal ( "removed legacy viewer should not authenticate" )
}
sessions , err := migrated . AuthSessions ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read migrated sessions: %v" , err )
}
if len ( sessions ) != 0 {
t . Fatalf ( "legacy sessions were not revoked: %#v" , sessions )
}
content , err := os . ReadFile ( authPath )
if err != nil {
t . Fatalf ( "read migrated auth store: %v" , err )
}
if strings . Contains ( string ( content ) , "mock-viewer" ) || strings . Contains ( string ( content ) , "mock-port-operator" ) {
t . Fatalf ( "migrated auth store still contains legacy users: %s" , content )
}
}
func TestAuthStoreRejectsUserPermissionDrift ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "auth.json" )
if _ , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } ) ; err != nil {
t . Fatalf ( "create store: %v" , err )
}
var snapshot authSnapshot
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read auth store: %v" , err )
}
if err := json . Unmarshal ( content , & snapshot ) ; err != nil {
t . Fatalf ( "decode auth store: %v" , err )
}
viewer := snapshot . Users [ "viewer" ]
viewer . Permissions = append ( viewer . Permissions , "auth.write" )
snapshot . Users [ "viewer" ] = viewer
content , err = json . Marshal ( snapshot )
if err != nil {
t . Fatalf ( "encode tampered auth store: %v" , err )
}
if err := os . WriteFile ( path , append ( content , '\n' ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write tampered auth store: %v" , err )
}
if _ , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } ) ; err == nil {
t . Fatalf ( "auth store with user permission drift should be rejected" )
}
}
func TestAuthStoreMigratesExactLegacyAdministratorPermissions ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "auth.json" )
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create store: %v" , err )
}
store . Close ( )
var snapshot authSnapshot
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read auth store: %v" , err )
}
if err := json . Unmarshal ( content , & snapshot ) ; err != nil {
t . Fatalf ( "decode auth store: %v" , err )
}
admin := snapshot . Users [ "admin" ]
admin . Permissions = legacyAdministratorPermissions ( )
snapshot . Users [ "admin" ] = admin
content , err = json . Marshal ( snapshot )
if err != nil {
t . Fatalf ( "encode legacy auth store: %v" , err )
}
if err := os . WriteFile ( path , append ( content , '\n' ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write legacy auth store: %v" , err )
}
reloaded , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "reload legacy auth store: %v" , err )
}
defer reloaded . Close ( )
users , err := reloaded . AuthUsers ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read migrated users: %v" , err )
}
for _ , user := range users {
if user . Username == "admin" {
for _ , wanted := range [ ] string { "firmware.read" , "firmware.upload" , "firmware.apply" } {
if ! slices . Contains ( user . Permissions , wanted ) {
t . Fatalf ( "migrated admin permissions omit %s: %v" , wanted , user . Permissions )
}
}
return
}
}
t . Fatal ( "migrated admin user not found" )
}
func TestAuthStoreCanUseInjectedPBKDF2PasswordKDF ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "auth.json" )
fipsKDF := password . NewPBKDF2SHA256 ( password . Params { Iterations : 2 , KeyLength : 32 } )
options := StoreOptions {
AuthPath : path ,
PasswordKDF : fipsKDF ,
}
first , err := NewStoreWithOptions ( fixedClock , options )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
session , err := first . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "admin" ,
Password : "mock-admin-secret" ,
} )
if err != nil {
t . Fatalf ( "login with pbkdf2 admin: %v" , err )
}
if session . Actor != "mock-admin" {
t . Fatalf ( "session actor = %q, want mock-admin" , session . Actor )
}
if _ , err := first . CreateAuthUser ( context . Background ( ) , domain . AuthUserCreateRequest {
Username : "fips-user" ,
Password : "fips-user-secret-1" ,
Role : "viewer" ,
} ) ; err != nil {
t . Fatalf ( "create pbkdf2 auth user: %v" , err )
}
if _ , err := first . UpdateAuthUserPassword ( context . Background ( ) , "fips-user" , domain . AuthUserPasswordUpdateRequest { Password : "fips-user-secret-2" } ) ; err != nil {
t . Fatalf ( "update pbkdf2 auth user password: %v" , err )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read auth store: %v" , err )
}
body := string ( content )
if strings . Contains ( body , "mock-admin-secret" ) || strings . Contains ( body , "fips-user-secret-1" ) || strings . Contains ( body , "fips-user-secret-2" ) {
t . Fatalf ( "auth store contains plaintext password" )
}
if ! strings . Contains ( body , ` "algorithm": "pbkdf2-hmac-sha256" ` ) {
t . Fatalf ( "auth store missing pbkdf2 metadata: %s" , body )
}
if strings . Contains ( body , ` "algorithm": "argon2id" ` ) {
t . Fatalf ( "auth store should use injected pbkdf2 KDF for all local password records: %s" , body )
}
second , err := NewStoreWithOptions ( fixedClock , options )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
reloaded , err := second . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "fips-user" ,
Password : "fips-user-secret-2" ,
} )
if err != nil {
t . Fatalf ( "login pbkdf2 user after reload: %v" , err )
}
if reloaded . Actor != "local-fips-user" {
t . Fatalf ( "reloaded actor = %q, want local-fips-user" , reloaded . Actor )
}
if _ , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } ) ; err == nil {
t . Fatalf ( "default KDF registry should reject pbkdf2 auth store until FIPS KDF is configured" )
}
}
func TestAuthSessionCapEvictsOldestSession ( t * testing . T ) {
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthMaxSessions : 1 } )
if err != nil {
t . Fatalf ( "create store: %v" , err )
}
first , err := store . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "admin" ,
Password : "mock-admin-secret" ,
} )
if err != nil {
t . Fatalf ( "first login: %v" , err )
}
second , err := store . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "viewer" ,
Password : "mock-viewer-secret" ,
} )
if err != nil {
t . Fatalf ( "second login: %v" , err )
}
if _ , err := store . ActorBySession ( context . Background ( ) , first . SessionToken ) ; err == nil {
t . Fatalf ( "first session should have been evicted" )
}
actor , err := store . ActorBySession ( context . Background ( ) , second . SessionToken )
if err != nil {
t . Fatalf ( "second session should remain valid: %v" , err )
}
if actor . Name != "mock-viewer" {
t . Fatalf ( "second actor = %q, want mock-viewer" , actor . Name )
}
sessions , err := store . AuthSessions ( context . Background ( ) )
if err != nil {
t . Fatalf ( "list sessions: %v" , err )
}
if len ( sessions ) != 1 || sessions [ 0 ] . Actor != "mock-viewer" {
t . Fatalf ( "sessions = %#v, want only mock-viewer" , sessions )
}
events , err := store . AuditEvents ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read audit events: %v" , err )
}
evictionCount := 0
for _ , event := range events {
if event . Action == "auth.session.evict" {
evictionCount ++
if event . Target != "mock-admin" {
t . Fatalf ( "eviction target = %q, want mock-admin" , event . Target )
}
}
}
if evictionCount != 1 {
t . Fatalf ( "eviction audit count = %d, want 1 in %#v" , evictionCount , events )
}
}
func TestAuthLoginFailureCapTrimsOldestFailureRecords ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "login-failures.json" )
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthMaxLoginFailures : 2 , LoginFailurePath : path } )
if err != nil {
t . Fatalf ( "create store: %v" , err )
}
for _ , username := range [ ] string { "missing-a" , "missing-b" , "missing-c" } {
if _ , err := store . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : username ,
Password : "wrong-password" ,
} ) ; err == nil {
t . Fatalf ( "login for %s should fail" , username )
}
}
store . mu . Lock ( )
defer store . mu . Unlock ( )
if len ( store . loginFailures ) != 2 {
t . Fatalf ( "login failure records = %d, want 2" , len ( store . loginFailures ) )
}
if _ , ok := store . loginFailures [ "missing-a" ] ; ok {
t . Fatalf ( "oldest login failure record should be trimmed" )
}
for _ , username := range [ ] string { "missing-b" , "missing-c" } {
record , ok := store . loginFailures [ username ]
if ! ok {
t . Fatalf ( "login failure record for %s missing" , username )
}
if record . Count != 1 || record . LastAttempt . IsZero ( ) {
t . Fatalf ( "login failure record for %s = %#v, want count and last attempt" , username , record )
}
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read login failure store: %v" , err )
}
text := string ( content )
if strings . Contains ( text , "missing-a" ) {
t . Fatalf ( "login failure store still contains trimmed oldest record: %s" , text )
}
if ! strings . Contains ( text , "missing-b" ) || ! strings . Contains ( text , "missing-c" ) {
t . Fatalf ( "login failure store = %s, want missing-b and missing-c records" , text )
}
if strings . Contains ( text , "wrong-password" ) {
t . Fatalf ( "login failure store contains password material" )
}
}
func TestLoginFailureStorePersistsTemporaryLockoutAcrossStoreInstances ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "login-failures.json" )
now := time . Date ( 2026 , 6 , 18 , 0 , 0 , 0 , 0 , time . UTC )
clock := func ( ) time . Time { return now }
first , err := NewStoreWithOptions ( clock , StoreOptions { LoginFailurePath : path } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
for attempt := 0 ; attempt < failedLoginLockThreshold ; attempt ++ {
if _ , err := first . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "viewer" ,
Password : "wrong-password" ,
} ) ; err == nil {
t . Fatalf ( "failed login attempt %d should fail" , attempt + 1 )
}
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read login failure store: %v" , err )
}
body := string ( content )
if strings . Contains ( body , "wrong-password" ) || strings . Contains ( body , "mock-viewer-secret" ) {
t . Fatalf ( "login failure store contains password material" )
}
if ! strings . Contains ( body , ` "viewer" ` ) || ! strings . Contains ( body , ` "locked_until" ` ) {
t . Fatalf ( "login failure store missing viewer lockout metadata: %s" , body )
}
second , err := NewStoreWithOptions ( clock , StoreOptions { LoginFailurePath : path } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
if _ , err := second . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "viewer" ,
Password : "mock-viewer-secret" ,
} ) ; err == nil {
t . Fatalf ( "persisted lockout should reject correct password before expiry" )
}
now = now . Add ( failedLoginLockDuration + time . Second )
recovered , err := second . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "viewer" ,
Password : "mock-viewer-secret" ,
} )
if err != nil {
t . Fatalf ( "login after lockout expiry: %v" , err )
}
if recovered . Actor != "mock-viewer" {
t . Fatalf ( "recovered actor = %q, want mock-viewer" , recovered . Actor )
}
content , err = os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read login failure store after recovery: %v" , err )
}
if strings . Contains ( string ( content ) , ` "viewer" ` ) {
t . Fatalf ( "expired lockout should be cleared after successful login: %s" , string ( content ) )
}
}
func TestLoginFailureStoreRejectsNonCanonicalUsernameKeys ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "login-failures.json" )
snapshot := loginFailureSnapshot {
Failures : map [ string ] loginFailureRecord {
"Viewer" : {
Count : 1 ,
LastAttempt : fixedClock ( ) ,
} ,
} ,
}
content , err := json . Marshal ( snapshot )
if err != nil {
t . Fatalf ( "encode login failure store: %v" , err )
}
if err := os . WriteFile ( path , append ( content , '\n' ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write login failure store: %v" , err )
}
if _ , err := NewStoreWithOptions ( fixedClock , StoreOptions { LoginFailurePath : path } ) ; err == nil {
t . Fatalf ( "login failure store with non-canonical username should be rejected" )
}
}
func TestLoginFailureStoreClearsFailureAfterSuccessfulLogin ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "login-failures.json" )
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { LoginFailurePath : path } )
if err != nil {
t . Fatalf ( "create store: %v" , err )
}
if _ , err := store . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "admin" ,
Password : "wrong-password" ,
} ) ; err == nil {
t . Fatalf ( "failed admin login should fail" )
}
if _ , err := store . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "admin" ,
Password : "mock-admin-secret" ,
} ) ; err != nil {
t . Fatalf ( "successful admin login: %v" , err )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read login failure store: %v" , err )
}
if strings . Contains ( string ( content ) , ` "admin" ` ) {
t . Fatalf ( "successful login should clear persisted failure: %s" , string ( content ) )
}
}
func TestAuthStorePersistsCreatedUsersWithoutPlaintextPasswords ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "auth.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
created , err := first . CreateAuthUser ( context . Background ( ) , domain . AuthUserCreateRequest {
Username : "FieldOps" ,
Password : "fieldops-secret-1" ,
Role : "port_operator" ,
} )
if err != nil {
t . Fatalf ( "create auth user: %v" , err )
}
if created . Username != "fieldops" || created . Actor != "local-fieldops" || created . Role != "port_operator" {
t . Fatalf ( "created user = %#v, want normalized fieldops port operator" , created )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read auth store: %v" , err )
}
body := string ( content )
if strings . Contains ( body , "fieldops-secret-1" ) {
t . Fatalf ( "auth store contains plaintext password" )
}
if ! strings . Contains ( body , ` "fieldops" ` ) || ! strings . Contains ( body , ` "algorithm": "argon2id" ` ) {
t . Fatalf ( "auth store missing created user hash metadata: %s" , body )
}
second , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
reloaded , err := second . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "fieldops" ,
Password : "fieldops-secret-1" ,
} )
if err != nil {
t . Fatalf ( "login created user after reload: %v" , err )
}
if reloaded . Actor != "local-fieldops" || reloaded . Role != "port_operator" {
t . Fatalf ( "reloaded session = %#v, want local-fieldops port_operator" , reloaded )
}
}
func TestAuthStorePersistsGroupUpdates ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "auth.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
name := "Read only users"
updated , err := first . UpdateAuthGroup ( context . Background ( ) , "viewers" , domain . AuthGroupUpdateRequest { Name : & name } )
if err != nil {
t . Fatalf ( "update auth group: %v" , err )
}
if updated . Name != name {
t . Fatalf ( "updated name = %q, want %q" , updated . Name , name )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read auth store: %v" , err )
}
body := string ( content )
if ! strings . Contains ( body , ` "groups" ` ) || ! strings . Contains ( body , name ) {
t . Fatalf ( "auth store missing group update: %s" , body )
}
second , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
groups , err := second . AuthGroups ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read auth groups: %v" , err )
}
var found domain . AuthGroup
for _ , group := range groups {
if group . GroupID == "viewers" {
found = group
break
}
}
if found . Name != name {
t . Fatalf ( "reloaded viewers name = %q, want %q" , found . Name , name )
}
}
func TestAuthStorePersistsDisabledUsers ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "auth.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
_ , err = first . CreateAuthUser ( context . Background ( ) , domain . AuthUserCreateRequest {
Username : "fieldops" ,
Password : "fieldops-secret-1" ,
Role : "viewer" ,
} )
if err != nil {
t . Fatalf ( "create auth user: %v" , err )
}
session , err := first . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "fieldops" , Password : "fieldops-secret-1" } )
if err != nil {
t . Fatalf ( "login before disable: %v" , err )
}
disabled := false
updated , err := first . UpdateAuthUser ( context . Background ( ) , "fieldops" , domain . AuthUserUpdateRequest { Enabled : & disabled } )
if err != nil {
t . Fatalf ( "disable auth user: %v" , err )
}
if updated . Enabled {
t . Fatalf ( "updated enabled = true, want false" )
}
if _ , err := first . ActorBySession ( context . Background ( ) , session . SessionToken ) ; err == nil {
t . Fatalf ( "disabled user session should be revoked" )
}
second , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
if _ , err := second . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "fieldops" , Password : "fieldops-secret-1" } ) ; err == nil {
t . Fatalf ( "disabled user should not log in after reload" )
}
enabled := true
reenabled , err := second . UpdateAuthUser ( context . Background ( ) , "fieldops" , domain . AuthUserUpdateRequest { Enabled : & enabled } )
if err != nil {
t . Fatalf ( "enable auth user: %v" , err )
}
if ! reenabled . Enabled {
t . Fatalf ( "reenabled enabled = false, want true" )
}
enabledSession , err := second . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "fieldops" , Password : "fieldops-secret-1" } )
if err != nil {
t . Fatalf ( "enabled user should log in: %v" , err )
}
if enabledSession . Actor != "local-fieldops" {
t . Fatalf ( "session actor = %q, want local-fieldops" , enabledSession . Actor )
}
}
func TestAuthStorePersistsRoleUpdatesAndRevokesSessions ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "auth.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
_ , err = first . CreateAuthUser ( context . Background ( ) , domain . AuthUserCreateRequest {
Username : "fieldops" ,
Password : "fieldops-secret-1" ,
Role : "viewer" ,
} )
if err != nil {
t . Fatalf ( "create auth user: %v" , err )
}
session , err := first . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "fieldops" , Password : "fieldops-secret-1" } )
if err != nil {
t . Fatalf ( "login before role update: %v" , err )
}
role := "port_operator"
updated , err := first . UpdateAuthUser ( context . Background ( ) , "fieldops" , domain . AuthUserUpdateRequest { Role : & role } )
if err != nil {
t . Fatalf ( "update auth user role: %v" , err )
}
if updated . Role != "port_operator" {
t . Fatalf ( "updated role = %q, want port_operator" , updated . Role )
}
if _ , err := first . ActorBySession ( context . Background ( ) , session . SessionToken ) ; err == nil {
t . Fatalf ( "role update should revoke existing session" )
}
second , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
reloaded , err := second . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "fieldops" , Password : "fieldops-secret-1" } )
if err != nil {
t . Fatalf ( "login after role update reload: %v" , err )
}
if reloaded . Role != "port_operator" {
t . Fatalf ( "reloaded role = %q, want port_operator" , reloaded . Role )
}
}
func TestAuthStorePersistsPasswordUpdatesWithoutPlaintext ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "auth.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
_ , err = first . CreateAuthUser ( context . Background ( ) , domain . AuthUserCreateRequest {
Username : "fieldops" ,
Password : "fieldops-secret-1" ,
Role : "viewer" ,
} )
if err != nil {
t . Fatalf ( "create auth user: %v" , err )
}
oldSession , err := first . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "fieldops" , Password : "fieldops-secret-1" } )
if err != nil {
t . Fatalf ( "login before password update: %v" , err )
}
_ , err = first . UpdateAuthUserPassword ( context . Background ( ) , "fieldops" , domain . AuthUserPasswordUpdateRequest { Password : "fieldops-secret-2" } )
if err != nil {
t . Fatalf ( "update auth password: %v" , err )
}
if _ , err := first . ActorBySession ( context . Background ( ) , oldSession . SessionToken ) ; err == nil {
t . Fatalf ( "password reset should revoke existing session" )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read auth store: %v" , err )
}
body := string ( content )
if strings . Contains ( body , "fieldops-secret-1" ) || strings . Contains ( body , "fieldops-secret-2" ) {
t . Fatalf ( "auth store contains plaintext password" )
}
if ! strings . Contains ( body , ` "algorithm": "argon2id" ` ) {
t . Fatalf ( "auth store missing argon2id metadata: %s" , body )
}
second , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
if _ , err := second . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "fieldops" , Password : "fieldops-secret-1" } ) ; err == nil {
t . Fatalf ( "old password should not log in after reload" )
}
session , err := second . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "fieldops" , Password : "fieldops-secret-2" } )
if err != nil {
t . Fatalf ( "new password should log in after reload: %v" , err )
}
if session . Actor != "local-fieldops" {
t . Fatalf ( "session actor = %q, want local-fieldops" , session . Actor )
}
}
func TestAuthStorePersistsDeletedUsers ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "auth.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
_ , err = first . CreateAuthUser ( context . Background ( ) , domain . AuthUserCreateRequest {
Username : "fieldops" ,
Password : "fieldops-secret-1" ,
Role : "viewer" ,
} )
if err != nil {
t . Fatalf ( "create auth user: %v" , err )
}
session , err := first . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "fieldops" , Password : "fieldops-secret-1" } )
if err != nil {
t . Fatalf ( "login before delete: %v" , err )
}
result , err := first . DeleteAuthUser ( context . Background ( ) , "fieldops" )
if err != nil {
t . Fatalf ( "delete auth user: %v" , err )
}
if result . Result != "auth_user_deleted" || result . AuditID == "" {
t . Fatalf ( "delete result = %#v, want auth_user_deleted with audit id" , result )
}
if _ , err := first . ActorBySession ( context . Background ( ) , session . SessionToken ) ; err == nil {
t . Fatalf ( "deleted user session should be revoked" )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read auth store: %v" , err )
}
if strings . Contains ( string ( content ) , "fieldops" ) {
t . Fatalf ( "auth store still contains deleted user: %s" , string ( content ) )
}
second , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
if _ , err := second . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "fieldops" , Password : "fieldops-secret-1" } ) ; err == nil {
t . Fatalf ( "deleted user should not log in after reload" )
}
users , err := second . AuthUsers ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read auth users: %v" , err )
}
for _ , user := range users {
if user . Username == "fieldops" {
t . Fatalf ( "deleted user still listed: %#v" , users )
}
}
}
func TestAuthStoreRejectsDeletingLastEnabledAdministrator ( t * testing . T ) {
store := NewStore ( fixedClock )
if _ , err := store . DeleteAuthUser ( context . Background ( ) , "admin" ) ; err == nil {
t . Fatalf ( "expected last administrator delete to fail" )
}
_ , err := store . CreateAuthUser ( context . Background ( ) , domain . AuthUserCreateRequest {
Username : "admin2" ,
Password : "admin2-secret-1" ,
Role : "administrator" ,
} )
if err != nil {
t . Fatalf ( "create second admin: %v" , err )
}
if _ , err := store . DeleteAuthUser ( context . Background ( ) , "admin" ) ; err != nil {
t . Fatalf ( "delete admin with second enabled administrator: %v" , err )
}
}
func TestSessionTokensAreHashedAndPersistentUntilLogout ( t * testing . T ) {
now := time . Date ( 2026 , 6 , 18 , 0 , 0 , 0 , 0 , time . UTC )
store := NewStore ( func ( ) time . Time { return now } )
session , err := store . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "admin" ,
Password : "mock-admin-secret" ,
} )
if err != nil {
t . Fatalf ( "login: %v" , err )
}
if session . SessionToken == "" {
t . Fatalf ( "expected session token" )
}
if _ , ok := store . sessions [ session . SessionToken ] ; ok {
t . Fatalf ( "session map stores raw token" )
}
if _ , err := store . ActorBySession ( context . Background ( ) , session . SessionToken ) ; err != nil {
t . Fatalf ( "session should authenticate before expiry: %v" , err )
}
if err := store . Logout ( context . Background ( ) , session . SessionToken ) ; err != nil {
t . Fatalf ( "logout: %v" , err )
}
if _ , err := store . ActorBySession ( context . Background ( ) , session . SessionToken ) ; err == nil {
t . Fatalf ( "revoked session should not authenticate" )
}
persistent , err := store . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "admin" ,
Password : "mock-admin-secret" ,
} )
if err != nil {
t . Fatalf ( "second login: %v" , err )
}
if ! persistent . Persistent {
t . Fatalf ( "new login session must be persistent" )
}
now = persistent . ExpiresAt . Add ( 24 * time . Hour )
if _ , err := store . ActorBySession ( context . Background ( ) , persistent . SessionToken ) ; err != nil {
t . Fatalf ( "persistent session should authenticate after legacy expiry metadata: %v" , err )
}
if err := store . Logout ( context . Background ( ) , persistent . SessionToken ) ; err != nil {
t . Fatalf ( "persistent logout: %v" , err )
}
if _ , err := store . ActorBySession ( context . Background ( ) , persistent . SessionToken ) ; err == nil {
t . Fatalf ( "logged out persistent session should not authenticate" )
}
}
func TestSessionStorePersistsHashedSessionsAcrossStoreInstances ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "sessions.json" )
now := time . Date ( 2026 , 6 , 18 , 0 , 0 , 0 , 0 , time . UTC )
clock := func ( ) time . Time { return now }
first , err := NewStoreWithOptions ( clock , StoreOptions { SessionPath : path } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
session , err := first . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "admin" ,
Password : "mock-admin-secret" ,
} )
if err != nil {
t . Fatalf ( "login: %v" , err )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read session store: %v" , err )
}
if strings . Contains ( string ( content ) , session . SessionToken ) {
t . Fatalf ( "session store contains raw bearer token" )
}
if ! strings . Contains ( string ( content ) , sessionTokenHash ( session . SessionToken ) ) {
t . Fatalf ( "session store missing token hash: %s" , string ( content ) )
}
now = session . ExpiresAt . Add ( 24 * time . Hour )
second , err := NewStoreWithOptions ( clock , StoreOptions { SessionPath : path } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
actor , err := second . ActorBySession ( context . Background ( ) , session . SessionToken )
if err != nil {
t . Fatalf ( "persisted session should authenticate after reload: %v" , err )
}
if actor . Name != "mock-admin" {
t . Fatalf ( "actor = %q, want mock-admin" , actor . Name )
}
if err := second . Logout ( context . Background ( ) , session . SessionToken ) ; err != nil {
t . Fatalf ( "logout persisted session: %v" , err )
}
third , err := NewStoreWithOptions ( clock , StoreOptions { SessionPath : path } )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
if _ , err := third . ActorBySession ( context . Background ( ) , session . SessionToken ) ; err == nil {
t . Fatalf ( "logged out session should not authenticate after reload" )
}
}
func TestSessionStoreRejectsActorPermissionDrift ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "sessions.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { SessionPath : path } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
session , err := first . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "viewer" ,
Password : "mock-viewer-secret" ,
} )
if err != nil {
t . Fatalf ( "login: %v" , err )
}
var snapshot sessionSnapshot
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read session store: %v" , err )
}
if err := json . Unmarshal ( content , & snapshot ) ; err != nil {
t . Fatalf ( "decode session store: %v" , err )
}
tokenHash := sessionTokenHash ( session . SessionToken )
record := snapshot . Sessions [ tokenHash ]
record . Actor . Permissions [ "auth.write" ] = true
snapshot . Sessions [ tokenHash ] = record
content , err = json . Marshal ( snapshot )
if err != nil {
t . Fatalf ( "encode tampered session store: %v" , err )
}
if err := os . WriteFile ( path , append ( content , '\n' ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write tampered session store: %v" , err )
}
if _ , err := NewStoreWithOptions ( fixedClock , StoreOptions { SessionPath : path } ) ; err == nil {
t . Fatalf ( "session store with actor permission drift should be rejected" )
}
}
func TestSessionSequenceContinuesAfterReload ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "sessions.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { SessionPath : path } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
firstSession , err := first . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "admin" ,
Password : "mock-admin-secret" ,
} )
if err != nil {
t . Fatalf ( "first login: %v" , err )
}
firstSessions , err := first . AuthSessions ( context . Background ( ) )
if err != nil {
t . Fatalf ( "list first sessions: %v" , err )
}
if len ( firstSessions ) != 1 || firstSessions [ 0 ] . SessionID != "session_000001" {
t . Fatalf ( "first sessions = %#v, want session_000001" , firstSessions )
}
second , err := NewStoreWithOptions ( fixedClock , StoreOptions { SessionPath : path } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
secondSession , err := second . Login ( context . Background ( ) , domain . AuthLoginRequest {
Username : "viewer" ,
Password : "mock-viewer-secret" ,
} )
if err != nil {
t . Fatalf ( "second login: %v" , err )
}
sessions , err := second . AuthSessions ( context . Background ( ) )
if err != nil {
t . Fatalf ( "list second sessions: %v" , err )
}
if len ( sessions ) != 2 || sessions [ 0 ] . SessionID != "session_000001" || sessions [ 1 ] . SessionID != "session_000002" {
t . Fatalf ( "sessions = %#v, want session_000001 and session_000002" , sessions )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read session store: %v" , err )
}
text := string ( content )
if strings . Contains ( text , firstSession . SessionToken ) || strings . Contains ( text , secondSession . SessionToken ) {
t . Fatalf ( "session store contains raw bearer token material" )
}
if ! strings . Contains ( text , sessionTokenHash ( firstSession . SessionToken ) ) || ! strings . Contains ( text , sessionTokenHash ( secondSession . SessionToken ) ) {
t . Fatalf ( "session store = %s, want both token hashes" , text )
}
if ! strings . Contains ( text , ` "sequence": 2 ` ) {
t . Fatalf ( "session store = %s, want persisted session sequence 2" , text )
}
}
func TestAuthTokenStorePersistsHashedTokensAcrossStoreInstances ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "auth.json" )
now := time . Date ( 2026 , 6 , 18 , 0 , 0 , 0 , 0 , time . UTC )
clock := func ( ) time . Time { return now }
first , err := NewStoreWithOptions ( clock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
expiresAt := now . Add ( 24 * time . Hour )
created , err := first . CreateAuthToken ( context . Background ( ) , domain . AuthTokenCreateRequest {
Name : "integration smoke" ,
Username : "admin" ,
ExpiresAt : & expiresAt ,
} )
if err != nil {
t . Fatalf ( "create auth token: %v" , err )
}
if created . TokenID != "token_000001" || created . Token == "" || created . Actor != "mock-admin" {
t . Fatalf ( "created token = %#v, want first mock-admin token with raw secret" , created )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read auth store: %v" , err )
}
text := string ( content )
if strings . Contains ( text , created . Token ) {
t . Fatalf ( "auth store contains raw api token" )
}
if ! strings . Contains ( text , sessionTokenHash ( created . Token ) ) {
t . Fatalf ( "auth store missing api token hash: %s" , text )
}
second , err := NewStoreWithOptions ( clock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
actor , err := second . ActorBySession ( context . Background ( ) , created . Token )
if err != nil {
t . Fatalf ( "persisted api token should authenticate after reload: %v" , err )
}
if actor . Name != "mock-admin" || ! actor . Permissions [ "auth.write" ] {
t . Fatalf ( "token actor = %#v, want mock-admin with auth.write" , actor )
}
tokens , err := second . AuthTokens ( context . Background ( ) )
if err != nil {
t . Fatalf ( "list api tokens: %v" , err )
}
if len ( tokens ) != 1 || tokens [ 0 ] . TokenID != created . TokenID || tokens [ 0 ] . Name != "integration smoke" || tokens [ 0 ] . LastUsedAt == nil {
t . Fatalf ( "tokens = %#v, want single listed token with last_used_at" , tokens )
}
if _ , err := second . RevokeAuthToken ( context . Background ( ) , created . TokenID ) ; err != nil {
t . Fatalf ( "revoke api token: %v" , err )
}
third , err := NewStoreWithOptions ( clock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
if _ , err := third . ActorBySession ( context . Background ( ) , created . Token ) ; err == nil {
t . Fatalf ( "revoked api token should not authenticate after reload" )
}
}
func TestAuthTokenStoreRejectsInvalidTokenHashKeys ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "auth.json" )
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create store: %v" , err )
}
created , err := store . CreateAuthToken ( context . Background ( ) , domain . AuthTokenCreateRequest {
Name : "automation" ,
Username : "admin" ,
} )
if err != nil {
t . Fatalf ( "create auth token: %v" , err )
}
var snapshot authSnapshot
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read auth store: %v" , err )
}
if err := json . Unmarshal ( content , & snapshot ) ; err != nil {
t . Fatalf ( "decode auth store: %v" , err )
}
if len ( snapshot . Tokens ) != 1 {
t . Fatalf ( "token count = %d, want 1" , len ( snapshot . Tokens ) )
}
for tokenHash , token := range snapshot . Tokens {
delete ( snapshot . Tokens , tokenHash )
snapshot . Tokens [ created . Token ] = token
break
}
content , err = json . Marshal ( snapshot )
if err != nil {
t . Fatalf ( "encode tampered auth store: %v" , err )
}
if err := os . WriteFile ( path , append ( content , '\n' ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write tampered auth store: %v" , err )
}
if _ , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } ) ; err == nil {
t . Fatalf ( "auth store with raw token key should be rejected" )
}
}
func TestAuthTokenStoreRejectsTokenPermissionDrift ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "auth.json" )
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create store: %v" , err )
}
created , err := store . CreateAuthToken ( context . Background ( ) , domain . AuthTokenCreateRequest {
Name : "viewer automation" ,
Username : "viewer" ,
} )
if err != nil {
t . Fatalf ( "create auth token: %v" , err )
}
var snapshot authSnapshot
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read auth store: %v" , err )
}
if err := json . Unmarshal ( content , & snapshot ) ; err != nil {
t . Fatalf ( "decode auth store: %v" , err )
}
for tokenHash , token := range snapshot . Tokens {
if token . TokenID != created . TokenID {
continue
}
token . Permissions = append ( token . Permissions , "auth.write" )
snapshot . Tokens [ tokenHash ] = token
break
}
content , err = json . Marshal ( snapshot )
if err != nil {
t . Fatalf ( "encode tampered auth store: %v" , err )
}
if err := os . WriteFile ( path , append ( content , '\n' ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write tampered auth store: %v" , err )
}
if _ , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } ) ; err == nil {
t . Fatalf ( "auth store with token permission drift should be rejected" )
}
}
func TestAuthTokenSequenceContinuesAfterReload ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "auth.json" )
first , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
firstToken , err := first . CreateAuthToken ( context . Background ( ) , domain . AuthTokenCreateRequest {
Name : "first automation" ,
Username : "admin" ,
} )
if err != nil {
t . Fatalf ( "create first auth token: %v" , err )
}
if firstToken . TokenID != "token_000001" {
t . Fatalf ( "first token id = %q, want token_000001" , firstToken . TokenID )
}
second , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
secondToken , err := second . CreateAuthToken ( context . Background ( ) , domain . AuthTokenCreateRequest {
Name : "second automation" ,
Username : "admin" ,
} )
if err != nil {
t . Fatalf ( "create second auth token: %v" , err )
}
if secondToken . TokenID != "token_000002" {
t . Fatalf ( "second token id = %q, want token_000002" , secondToken . TokenID )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read auth store: %v" , err )
}
text := string ( content )
if strings . Contains ( text , firstToken . Token ) || strings . Contains ( text , secondToken . Token ) {
t . Fatalf ( "auth store contains raw api token material" )
}
if ! strings . Contains ( text , sessionTokenHash ( firstToken . Token ) ) || ! strings . Contains ( text , sessionTokenHash ( secondToken . Token ) ) {
t . Fatalf ( "auth store = %s, want both token hashes" , text )
}
if ! strings . Contains ( text , ` "token_sequence": 2 ` ) {
t . Fatalf ( "auth store = %s, want persisted token sequence 2" , text )
}
}
func TestAuthTokenRevokedOnUserSecurityChanges ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "auth.json" )
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthPath : path } )
if err != nil {
t . Fatalf ( "create store: %v" , err )
}
if _ , err := store . CreateAuthUser ( context . Background ( ) , domain . AuthUserCreateRequest {
Username : "fieldops" ,
Password : "fieldops-secret-1" ,
Role : "viewer" ,
} ) ; err != nil {
t . Fatalf ( "create auth user: %v" , err )
}
token , err := store . CreateAuthToken ( context . Background ( ) , domain . AuthTokenCreateRequest {
Name : "field automation" ,
Username : "fieldops" ,
} )
if err != nil {
t . Fatalf ( "create auth token: %v" , err )
}
if _ , err := store . ActorBySession ( context . Background ( ) , token . Token ) ; err != nil {
t . Fatalf ( "api token should authenticate before user change: %v" , err )
}
enabled := false
if _ , err := store . UpdateAuthUser ( context . Background ( ) , "fieldops" , domain . AuthUserUpdateRequest { Enabled : & enabled } ) ; err != nil {
t . Fatalf ( "disable auth user: %v" , err )
}
if _ , err := store . ActorBySession ( context . Background ( ) , token . Token ) ; err == nil {
t . Fatalf ( "disabled user api token should be revoked" )
}
}
func TestAuthTokenStoreRejectsTokenCap ( t * testing . T ) {
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AuthMaxTokens : 1 } )
if err != nil {
t . Fatalf ( "create store: %v" , err )
}
if _ , err := store . CreateAuthToken ( context . Background ( ) , domain . AuthTokenCreateRequest { Name : "first" , Username : "admin" } ) ; err != nil {
t . Fatalf ( "create first auth token: %v" , err )
}
if _ , err := store . CreateAuthToken ( context . Background ( ) , domain . AuthTokenCreateRequest { Name : "second" , Username : "admin" } ) ; err == nil {
t . Fatalf ( "second auth token should be rejected at cap" )
}
}
func TestAuthTokenExpiryValidationAndPruning ( t * testing . T ) {
now := time . Date ( 2026 , 6 , 18 , 0 , 0 , 0 , 0 , time . UTC )
store := NewStore ( func ( ) time . Time { return now } )
past := now . Add ( - time . Second )
if _ , err := store . CreateAuthToken ( context . Background ( ) , domain . AuthTokenCreateRequest { Name : "past" , Username : "admin" , ExpiresAt : & past } ) ; err == nil {
t . Fatalf ( "past token expiry should be rejected" )
}
expiresAt := now . Add ( time . Hour )
created , err := store . CreateAuthToken ( context . Background ( ) , domain . AuthTokenCreateRequest { Name : "short lived" , Username : "admin" , ExpiresAt : & expiresAt } )
if err != nil {
t . Fatalf ( "create expiring auth token: %v" , err )
}
now = expiresAt . Add ( time . Second )
if _ , err := store . ActorBySession ( context . Background ( ) , created . Token ) ; err == nil {
t . Fatalf ( "expired api token should not authenticate" )
}
tokens , err := store . AuthTokens ( context . Background ( ) )
if err != nil {
t . Fatalf ( "list tokens after expiry: %v" , err )
}
if len ( tokens ) != 0 {
t . Fatalf ( "tokens after expiry = %#v, want empty" , tokens )
}
}
func TestSessionStorePersistsRevocationsAndExpiry ( t * testing . T ) {
sessionPath := filepath . Join ( t . TempDir ( ) , "sessions.json" )
authPath := filepath . Join ( t . TempDir ( ) , "auth.json" )
now := time . Date ( 2026 , 6 , 18 , 0 , 0 , 0 , 0 , time . UTC )
clock := func ( ) time . Time { return now }
first , err := NewStoreWithOptions ( clock , StoreOptions { AuthPath : authPath , SessionPath : sessionPath } )
if err != nil {
t . Fatalf ( "create first store: %v" , err )
}
if _ , err := first . CreateAuthUser ( context . Background ( ) , domain . AuthUserCreateRequest {
Username : "fieldops" ,
Password : "fieldops-secret-1" ,
Role : "viewer" ,
} ) ; err != nil {
t . Fatalf ( "create auth user: %v" , err )
}
session , err := first . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "fieldops" , Password : "fieldops-secret-1" } )
if err != nil {
t . Fatalf ( "login before disable: %v" , err )
}
enabled := false
if _ , err := first . UpdateAuthUser ( context . Background ( ) , "fieldops" , domain . AuthUserUpdateRequest { Enabled : & enabled } ) ; err != nil {
t . Fatalf ( "disable auth user: %v" , err )
}
second , err := NewStoreWithOptions ( clock , StoreOptions { AuthPath : authPath , SessionPath : sessionPath } )
if err != nil {
t . Fatalf ( "create second store: %v" , err )
}
if _ , err := second . ActorBySession ( context . Background ( ) , session . SessionToken ) ; err == nil {
t . Fatalf ( "disabled user session should remain revoked after reload" )
}
adminSession , err := second . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "admin" , Password : "mock-admin-secret" } )
if err != nil {
t . Fatalf ( "admin login: %v" , err )
}
legacyHash := sessionTokenHash ( adminSession . SessionToken )
legacySession := second . sessions [ legacyHash ]
legacySession . Persistent = false
second . sessions [ legacyHash ] = legacySession
if err := second . persistSessionStoreLocked ( ) ; err != nil {
t . Fatalf ( "persist legacy expiring session: %v" , err )
}
now = adminSession . ExpiresAt . Add ( time . Second )
if _ , err := second . ActorBySession ( context . Background ( ) , adminSession . SessionToken ) ; err == nil {
t . Fatalf ( "expired session should not authenticate" )
}
third , err := NewStoreWithOptions ( clock , StoreOptions { SessionPath : sessionPath } )
if err != nil {
t . Fatalf ( "create third store: %v" , err )
}
sessions , err := third . AuthSessions ( context . Background ( ) )
if err != nil {
t . Fatalf ( "read sessions after expiry reload: %v" , err )
}
if len ( sessions ) != 0 {
t . Fatalf ( "expired sessions after reload = %#v, want empty" , sessions )
}
}
func TestLogRetentionRejectsMoreThanOneYear ( t * testing . T ) {
store := NewStore ( fixedClock )
days := 366
if _ , err := store . UpdateLogRetention ( context . Background ( ) , domain . LogRetentionUpdate { PortDays : & days } ) ; err == nil || ! strings . Contains ( err . Error ( ) , "between 1 and 365" ) {
t . Fatalf ( "UpdateLogRetention(366) error = %v, want one-year maximum" , err )
}
}
func TestPortCommandLogPersistsRedactedCommandResultAndKeywordMatch ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "port.jsonl" )
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { PortLogPath : path } )
if err != nil {
t . Fatal ( err )
}
ctx := domain . WithActor ( context . Background ( ) , domain . Actor { Name : "operator" } )
recorded , err := store . RecordPortLog ( ctx , domain . PortLogEvent {
Channel : "CH01" , EventType : "command" , Command : "show LOGIN FAILED password=hunter2" ,
ReturnValue : "token=abc123 result=5" , Message : "command completed" ,
} )
if err != nil {
t . Fatal ( err )
}
if recorded . Actor != "operator" || ! recorded . Redacted || ! recorded . AlarmTriggered || strings . Join ( recorded . KeywordMatches , "," ) != "port_ch01_keyword" {
t . Fatalf ( "recorded event = %+v" , recorded )
}
content , err := os . ReadFile ( path )
if err != nil {
t . Fatal ( err )
}
if strings . Contains ( string ( content ) , "hunter2" ) || strings . Contains ( string ( content ) , "abc123" ) {
t . Fatalf ( "persistent port log leaked credential: %s" , content )
}
reloaded , err := NewStoreWithOptions ( fixedClock , StoreOptions { PortLogPath : path } )
if err != nil {
t . Fatal ( err )
}
events , err := reloaded . PortLogEvents ( context . Background ( ) , domain . PortLogFilter { EventType : "command" , Keyword : "LOGIN FAILED" } )
if err != nil || len ( events ) != 1 || events [ 0 ] . ReturnValue == "" {
t . Fatalf ( "reloaded events = %+v, err=%v" , events , err )
}
}
type recordingAlarmDeliveryController struct {
requests [ ] alarmdelivery . Request
}
func ( controller * recordingAlarmDeliveryController ) Deliver ( _ context . Context , request alarmdelivery . Request ) ( alarmdelivery . Result , error ) {
controller . requests = append ( controller . requests , request )
return alarmdelivery . Result { DeliveryID : fmt . Sprintf ( "delivery-%d" , len ( controller . requests ) ) , State : "delivered" } , nil
}
func ( * recordingAlarmDeliveryController ) Status ( context . Context , string ) ( alarmdelivery . Result , error ) {
return alarmdelivery . Result { } , nil
}
func TestAuthenticationFailureThresholdDispatchesConfiguredAlarmOnce ( t * testing . T ) {
controller := & recordingAlarmDeliveryController { }
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AlarmDeliveryController : controller } )
if err != nil {
t . Fatal ( err )
}
for attempt := 1 ; attempt <= failedLoginLockThreshold ; attempt ++ {
if _ , err := store . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "unknown-user" , Password : "wrong-password" } ) ; err == nil {
t . Fatalf ( "login attempt %d unexpectedly succeeded" , attempt )
}
}
if len ( controller . requests ) != 1 || controller . requests [ 0 ] . RuleID != "auth_failures" || ! slices . Contains ( controller . requests [ 0 ] . Channels , alarmdelivery . ChannelWeb ) {
t . Fatalf ( "authentication failure alarm requests = %+v, want one auth_failures web alarm" , controller . requests )
}
if _ , err := store . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "unknown-user" , Password : "wrong-password" } ) ; err == nil {
t . Fatal ( "locked login unexpectedly succeeded" )
}
if len ( controller . requests ) != 1 {
t . Fatalf ( "locked retries emitted duplicate threshold alarms: %+v" , controller . requests )
}
}
func TestAuthenticationFailureThresholdUsesAppliedManagementPolicy ( t * testing . T ) {
controller := & recordingAlarmDeliveryController { }
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AlarmDeliveryController : controller } )
if err != nil {
t . Fatal ( err )
}
detectionMinutes , securityHits := 7 , 3
if _ , err := store . UpdateManagementAccess ( context . Background ( ) , domain . ManagementAccessUpdate {
LoginDetectionMinutes : & detectionMinutes ,
LoginSecurityHitCount : & securityHits ,
} ) ; err != nil {
t . Fatal ( err )
}
if _ , err := store . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatal ( err )
}
for attempt := 1 ; attempt <= securityHits ; attempt ++ {
if _ , err := store . Login ( context . Background ( ) , domain . AuthLoginRequest { Username : "policy-user" , Password : "wrong-password" } ) ; err == nil {
t . Fatalf ( "login attempt %d unexpectedly succeeded" , attempt )
}
}
if len ( controller . requests ) != 1 || controller . requests [ 0 ] . RuleID != "auth_failures" {
t . Fatalf ( "authentication alarm requests = %+v, want one configured-threshold alarm" , controller . requests )
}
failure := store . loginFailures [ "policy-user" ]
if failure . Count != securityHits || ! failure . LockedUntil . Equal ( fixedClock ( ) . Add ( 7 * time . Minute ) ) {
t . Fatalf ( "login protection record = %+v" , failure )
}
}
func TestPrimaryEthernetDownDispatchesFailoverAndLinkAlarms ( t * testing . T ) {
controller := & recordingAlarmDeliveryController { }
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AlarmDeliveryController : controller } )
if err != nil {
t . Fatal ( err )
}
start := fixedClock ( )
if _ , err := store . RecordLinkState ( context . Background ( ) , domain . LinkStateObservation { Kind : "ethernet" , Resource : "Interface-1" , State : "up" , Signal : "carrier" , ObservedAt : start } ) ; err != nil {
t . Fatal ( err )
}
event , err := store . RecordLinkState ( context . Background ( ) , domain . LinkStateObservation { Kind : "ethernet" , Resource : "Interface-1" , State : "down" , Signal : "carrier" , ObservedAt : start . Add ( time . Minute ) } )
if err != nil {
t . Fatal ( err )
}
if event == nil || strings . Join ( event . MatchedRuleIDs , "," ) != "ethernet_link_down,link_failover" {
t . Fatalf ( "primary link down event = %+v, want both ethernet and failover rules" , event )
}
ruleIDs := make ( [ ] string , 0 , len ( controller . requests ) )
for _ , request := range controller . requests {
ruleIDs = append ( ruleIDs , request . RuleID )
}
slices . Sort ( ruleIDs )
if strings . Join ( ruleIDs , "," ) != "ethernet_link_down,link_failover" {
t . Fatalf ( "primary link delivery rules = %v" , ruleIDs )
}
}
func TestPortAlarmKeywordStagesWithPortAndMatchesReceivedPayload ( t * testing . T ) {
controller := & recordingAlarmDeliveryController { }
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AlarmDeliveryController : controller } )
if err != nil {
t . Fatal ( err )
}
keyword := "READY>"
result , err := store . UpdatePort ( context . Background ( ) , "CH12" , domain . PortUpdate { AlarmKeyword : & keyword } )
if err != nil {
t . Fatal ( err )
}
if result . Config == nil || ! slices . Contains ( result . Config . PendingSections , "ports" ) || ! slices . Contains ( result . Config . PendingSections , "alarm_rules" ) {
t . Fatalf ( "pending config = %+v, want ports and alarms" , result . Config )
}
if _ , err := store . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatal ( err )
}
rule , err := store . AlarmRule ( context . Background ( ) , "port_ch12_keyword" )
if err != nil || ! rule . Enabled || rule . Trigger != keyword || rule . Channel != "CH12" {
t . Fatalf ( "CH12 keyword rule = %+v, %v" , rule , err )
}
event , err := store . RecordPortLog ( context . Background ( ) , domain . PortLogEvent {
Channel : "CH12" , EventType : "traffic" , Direction : "RX" , Payload : [ ] byte ( "switch READY>" ) ,
PayloadBytes : 13 , PayloadEncoding : "base64" , Message : "serial payload" ,
} )
if err != nil {
t . Fatal ( err )
}
if ! event . AlarmTriggered || strings . Join ( event . KeywordMatches , "," ) != "port_ch12_keyword" {
t . Fatalf ( "received payload event = %+v" , event )
}
if len ( controller . requests ) != 1 || controller . requests [ 0 ] . RuleID != "port_ch12_keyword" {
t . Fatalf ( "delivery requests = %+v" , controller . requests )
}
}
func TestLegacyAlarmExportGainsMissingPerPortAndLifecycleRules ( t * testing . T ) {
legacy := [ ] domain . AlarmRule { {
RuleID : "auth_failures" , Name : "Legacy auth" , Source : "auth" , Trigger : "5 failures / 10 min" ,
Severity : "warning" , Delivery : [ ] string { "web" } , Enabled : true ,
} }
restored , err := alarmRuleConfigFromExport ( legacy )
if err != nil {
t . Fatal ( err )
}
if restored [ "auth_failures" ] . Name != "Legacy auth" {
t . Fatalf ( "legacy rule was overwritten: %+v" , restored [ "auth_failures" ] )
}
for _ , ruleID := range [ ] string { "cold_start" , "warm_start" , "port_ch01_keyword" , "port_ch48_dcd" , "port_ch48_dsr" } {
if _ , ok := restored [ ruleID ] ; ! ok {
t . Fatalf ( "restored legacy export is missing %s" , ruleID )
}
}
}
func TestSerialDCDAndDSRChangeRulesTrackSignalsIndependently ( t * testing . T ) {
controller := & recordingAlarmDeliveryController { }
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AlarmDeliveryController : controller } )
if err != nil {
t . Fatal ( err )
}
enabled := true
for _ , ruleID := range [ ] string { "port_ch12_dcd" , "port_ch12_dsr" } {
if _ , err := store . UpdateAlarmRule ( context . Background ( ) , ruleID , domain . AlarmRuleUpdate { Enabled : & enabled } ) ; err != nil {
t . Fatal ( err )
}
}
if _ , err := store . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatal ( err )
}
start := fixedClock ( )
for _ , signal := range [ ] string { "dcd" , "dsr" } {
event , err := store . RecordLinkState ( context . Background ( ) , domain . LinkStateObservation {
Kind : "serial" , Resource : "CH12" , State : "up" , Signal : signal , ObservedAt : start ,
} )
if err != nil || event == nil || event . EventType != "snapshot" || event . AlarmTriggered {
t . Fatalf ( "initial %s event = %+v, %v" , signal , event , err )
}
}
dcd , err := store . RecordLinkState ( context . Background ( ) , domain . LinkStateObservation {
Kind : "serial" , Resource : "CH12" , State : "down" , Signal : "dcd" , ObservedAt : start . Add ( time . Minute ) ,
} )
if err != nil || strings . Join ( dcd . MatchedRuleIDs , "," ) != "port_ch12_dcd,serial_link_down" {
t . Fatalf ( "DCD transition = %+v, %v" , dcd , err )
}
dsr , err := store . RecordLinkState ( context . Background ( ) , domain . LinkStateObservation {
Kind : "serial" , Resource : "CH12" , State : "down" , Signal : "dsr" , ObservedAt : start . Add ( 2 * time . Minute ) ,
} )
if err != nil || strings . Join ( dsr . MatchedRuleIDs , "," ) != "port_ch12_dsr,serial_link_down" {
t . Fatalf ( "DSR transition = %+v, %v" , dsr , err )
}
}
func TestLifecycleAlarmRecordsSystemEventAndDispatchesRule ( t * testing . T ) {
controller := & recordingAlarmDeliveryController { }
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AlarmDeliveryController : controller } )
if err != nil {
t . Fatal ( err )
}
if err := store . EmitLifecycleAlarm ( context . Background ( ) , "cold_start" , "device cold start detected" ) ; err != nil {
t . Fatal ( err )
}
if len ( controller . requests ) != 1 || controller . requests [ 0 ] . RuleID != "cold_start" {
t . Fatalf ( "lifecycle deliveries = %+v" , controller . requests )
}
events , err := store . SystemLogEvents ( context . Background ( ) , domain . SystemLogFilter { Limit : 10 } )
if err != nil || len ( events ) == 0 || events [ len ( events ) - 1 ] . Source != "system-lifecycle" {
t . Fatalf ( "lifecycle system events = %+v, %v" , events , err )
}
}
func TestPortCommandLogForwardsConfiguredSyslogAndKeywordAlarm ( t * testing . T ) {
controller := & recordingAlarmDeliveryController { }
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AlarmDeliveryController : controller } )
if err != nil {
t . Fatal ( err )
}
enabled := true
if _ , err := store . UpdateRemoteSyslog ( context . Background ( ) , domain . RemoteSyslogUpdate { Enabled : & enabled } ) ; err != nil {
t . Fatal ( err )
}
if _ , err := store . UpdateLogRetention ( context . Background ( ) , domain . LogRetentionUpdate { RemoteOperationEnabled : & enabled } ) ; err != nil {
t . Fatal ( err )
}
if _ , err := store . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatal ( err )
}
controller . requests = nil
if _ , err := store . RecordPortLog ( context . Background ( ) , domain . PortLogEvent {
Channel : "CH01" , EventType : "command" , Command : "LOGIN FAILED" , ReturnValue : "rejected" , Message : "command rejected" ,
} ) ; err != nil {
t . Fatal ( err )
}
if len ( controller . requests ) != 2 {
t . Fatalf ( "delivery requests = %+v, want keyword alarm and general syslog forward" , controller . requests )
}
ruleIDs := [ ] string { controller . requests [ 0 ] . RuleID , controller . requests [ 1 ] . RuleID }
slices . Sort ( ruleIDs )
if strings . Join ( ruleIDs , "," ) != "log_forward_operation,port_ch01_keyword" {
t . Fatalf ( "delivery rule IDs = %v" , ruleIDs )
}
for _ , request := range controller . requests {
if ! slices . Contains ( request . Channels , alarmdelivery . ChannelSyslog ) || request . Syslog == nil {
t . Fatalf ( "delivery request is missing configured syslog target: %+v" , request )
}
}
}
func TestLogCategorySelectorsGateLocalOperationAndRemoteForwarding ( t * testing . T ) {
controller := & recordingAlarmDeliveryController { }
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { AlarmDeliveryController : controller } )
if err != nil {
t . Fatal ( err )
}
disabled := false
enabled := true
if _ , err := store . UpdateLogRetention ( context . Background ( ) , domain . LogRetentionUpdate { LocalOperationEnabled : & disabled } ) ; err != nil {
t . Fatal ( err )
}
if _ , err := store . UpdateRemoteSyslog ( context . Background ( ) , domain . RemoteSyslogUpdate { Enabled : & enabled } ) ; err != nil {
t . Fatal ( err )
}
if _ , err := store . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatal ( err )
}
controller . requests = nil
if _ , err := store . RecordPortLog ( context . Background ( ) , domain . PortLogEvent {
Channel : "CH02" , EventType : "command" , Command : "show version" , ReturnValue : "ok" , Message : "completed" ,
} ) ; err != nil {
t . Fatal ( err )
}
events , err := store . PortLogEvents ( context . Background ( ) , domain . PortLogFilter { Channel : "CH02" } )
if err != nil {
t . Fatal ( err )
}
if len ( events ) != 0 || len ( controller . requests ) != 0 {
t . Fatalf ( "disabled local/remote operation selectors stored=%+v forwarded=%+v" , events , controller . requests )
}
if _ , err := store . UpdateLogRetention ( context . Background ( ) , domain . LogRetentionUpdate { RemoteOperationEnabled : & enabled } ) ; err != nil {
t . Fatal ( err )
}
if _ , err := store . ApplyConfig ( context . Background ( ) ) ; err != nil {
t . Fatal ( err )
}
if _ , err := store . RecordPortLog ( context . Background ( ) , domain . PortLogEvent {
Channel : "CH02" , EventType : "command" , Command : "show clock" , ReturnValue : "ok" , Message : "completed" ,
} ) ; err != nil {
t . Fatal ( err )
}
if len ( controller . requests ) != 1 || controller . requests [ 0 ] . RuleID != "log_forward_operation" {
t . Fatalf ( "enabled remote operation selector requests = %+v" , controller . requests )
}
policy , err := store . LogPolicy ( context . Background ( ) )
if err != nil || ! policy . RemoteSyslogForwarding {
t . Fatalf ( "effective log policy = %+v, err=%v" , policy , err )
}
}
func TestLinkLogPersistsSnapshotsAndOnlyAlarmsOnUpToDown ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "links.jsonl" )
store , err := NewStoreWithOptions ( fixedClock , StoreOptions { LinkLogPath : path , LinkSnapshotInterval : 5 * time . Minute } )
if err != nil {
t . Fatal ( err )
}
start := fixedClock ( )
first , err := store . RecordLinkState ( context . Background ( ) , domain . LinkStateObservation { Kind : "serial" , Resource : "CH01" , State : "down" , Signal : "dcd" , ObservedAt : start } )
if err != nil || first == nil || first . AlarmTriggered {
t . Fatalf ( "first event = %+v, err=%v" , first , err )
}
quiet , err := store . RecordLinkState ( context . Background ( ) , domain . LinkStateObservation { Kind : "serial" , Resource : "CH01" , State : "down" , Signal : "dcd" , ObservedAt : start . Add ( time . Minute ) } )
if err != nil || quiet != nil {
t . Fatalf ( "quiet event = %+v, err=%v" , quiet , err )
}
if _ , err := store . RecordLinkState ( context . Background ( ) , domain . LinkStateObservation { Kind : "serial" , Resource : "CH01" , State : "up" , Signal : "dcd" , ObservedAt : start . Add ( 2 * time . Minute ) } ) ; err != nil {
t . Fatal ( err )
}
down , err := store . RecordLinkState ( context . Background ( ) , domain . LinkStateObservation { Kind : "serial" , Resource : "CH01" , State : "down" , Signal : "dcd" , ObservedAt : start . Add ( 3 * time . Minute ) } )
if err != nil || down == nil || ! down . AlarmTriggered || strings . Join ( down . MatchedRuleIDs , "," ) != "serial_link_down" {
t . Fatalf ( "down event = %+v, err=%v" , down , err )
}
periodic , err := store . RecordLinkState ( context . Background ( ) , domain . LinkStateObservation { Kind : "serial" , Resource : "CH01" , State : "down" , Signal : "dcd" , ObservedAt : start . Add ( 9 * time . Minute ) } )
if err != nil || periodic == nil || periodic . EventType != "snapshot" || periodic . AlarmTriggered {
t . Fatalf ( "periodic event = %+v, err=%v" , periodic , err )
}
reloaded , err := NewStoreWithOptions ( fixedClock , StoreOptions { LinkLogPath : path } )
if err != nil {
t . Fatal ( err )
}
events , err := reloaded . LinkLogEvents ( context . Background ( ) , domain . LinkLogFilter { Kind : "serial" , Resource : "CH01" } )
if err != nil || len ( events ) != 4 {
t . Fatalf ( "reloaded link events = %+v, err=%v" , events , err )
}
snapshots , err := reloaded . LinkLogEvents ( context . Background ( ) , domain . LinkLogFilter {
Kind : "serial" , Resource : "CH01" , State : "down" , EventType : "snapshot" , Keyword : "dcd" ,
From : start . Add ( 5 * time . Minute ) , To : start . Add ( 10 * time . Minute ) ,
} )
if err != nil || len ( snapshots ) != 1 || snapshots [ 0 ] . EventType != "snapshot" {
t . Fatalf ( "filtered link snapshots = %+v, err=%v" , snapshots , err )
}
if _ , err := reloaded . LinkLogEvents ( context . Background ( ) , domain . LinkLogFilter { EventType : "command" } ) ; err == nil {
t . Fatal ( "invalid link event type was accepted" )
}
}
func fixedClock ( ) time . Time {
return time . Date ( 2026 , 6 , 18 , 0 , 0 , 0 , 0 , time . UTC )
}
func assertStoredOperationMetadata ( t * testing . T , operation domain . Operation ) {
t . Helper ( )
now := fixedClock ( )
if operation . ProgressPercent != 100 {
t . Fatalf ( "operation progress = %d, want 100" , operation . ProgressPercent )
}
if ! operation . CreatedAt . Equal ( now ) {
t . Fatalf ( "operation created_at = %s, want %s" , operation . CreatedAt , now )
}
if ! operation . UpdatedAt . Equal ( now ) {
t . Fatalf ( "operation updated_at = %s, want %s" , operation . UpdatedAt , now )
}
if operation . CompletedAt == nil || ! operation . CompletedAt . Equal ( now ) {
t . Fatalf ( "operation completed_at = %v, want %s" , operation . CompletedAt , now )
}
}
func assertOperationResult ( t * testing . T , operation domain . Operation , key string , want any ) {
t . Helper ( )
got , ok := operation . Result [ key ]
if ! ok {
t . Fatalf ( "operation result missing %q: %#v" , key , operation . Result )
}
if got != want {
t . Fatalf ( "operation result[%q] = %#v, want %#v" , key , got , want )
}
}