okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
8e3e173
main
1package daemon 2 3import ( 4"encoding/json" 5"fmt" 6"net" 7"net/http" 8"net/http/httptest" 9"sync/atomic" 10"testing" 11"time" 12 13"albedo/cli/internal/daemon/protocol" 14"albedo/cli/internal/testwire" 15) 16 17func quotaConnection (t * testing.T ,handler http.HandlerFunc )* Connection { 18t .Helper () 19server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 20if r .URL .Path == "/server" && r .URL .Query ().Get ("include" )!= "quota_history" { 21_ ,_ = w .Write ([]byte (testwire .Server )) 22return 23 } 24handler (w ,r ) 25 })) 26t .Cleanup (server .Close ) 27conn ,err := Attach (t .Context (),ConnectionSnapshot { 28Port :server .Listener .Addr ().(* net.TCPAddr ).Port , 29Token :"token" , 30Version :ProtocolVersion , 31 },nil ) 32if err != nil { 33t .Fatal (err ) 34 } 35t .Cleanup (conn .HTTPClient ().CloseIdleConnections ) 36return conn 37} 38 39func observation (seq int64 ,account ,provider ,limit string )QuotaObservation { 40return QuotaObservation { 41Sequence :seq , 42AccountID :account , 43Provider :provider , 44LimitID :limit , 45Label :"Primary" , 46Status :"ok" , 47Source :"poll" , 48ObservedAt :time .UnixMilli (seq * 1000 ).UTC ().Format (time .RFC3339 ), 49 } 50} 51 52func TestGetQuotaTrailPagingAndFiltering (t * testing.T ) { 53since := time .Date (2026 ,10 ,3 ,12 ,0 ,0 ,0 ,time .UTC ) 54var requestCount atomic.Int32 55 56conn := quotaConnection (t ,func (w http.ResponseWriter ,r * http.Request ) { 57q := r .URL .Query () 58if q .Get ("include" )!= "quota_history" { 59t .Errorf ("expected include=quota_history, got %q" ,q .Get ("include" )) 60 } 61if q .Get ("limit" )!= "200" { 62t .Errorf ("expected limit=200, got %q" ,q .Get ("limit" )) 63 } 64if q .Get ("quota_provider" )!= "hyper" { 65t .Errorf ("expected quota_provider=hyper, got %q" ,q .Get ("quota_provider" )) 66 } 67if q .Get ("quota_account" )!= "charm" { 68t .Errorf ("expected quota_account=charm, got %q" ,q .Get ("quota_account" )) 69 } 70if q .Get ("quota_limit" )!= "primary" { 71t .Errorf ("expected quota_limit=primary, got %q" ,q .Get ("quota_limit" )) 72 } 73if q .Get ("quota_since" )!= since .Format (time .RFC3339Nano ) { 74t .Errorf ("expected quota_since=%s, got %q" ,since .Format (time .RFC3339Nano ),q .Get ("quota_since" )) 75 } 76 77reqNum := requestCount .Add (1 ) 78var page protocol.QuotaPage 79switch reqNum { 80case 1 : 81if q .Get ("next" )!= "" { 82t .Errorf ("expected empty next on page 1, got %q" ,q .Get ("next" )) 83 } 84// Server returns newest-first: [seq 30, seq 20] 85nextCursor := "cursor-page-2" 86page = protocol.QuotaPage { 87Items : []QuotaObservation { 88observation (30 ,"charm" ,"hyper" ,"primary" ), 89observation (20 ,"charm" ,"hyper" ,"primary" ), 90 }, 91Next :& nextCursor , 92 } 93case 2 : 94if q .Get ("next" )!= "cursor-page-2" { 95t .Errorf ("expected next=cursor-page-2 on page 2, got %q" ,q .Get ("next" )) 96 } 97// Page 2 returns seq 10, next is nil 98page = protocol.QuotaPage { 99Items : []QuotaObservation { 100observation (10 ,"charm" ,"hyper" ,"primary" ), 101 }, 102Next :nil , 103 } 104default : 105t .Fatalf ("unexpected request number %d" ,reqNum ) 106 } 107 108var resp map [string ]any 109_ = json .Unmarshal ([]byte (testwire .Server ),& resp ) 110resp ["quota_history" ]= page 111w .Header ().Set ("Content-Type" ,"application/json" ) 112_ = json .NewEncoder (w ).Encode (resp ) 113 }) 114 115results ,err := GetQuotaTrail (t .Context (),conn ,QuotaTrailQuery { 116Provider :"hyper" , 117Account :"charm" , 118Limit :"primary" , 119Since :since , 120 }) 121if err != nil { 122t .Fatalf ("unexpected error: %v" ,err ) 123 } 124 125if int (requestCount .Load ())!= 2 { 126t .Fatalf ("expected 2 requests, made %d" ,requestCount .Load ()) 127 } 128 129// Must be returned OLDEST FIRST: [seq 10, seq 20, seq 30] 130if len (results )!= 3 { 131t .Fatalf ("expected 3 results, got %d" ,len (results )) 132 } 133expectedSeqs := []int64 {10 ,20 ,30 } 134for i ,exp := range expectedSeqs { 135if results [i ].Sequence != exp { 136t .Errorf ("result[%d] sequence: expected %d, got %d" ,i ,exp ,results [i ].Sequence ) 137 } 138 } 139} 140 141func TestGetQuotaTrailCapsAtFivePages (t * testing.T ) { 142var requestCount atomic.Int32 143 144conn := quotaConnection (t ,func (w http.ResponseWriter ,r * http.Request ) { 145reqNum := requestCount .Add (1 ) 146nextCursor := fmt .Sprintf ("cursor-%d" ,reqNum + 1 ) 147page := protocol.QuotaPage { 148Items : []QuotaObservation { 149observation (int64 (100 - reqNum ),"acc" ,"prov" ,"lim" ), 150 }, 151Next :& nextCursor , 152 } 153 154var resp map [string ]any 155_ = json .Unmarshal ([]byte (testwire .Server ),& resp ) 156resp ["quota_history" ]= page 157w .Header ().Set ("Content-Type" ,"application/json" ) 158_ = json .NewEncoder (w ).Encode (resp ) 159 }) 160 161results ,err := GetQuotaTrail (t .Context (),conn ,QuotaTrailQuery { 162Provider :"prov" , 163Account :"acc" , 164Limit :"lim" , 165 }) 166if err != nil { 167t .Fatalf ("unexpected error: %v" ,err ) 168 } 169 170// Must stop at 5 pages 171if requestCount .Load ()!= 5 { 172t .Fatalf ("expected exactly 5 requests, got %d" ,requestCount .Load ()) 173 } 174if len (results )!= 5 { 175t .Fatalf ("expected 5 results, got %d" ,len (results )) 176 } 177// Oldest first: sequences were [99, 98, 97, 96, 95] returned newest first, 178// so reversed should be [95, 96, 97, 98, 99]. 179expectedSeqs := []int64 {95 ,96 ,97 ,98 ,99 } 180for i ,exp := range expectedSeqs { 181if results [i ].Sequence != exp { 182t .Errorf ("result[%d] sequence: expected %d, got %d" ,i ,exp ,results [i ].Sequence ) 183 } 184 } 185} 186 187func TestGetQuotaTrailWithoutSinceSendsNoBound (t * testing.T ) { 188conn := quotaConnection (t ,func (w http.ResponseWriter ,r * http.Request ) { 189if r .URL .Query ().Has ("quota_since" ) { 190t .Errorf ("a zero Since must not be sent, got quota_since=%q" ,r .URL .Query ().Get ("quota_since" )) 191 } 192var resp map [string ]any 193_ = json .Unmarshal ([]byte (testwire .Server ),& resp ) 194resp ["quota_history" ]= protocol.QuotaPage {Items : []QuotaObservation {}} 195w .Header ().Set ("Content-Type" ,"application/json" ) 196_ = json .NewEncoder (w ).Encode (resp ) 197 }) 198if _ ,err := GetQuotaTrail (t .Context (),conn ,QuotaTrailQuery {Provider :"p" ,Account :"a" ,Limit :"l" });err != nil { 199t .Fatal (err ) 200 } 201}