okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
61c43ca
main
1// Hanging refusal bodies and failures after stream delivery require controlled 2// peers; real daemon events cannot reliably force these transport boundaries. 3package daemon 4 5import ( 6"context" 7"errors" 8"io" 9"net" 10"net/http" 11"net/http/httptest" 12"sync/atomic" 13"testing" 14"time" 15) 16 17type streamCaller struct { 18start func (context.Context ,* Connection ,func ()error )error 19name string 20} 21 22func streamCallers () []streamCaller { 23return []streamCaller { 24 {name :"agents" ,start :func (ctx context.Context ,conn * Connection ,deliver func ()error )error { 25return StreamAgents (ctx ,conn ,func (_ []AgentEvent )error {return deliver () }) 26 }}, 27 {name :"chat" ,start :func (ctx context.Context ,conn * Connection ,deliver func ()error )error { 28return NewChatClient (conn ,"test" ).Stream (ctx ,0 ,func (event StreamEvent )error { 29if event .Type == EventText { 30return deliver () 31 } 32return nil 33 }) 34 }}, 35 } 36} 37 38func TestStreamAuthenticationRecoveryBeforeDelivery (t * testing.T ) { 39for _ ,caller := range streamCallers () { 40t .Run (caller .name ,func (t * testing.T ) { 41for _ ,test := range []struct { 42name ,token string 43marked bool 44requests int32 45 }{ 46 {name :"changed credentials" ,token :"new" ,marked :true ,requests :2 }, 47 {name :"unchanged credentials" ,token :"old" ,marked :true ,requests :1 }, 48 {name :"unmarked refusal" ,token :"new" ,requests :1 }, 49 } { 50t .Run (test .name ,func (t * testing.T ) { 51var requests atomic.Int32 52var deliveries atomic.Int32 53server := httptest .NewServer (http .HandlerFunc (func (writer http.ResponseWriter ,request * http.Request ) { 54if request .URL .Path == "/server" { 55_ ,_ = writer .Write ([]byte (readyServer )) 56return 57 } 58if requests .Add (1 )== 1 { 59if test .marked { 60writer .Header ().Set ("WWW-Authenticate" ,"Bearer" ) 61writer .WriteHeader (http .StatusUnauthorized ) 62 }else { 63writer .WriteHeader (http .StatusForbidden ) 64 } 65if caller .name == "chat" { 66if test .marked { 67_ ,_ = writer .Write ([]byte (`{"code":"authentication_required"}` )) 68 }else { 69_ ,_ = writer .Write ([]byte (`{"detail":"denied"}` )) 70 } 71return 72 } 73// The refusal marker allows closing the unfinished 74// body must cancel this handler without waiting for more bytes. 75writer .(http.Flusher ).Flush () 76<- request .Context ().Done () 77return 78 } 79if request .Header .Get ("Authorization" )!= "Bearer new" { 80t .Error ("stream reused old credentials" ) 81 } 82writer .Header ().Set ("Content-Type" ,"text/event-stream" ) 83writeInitialStream (writer ,caller .name ) 84 })) 85defer server .Close () 86 87snapshot := ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Token :test .token ,Version :ProtocolVersion } 88replacement := snapshot 89snapshot .Token = "old" 90conn := NewConnection (snapshot ,func (context.Context ) (ConnectionSnapshot ,error ) {return replacement ,nil }) 91defer conn .HTTPClient ().CloseIdleConnections () 92ctx ,cancel := context .WithTimeout (context .Background (),time .Second ) 93defer cancel () 94streamErr := caller .start (ctx ,conn ,func ()error {deliveries .Add (1 );return nil }) 95if errors .Is (streamErr ,context .DeadlineExceeded ) { 96t .Fatalf ("stream waited for unfinished refusal body: %v" ,streamErr ) 97 } 98if requests .Load ()!= test .requests { 99t .Fatalf ("got %d requests, expected %d" ,requests .Load (),test .requests ) 100 } 101if test .requests == 2 { 102if streamErr != nil || deliveries .Load ()!= 1 { 103t .Fatalf ("stream failed recovery: %v deliveries=%d" ,streamErr ,deliveries .Load ()) 104 } 105 }else { 106failure ,ok := errors.AsType [* APIError ](streamErr ) 107expectedStatus := http .StatusForbidden 108if test .marked { 109expectedStatus = http .StatusUnauthorized 110 } 111if ! ok || failure .StatusCode != expectedStatus || deliveries .Load ()!= 0 { 112t .Fatalf ("refusal changed: %v deliveries=%d" ,streamErr ,deliveries .Load ()) 113 } 114 } 115 }) 116 } 117 }) 118 } 119} 120 121func TestAcceptedStreamIsNeverReplayed (t * testing.T ) { 122for _ ,caller := range streamCallers () { 123t .Run (caller .name ,func (t * testing.T ) { 124for _ ,failure := range []string {"truncated body" ,"callback error" } { 125t .Run (failure ,func (t * testing.T ) { 126var requests atomic.Int32 127var deliveries atomic.Int32 128callbackFailure := errors .New ("consumer stopped" ) 129server := httptest .NewServer (http .HandlerFunc (func (writer http.ResponseWriter ,request * http.Request ) { 130if request .URL .Path == "/server" { 131_ ,_ = writer .Write ([]byte (readyServer )) 132return 133 } 134requests .Add (1 ) 135if failure == "truncated body" { 136writer .Header ().Set ("Content-Length" ,"1000000" ) 137 } 138writer .Header ().Set ("Content-Type" ,"text/event-stream" ) 139writeInitialStream (writer ,caller .name ) 140 })) 141defer server .Close () 142snapshot := ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"token" ,Version :ProtocolVersion } 143 144replacement := snapshot 145conn := NewConnection (snapshot ,func (context.Context ) (ConnectionSnapshot ,error ) {return replacement ,nil }) 146defer conn .HTTPClient ().CloseIdleConnections () 147ctx ,cancel := context .WithTimeout (context .Background (),time .Second ) 148defer cancel () 149streamErr := caller .start (ctx ,conn ,func ()error { 150deliveries .Add (1 ) 151if failure == "callback error" { 152return callbackFailure 153 } 154return nil 155 }) 156if failure == "callback error" && ! errors .Is (streamErr ,callbackFailure ) { 157t .Fatalf ("callback failure lost: %v" ,streamErr ) 158 } 159if failure == "truncated body" && ! errors .Is (streamErr ,io .ErrUnexpectedEOF ) { 160t .Fatalf ("stream truncation lost: %v" ,streamErr ) 161 } 162if requests .Load ()!= 1 || deliveries .Load ()!= 1 { 163t .Fatalf ("accepted stream replayed: requests=%d deliveries=%d" ,requests .Load (),deliveries .Load ()) 164 } 165 }) 166 } 167 }) 168 } 169}