okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
eaef427
main
1// Connection snapshot races, refresh wait cancellation, and pooled transport 2// retries need deterministic gates that a live daemon cannot reliably expose. 3package daemon 4 5import ( 6"context" 7"encoding/json" 8"errors" 9"io" 10"net" 11"net/http" 12"net/http/httptest" 13"strings" 14"sync" 15"sync/atomic" 16"testing" 17 18"albedo/cli/internal/daemon/protocol" 19) 20 21type captureTransport struct { 22entered chan * http.Request 23release chan struct {} 24body string 25status int 26} 27 28func (transport captureTransport )RoundTrip (request * http.Request ) (* http.Response ,error ) { 29transport .entered <- request 30<- transport .release 31body ,status := transport .body ,transport .status 32if body == "" { 33body = `{"result":"accepted"}` 34 } 35if status == 0 { 36status = http .StatusOK 37 } 38return & http.Response {StatusCode :status ,Body :io .NopCloser (strings .NewReader (body )),Header :make (http.Header )},nil 39} 40 41func TestOperationUsesCapturedAddressAndToken (t * testing.T ) { 42conn := NewConnection (ConnectionSnapshot {Port :34567 ,Token :"old" },nil ) 43transport := captureTransport {entered :make (chan * http.Request ),release :make (chan struct {})} 44conn .HTTPClient ().Transport = transport 45finished := make (chan error ,1 ) 46go func () { 47_ ,err := requestBytes (context .Background (),conn ,operation {Name :"submit" ,Method :http .MethodPost ,Path :"/sessions/s/inputs/i" ,Body :map [string ]string {"text" :"hello" },Policy :authRecovery },responseLimits {bodyBytes :50 * 1024 * 1024 ,errorBytes :64 * 1024 }) 48finished <- err 49 }() 50request := <- transport .entered 51conn .Update (NewConnection (ConnectionSnapshot {Port :45678 ,Token :"new" },nil )) 52if request .URL .Host != "127.0.0.1:34567" || request .Header .Get ("Authorization" )!= "Bearer old" { 53t .Errorf ("request mixed connection snapshots: %s, %s" ,request .URL .Host ,request .Header .Get ("Authorization" )) 54 } 55close (transport .release ) 56if err := <- finished ;err != nil { 57t .Fatal (err ) 58 } 59} 60 61func TestPooledMutationAcknowledgementLossIsNotReplayed (t * testing.T ) { 62for _ ,test := range []struct { 63body any 64name string 65method string 66 }{ 67 {name :"submission" ,method :http .MethodPost ,body :map [string ]string {"text" :"hello" }}, 68 {name :"sign-in start" ,method :http .MethodPost }, 69 {name :"delete" ,method :http .MethodDelete }, 70 } { 71t .Run (test .name ,func (t * testing.T ) { 72var calls atomic.Int32 73var connections atomic.Int32 74server := httptest .NewUnstartedServer (http .HandlerFunc (func (writer http.ResponseWriter ,request * http.Request ) { 75_ ,_ = io .Copy (io .Discard ,request .Body ) 76if calls .Add (1 )== 1 { 77_ ,_ = writer .Write ([]byte (`{"result":"accepted"}` )) 78return 79 } 80connection ,_ ,err := writer .(http.Hijacker ).Hijack () 81if err != nil { 82t .Error (err ) 83return 84 } 85_ = connection .Close () 86 })) 87server .Config .ConnState = func (_ net.Conn ,state http.ConnState ) { 88if state == http .StateNew { 89connections .Add (1 ) 90 } 91 } 92server .Start () 93defer server .Close () 94conn := NewConnection (ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port },nil ) 95defer conn .HTTPClient ().CloseIdleConnections () 96operation := operation {Name :"submit" ,Method :test .method ,Path :"/sessions/s/inputs/i" ,Body :test .body ,Policy :authRecovery } 97if _ ,err := requestBytes (context .Background (),conn ,operation ,responseLimits {bodyBytes :50 * 1024 * 1024 ,errorBytes :64 * 1024 });err != nil { 98t .Fatal (err ) 99 } 100_ ,err := requestBytes (context .Background (),conn ,operation ,responseLimits {bodyBytes :50 * 1024 * 1024 ,errorBytes :64 * 1024 }) 101if _ ,ok := errors.AsType [* UncertainOutcomeError ](err );! ok { 102t .Fatalf ("lost response returned %v" ,err ) 103 } 104if calls .Load ()!= 2 || connections .Load ()!= 1 { 105t .Fatalf ("mutation was replayed or fixture missed reuse: calls=%d connections=%d" ,calls .Load (),connections .Load ()) 106 } 107 }) 108 } 109} 110 111func TestConcurrentOperationsNeverMixConnectionSnapshots (t * testing.T ) { 112var failures atomic.Int32 113newServer := func (token string )* httptest.Server { 114return httptest .NewServer (http .HandlerFunc (func (writer http.ResponseWriter ,request * http.Request ) { 115if request .Header .Get ("Authorization" )!= "Bearer " + token { 116failures .Add (1 ) 117 } 118_ ,_ = writer .Write ([]byte (`{}` )) 119 })) 120 } 121first ,second := newServer ("first" ),newServer ("second" ) 122defer first .Close () 123defer second .Close () 124firstConn := NewConnection (ConnectionSnapshot {Port :first .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"first" },nil ) 125secondConn := NewConnection (ConnectionSnapshot {Port :second .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"second" },nil ) 126conn := NewConnection (firstConn .Snapshot (),nil ) 127defer conn .HTTPClient ().CloseIdleConnections () 128var readers sync.WaitGroup 129for range 4 { 130readers .Go (func () { 131for range 50 { 132_ ,err := requestBytes (context .Background (),conn ,operation {Name :"read" ,Method :http .MethodGet ,Path :"/server" ,Policy :readRecovery },responseLimits {bodyBytes :50 * 1024 * 1024 ,errorBytes :64 * 1024 }) 133if err != nil { 134t .Error (err ) 135 } 136 } 137 }) 138 } 139for range 100 { 140conn .Update (secondConn ) 141conn .Update (firstConn ) 142 } 143readers .Wait () 144if failures .Load ()!= 0 { 145t .Fatalf ("%d requests paired address with another daemon's token" ,failures .Load ()) 146 } 147} 148 149// Auth refusal must prove non-admission before a mutation can be dispatched again. 150func TestMutationAuthRecoveryBudget (t * testing.T ) { 151for _ ,test := range []struct { 152name string 153token string 154status int 155expected int32 156marker bool 157rediscover bool 158repeated bool 159 }{ 160 {name :"changed credentials" ,marker :true ,status :401 ,token :"new" ,rediscover :true ,expected :2 }, 161 {name :"unchanged credentials" ,marker :true ,status :401 ,token :"old" ,rediscover :true ,expected :1 }, 162 {name :"unmarked refusal" ,status :403 ,token :"new" ,rediscover :true ,expected :1 }, 163 {name :"unmarked unauthorized" ,status :401 ,token :"new" ,rediscover :true ,expected :1 }, 164 {name :"marked unauthorized" ,marker :true ,status :401 ,token :"new" ,rediscover :true ,expected :2 }, 165 {name :"no discovery" ,marker :true ,status :401 ,token :"new" ,expected :1 }, 166 {name :"repeated refusal" ,marker :true ,status :401 ,token :"new" ,rediscover :true ,repeated :true ,expected :2 }, 167 } { 168t .Run (test .name ,func (t * testing.T ) { 169var calls atomic.Int32 170server := httptest .NewServer (http .HandlerFunc (func (writer http.ResponseWriter ,request * http.Request ) { 171if request .URL .Path == "/server" { 172_ ,_ = writer .Write ([]byte (readyServer )) 173return 174 } 175count := calls .Add (1 ) 176if count == 1 || test .repeated { 177writer .WriteHeader (test .status ) 178if test .marker { 179_ ,_ = writer .Write ([]byte (`{"type":"about:blank","title":"Unauthorized","status":401,"code":"authentication_required","detail":"invalid bearer"}` )) 180 }else { 181_ ,_ = writer .Write ([]byte (`{"detail":"forbidden"}` )) 182 } 183return 184 } 185if request .Header .Get ("Authorization" )!= "Bearer new" { 186t .Errorf ("recovery reused old credentials" ) 187 } 188_ ,_ = writer .Write ([]byte (`{"result":"accepted"}` )) 189 })) 190defer server .Close () 191port := server .Listener .Addr ().(* net.TCPAddr ).Port 192var rediscover Rediscovery 193if test .rediscover { 194rediscover = func (context.Context ) (ConnectionSnapshot ,error ) { 195return ConnectionSnapshot {Port :port ,Token :test .token ,Version :ProtocolVersion },nil 196 } 197 } 198conn := NewConnection (ConnectionSnapshot {Port :port ,Token :"old" ,Version :ProtocolVersion },rediscover ) 199defer conn .HTTPClient ().CloseIdleConnections () 200_ ,err := requestBytes (context .Background (),conn ,operation {Name :"mutate" ,Method :http .MethodPost ,Path :"/sessions/s/inputs/i" ,Policy :authRecovery },responseLimits {bodyBytes :50 * 1024 * 1024 ,errorBytes :64 * 1024 }) 201if calls .Load ()!= test .expected { 202t .Fatalf ("got %d dispatches, expected %d" ,calls .Load (),test .expected ) 203 } 204success := test .expected == 2 && ! test .repeated 205if (err == nil )!= success { 206t .Fatalf ("unexpected result %v" ,err ) 207 } 208if ! success { 209if _ ,uncertain := errors.AsType [* UncertainOutcomeError ](err );uncertain { 210t .Fatalf ("pre-admission rejection reported uncertain: %v" ,err ) 211 } 212 } 213 }) 214 } 215} 216 217func TestReadRecoveryIncludesTruncatedBody (t * testing.T ) { 218var calls atomic.Int32 219server := httptest .NewServer (http .HandlerFunc (func (writer http.ResponseWriter ,request * http.Request ) { 220if request .URL .Path == "/server" { 221_ ,_ = writer .Write ([]byte (readyServer )) 222return 223 } 224if calls .Add (1 )== 1 { 225writer .Header ().Set ("Content-Length" ,"100" ) 226_ ,_ = writer .Write ([]byte (`{"ok":` )) 227return 228 } 229_ ,_ = writer .Write ([]byte (`{"items":[],"next":null}` )) 230 })) 231defer server .Close () 232snapshot := ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"token" ,Version :ProtocolVersion } 233conn := NewConnection (snapshot ,func (context.Context ) (ConnectionSnapshot ,error ) {return snapshot ,nil }) 234defer conn .HTTPClient ().CloseIdleConnections () 235result ,err := ListSessions (context .Background (),conn ) 236if err != nil || len (result )!= 0 || calls .Load ()!= 2 { 237t .Fatalf ("truncated read did not recover once: %v, %v, %d" ,result ,err ,calls .Load ()) 238 } 239} 240 241func TestMalformedMutationAcknowledgementIsUncertain (t * testing.T ) { 242var calls atomic.Int32 243server := httptest .NewServer (http .HandlerFunc (func (writer http.ResponseWriter ,_ * http.Request ) {calls .Add (1 );_ ,_ = writer .Write ([]byte (`{"ok":` )) })) 244defer server .Close () 245conn := NewConnection (ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port },nil ) 246defer conn .HTTPClient ().CloseIdleConnections () 247err := executeMutation (context .Background (),conn ,operation {Name :"interrupt session" ,Method :http .MethodPost ,Path :"/sessions/s/interrupt" ,Body :struct {}{},Policy :noRecovery }, []int {200 },func (body []byte ,_ int )error {var result protocol.Interruption ;return json .Unmarshal (body ,& result ) }) 248if _ ,ok := errors.AsType [* UncertainOutcomeError ](err );! ok { 249t .Fatalf ("invalid acknowledgement returned %v" ,err ) 250 } 251if _ ,ok := errors.AsType [* json.SyntaxError ](err );! ok { 252t .Fatalf ("decode cause lost: %v" ,err ) 253 } 254if calls .Load ()!= 1 { 255t .Fatalf ("invalid acknowledgement replayed %d times" ,calls .Load ()) 256 } 257} 258 259func TestRecordOpenDoesNotRecoverAuthentication (t * testing.T ) { 260var calls atomic.Int32 261server := httptest .NewServer (http .HandlerFunc (func (writer http.ResponseWriter ,request * http.Request ) { 262if request .URL .Path == "/server" { 263_ ,_ = writer .Write ([]byte (readyServer )) 264return 265 } 266calls .Add (1 ) 267writer .WriteHeader (http .StatusUnauthorized ) 268_ ,_ = writer .Write ([]byte (`{"type":"about:blank","title":"Unauthorized","status":401,"code":"authentication_required","detail":"invalid bearer"}` )) 269 })) 270defer server .Close () 271snapshot := ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"new" ,Version :ProtocolVersion } 272replacement := snapshot 273snapshot .Token = "old" 274conn := NewConnection (snapshot ,func (context.Context ) (ConnectionSnapshot ,error ) {return replacement ,nil }) 275defer conn .HTTPClient ().CloseIdleConnections () 276_ ,err := RecordOpen (context .Background (),conn ,"test" ) 277if err == nil || calls .Load ()!= 1 || conn .Snapshot ().Token != "old" { 278t .Fatalf ("open recovered authentication: %v, %d, %s" ,err ,calls .Load (),conn .Snapshot ().Token ) 279 } 280} 281 282type trackedResponseBody struct { 283 io.Reader 284closed bool 285} 286 287func (body * trackedResponseBody )Close ()error {body .closed = true ;return nil } 288 289type responseTransport struct {response * http.Response } 290 291func (transport responseTransport )RoundTrip (_ * http.Request ) (* http.Response ,error ) { 292return transport .response ,nil 293} 294 295func TestOperationClosesResponsesAndBoundsReads (t * testing.T ) { 296for _ ,test := range []struct { 297name string 298content string 299status int 300success bool 301 }{ 302 {name :"success" ,status :200 ,content :"ok" ,success :true }, 303 {name :"oversized success" ,status :200 ,content :"123456" }, 304 {name :"rejection" ,status :403 ,content :`{"detail":"no"}` }, 305 {name :"server failure" ,status :503 ,content :"bad" }, 306 } { 307t .Run (test .name ,func (t * testing.T ) { 308body := & trackedResponseBody {Reader :strings .NewReader (test .content )} 309conn := NewConnection (ConnectionSnapshot {Port :12345 },nil ) 310conn .HTTPClient ().Transport = responseTransport {response :& http.Response {StatusCode :test .status ,Body :body ,Header :make (http.Header )}} 311_ ,err := requestBytes (context .Background (),conn ,operation {Name :"mutate" ,Method :http .MethodPost ,Path :"/sessions/s/inputs/i" ,Policy :authRecovery },responseLimits {bodyBytes :4 ,errorBytes :4 }) 312if ! body .closed { 313t .Fatal ("operation leaked response body" ) 314 } 315if (err == nil )!= test .success { 316t .Fatalf ("unexpected result %v" ,err ) 317 } 318if test .name == "oversized success" { 319if _ ,uncertain := errors.AsType [* UncertainOutcomeError ](err );! uncertain { 320t .Fatalf ("lost acknowledgement not uncertain: %v" ,err ) 321 } 322 } 323 }) 324 } 325} 326 327func TestMutationRedirectIsNotFollowed (t * testing.T ) { 328var admissions atomic.Int32 329target := httptest .NewServer (http .HandlerFunc (func (writer http.ResponseWriter ,_ * http.Request ) { 330admissions .Add (1 ) 331_ ,_ = writer .Write ([]byte (`{}` )) 332 })) 333defer target .Close () 334source := httptest .NewServer (http .HandlerFunc (func (writer http.ResponseWriter ,request * http.Request ) { 335http .Redirect (writer ,request ,target .URL ,http .StatusTemporaryRedirect ) 336 })) 337defer source .Close () 338conn := NewConnection (ConnectionSnapshot {Port :source .Listener .Addr ().(* net.TCPAddr ).Port },nil ) 339defer conn .HTTPClient ().CloseIdleConnections () 340_ ,err := requestBytes (context .Background (),conn ,operation {Name :"mutate" ,Method :http .MethodPost ,Path :"/sessions/s/inputs/i" ,Body :map [string ]string {"text" :"hello" },Policy :authRecovery },responseLimits {bodyBytes :50 * 1024 * 1024 ,errorBytes :64 * 1024 }) 341failure ,ok := errors.AsType [* APIError ](err ) 342if ! ok || failure .StatusCode != 307 || admissions .Load ()!= 0 { 343t .Fatalf ("redirect policy changed: %v admissions=%d" ,err ,admissions .Load ()) 344 } 345}