okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
48f3878
main
1// Request pointers and omission can change replay intent after preparation. 2// Controlled acknowledgement loss exercises the public API's saved request. 3package daemon 4 5import ( 6"encoding/json" 7"errors" 8"fmt" 9"io" 10"net" 11"net/http" 12"net/http/httptest" 13"strings" 14"sync" 15"sync/atomic" 16"testing" 17) 18 19func TestSubmissionRecoversRejectedInputWithoutReplay (t * testing.T ) { 20var admissions atomic.Int64 21var lookups atomic.Int64 22conn := controlledConnection (t ,func (w http.ResponseWriter ,r * http.Request ) { 23id := strings .TrimPrefix (r .URL .Path ,"/sessions/s/inputs/" ) 24if id == r .URL .Path { 25t .Errorf ("unexpected input resource %s" ,r .URL ) 26w .WriteHeader (http .StatusNotFound ) 27return 28 } 29switch r .Method { 30case http .MethodPut : 31admissions .Add (1 ) 32connection ,_ ,err := w .(http.Hijacker ).Hijack () 33if err != nil { 34t .Error (err ) 35return 36 } 37_ = connection .Close () 38case http .MethodGet : 39lookups .Add (1 ) 40input := canonicalInput ("s" ,id ,"message" ,"pending" ) 41input ["admission" ],input ["http_status" ]= "rejected" ,http .StatusUnprocessableEntity 42input ["accepted_at" ],input ["acceptance_order" ],input ["delivery" ]= nil ,nil ,nil 43input ["problem" ]= map [string ]any {"type" :"about:blank" ,"title" :"Input refused" ,"status" :http .StatusUnprocessableEntity ,"code" :"invalid_input" ,"detail" :"" } 44_ = json .NewEncoder (w ).Encode (input ) 45default : 46t .Errorf ("unexpected input method %s" ,r .Method ) 47w .WriteHeader (http .StatusMethodNotAllowed ) 48 } 49 }) 50text := "rejected once" 51handle ,err := NewSubmission ("s" ,SubmissionRequest {Content :& text }) 52if err != nil { 53t .Fatal (err ) 54 } 55_ ,err = SubmitOperation (t .Context (),conn ,handle ) 56problem ,ok := errors.AsType [* APIError ](err ) 57if ! ok || problem .StatusCode != http .StatusUnprocessableEntity || problem .Code != "invalid_input" || problem .Message != "Input refused" { 58t .Fatalf ("lost rejection was not recovered: %v" ,err ) 59 } 60if admissions .Load ()!= 1 || lookups .Load ()!= 1 { 61t .Fatalf ("authoritative rejection replayed: admissions=%d lookups=%d" ,admissions .Load (),lookups .Load ()) 62 } 63} 64 65func TestSubmissionFreezesContentAndOmissionAcrossLostAcknowledgement (t * testing.T ) { 66for _ ,continuation := range []bool {false ,true } { 67t .Run (fmt .Sprint (continuation ),func (t * testing.T ) { 68var mu sync.Mutex 69var bodies []string 70server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 71if r .Method == http .MethodGet { 72w .WriteHeader (404 ) 73_ ,_ = fmt .Fprint (w ,`{"type":"about:blank","title":"Input not found","status":404,"code":"input_unknown","detail":"not found"}` ) 74return 75 } 76body ,_ := io .ReadAll (r .Body ) 77mu .Lock () 78bodies = append (bodies ,string (body )) 79first := len (bodies )== 1 80mu .Unlock () 81if first { 82connection ,_ ,err := w .(http.Hijacker ).Hijack () 83if err != nil { 84t .Error (err ) 85return 86 } 87_ = connection .Close () 88return 89 } 90var fields map [string ]string 91_ = json .Unmarshal (body ,& fields ) 92id := strings .TrimPrefix (r .URL .Path ,"/sessions/s/inputs/" ) 93if r .Method != http .MethodPut || id == r .URL .Path { 94t .Errorf ("wrong admission resource %s %s" ,r .Method ,r .URL ) 95 } 96w .WriteHeader (202 ) 97_ = json .NewEncoder (w ).Encode (canonicalInput ("s" ,id ,fields ["kind" ],"pending" )) 98 })) 99defer server .Close () 100conn := NewConnection (ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"token" },nil ) 101content := "" 102request := SubmissionRequest {Content :& content ,Type :"user" } 103if continuation { 104request .Content = nil 105request .Type = "continue" 106 } 107handle ,err := NewSubmission ("s" ,request ) 108if err != nil { 109t .Fatal (err ) 110 } 111content = "changed after preparation" 112_ ,err = SubmitOperation (t .Context (),conn ,handle ) 113if err != nil { 114t .Fatal (err ) 115 } 116mu .Lock () 117defer mu .Unlock () 118if len (bodies )!= 2 || bodies [0 ]!= bodies [1 ] { 119t .Fatalf ("recovery changed saved request: %v" ,bodies ) 120 } 121var fields map [string ]json.RawMessage 122if err = json .Unmarshal ([]byte (bodies [0 ]),& fields );err != nil { 123t .Fatal (err ) 124 } 125if continuation { 126if _ ,present := fields ["text" ];present { 127t .Fatalf ("continuation gained content: %s" ,bodies [0 ]) 128 } 129 }else if string (fields ["text" ])!= `""` { 130t .Fatalf ("prepared empty content changed: %s" ,bodies [0 ]) 131 } 132if _ ,present := fields ["submissionId" ];present { 133t .Fatalf ("input identity duplicated in body: %s" ,bodies [0 ]) 134 } 135 }) 136 } 137}