server_test.go 181 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762
  1. package server
  2. import (
  3. "bufio"
  4. "context"
  5. "crypto/rand"
  6. _ "embed"
  7. "encoding/base64"
  8. "encoding/json"
  9. "fmt"
  10. "io"
  11. "net/http"
  12. "net/http/httptest"
  13. "net/netip"
  14. "os"
  15. "path/filepath"
  16. "runtime/debug"
  17. "strings"
  18. "sync"
  19. "sync/atomic"
  20. "testing"
  21. "time"
  22. "github.com/stretchr/testify/require"
  23. "golang.org/x/crypto/bcrypt"
  24. "heckel.io/ntfy/v2/log"
  25. "heckel.io/ntfy/v2/user"
  26. "heckel.io/ntfy/v2/util"
  27. )
  28. func TestMain(m *testing.M) {
  29. log.SetLevel(log.ErrorLevel)
  30. os.Exit(m.Run())
  31. }
  32. func TestServer_PublishAndPoll(t *testing.T) {
  33. s := newTestServer(t, newTestConfig(t))
  34. response1 := request(t, s, "PUT", "/mytopic", "my first message", nil)
  35. msg1 := toMessage(t, response1.Body.String())
  36. require.NotEmpty(t, msg1.ID)
  37. require.Equal(t, "my first message", msg1.Message)
  38. response2 := request(t, s, "PUT", "/mytopic", "my second\n\nmessage", nil)
  39. msg2 := toMessage(t, response2.Body.String())
  40. require.NotEqual(t, msg1.ID, msg2.ID)
  41. require.NotEmpty(t, msg2.ID)
  42. require.Equal(t, "my second\n\nmessage", msg2.Message)
  43. response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  44. messages := toMessages(t, response.Body.String())
  45. require.Equal(t, 2, len(messages))
  46. require.Equal(t, "my first message", messages[0].Message)
  47. require.Equal(t, "my second\n\nmessage", messages[1].Message)
  48. response = request(t, s, "GET", "/mytopic/sse?poll=1&since=all", "", nil)
  49. lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n")
  50. require.Equal(t, 3, len(lines))
  51. require.Equal(t, "my first message", toMessage(t, strings.TrimPrefix(lines[0], "data: ")).Message)
  52. require.Equal(t, "", lines[1])
  53. require.Equal(t, "my second\n\nmessage", toMessage(t, strings.TrimPrefix(lines[2], "data: ")).Message)
  54. response = request(t, s, "GET", "/mytopic/raw?poll=1", "", nil)
  55. lines = strings.Split(strings.TrimSpace(response.Body.String()), "\n")
  56. require.Equal(t, 2, len(lines))
  57. require.Equal(t, "my first message", lines[0])
  58. require.Equal(t, "my second message", lines[1]) // \n -> " "
  59. }
  60. func TestServer_PublishWithFirebase(t *testing.T) {
  61. sender := newTestFirebaseSender(10)
  62. s := newTestServer(t, newTestConfig(t))
  63. s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true})
  64. response := request(t, s, "PUT", "/mytopic", "my first message", nil)
  65. msg1 := toMessage(t, response.Body.String())
  66. require.NotEmpty(t, msg1.ID)
  67. require.Equal(t, "my first message", msg1.Message)
  68. time.Sleep(100 * time.Millisecond) // Firebase publishing happens
  69. require.Equal(t, 1, len(sender.Messages()))
  70. require.Equal(t, "my first message", sender.Messages()[0].Data["message"])
  71. require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.Aps.Alert.Body)
  72. require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.CustomData["message"])
  73. }
  74. func TestServer_PublishWithoutFirebase(t *testing.T) {
  75. sender := newTestFirebaseSender(10)
  76. s := newTestServer(t, newTestConfig(t))
  77. s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true})
  78. response := request(t, s, "PUT", "/mytopic", "my first message", map[string]string{
  79. "firebase": "no",
  80. })
  81. msg1 := toMessage(t, response.Body.String())
  82. require.NotEmpty(t, msg1.ID)
  83. require.Equal(t, "my first message", msg1.Message)
  84. time.Sleep(100 * time.Millisecond) // Firebase publishing happens
  85. require.Equal(t, 0, len(sender.Messages()))
  86. }
  87. func TestServer_PublishWithFirebase_WithoutUsers_AndWithoutPanic(t *testing.T) {
  88. // This tests issue #641, which used to panic before the fix
  89. firebaseKeyFile := filepath.Join(t.TempDir(), "firebase.json")
  90. contents := `{
  91. "type": "service_account",
  92. "project_id": "ntfy-test",
  93. "private_key_id": "fsfhskjdfhskdhfskdjfhsdf",
  94. "private_key": "lalala",
  95. "client_email": "firebase-adminsdk-muv04@ntfy-test.iam.gserviceaccount.com",
  96. "client_id": "123123213",
  97. "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  98. "token_uri": "https://oauth2.googleapis.com/token",
  99. "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  100. "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-muv04%40ntfy-test.iam.gserviceaccount.com"
  101. }
  102. `
  103. require.Nil(t, os.WriteFile(firebaseKeyFile, []byte(contents), 0600))
  104. c := newTestConfig(t)
  105. c.FirebaseKeyFile = firebaseKeyFile
  106. s := newTestServer(t, c)
  107. response := request(t, s, "PUT", "/mytopic", "my first message", nil)
  108. require.Equal(t, "my first message", toMessage(t, response.Body.String()).Message)
  109. }
  110. func TestServer_SubscribeOpenAndKeepalive(t *testing.T) {
  111. t.Parallel()
  112. c := newTestConfig(t)
  113. c.KeepaliveInterval = time.Second
  114. s := newTestServer(t, c)
  115. rr := httptest.NewRecorder()
  116. ctx, cancel := context.WithCancel(context.Background())
  117. req, err := http.NewRequestWithContext(ctx, "GET", "/mytopic/json", nil)
  118. if err != nil {
  119. t.Fatal(err)
  120. }
  121. doneChan := make(chan bool)
  122. go func() {
  123. s.handle(rr, req)
  124. doneChan <- true
  125. }()
  126. time.Sleep(1300 * time.Millisecond)
  127. cancel()
  128. <-doneChan
  129. messages := toMessages(t, rr.Body.String())
  130. require.Equal(t, 2, len(messages))
  131. require.Equal(t, openEvent, messages[0].Event)
  132. require.Equal(t, "mytopic", messages[0].Topic)
  133. require.Equal(t, "", messages[0].Message)
  134. require.Equal(t, "", messages[0].Title)
  135. require.Equal(t, 0, messages[0].Priority)
  136. require.Nil(t, messages[0].Tags)
  137. require.Equal(t, keepaliveEvent, messages[1].Event)
  138. require.Equal(t, "mytopic", messages[1].Topic)
  139. require.Equal(t, "", messages[1].Message)
  140. require.Equal(t, "", messages[1].Title)
  141. require.Equal(t, 0, messages[1].Priority)
  142. require.Nil(t, messages[1].Tags)
  143. }
  144. func TestServer_PublishAndSubscribe(t *testing.T) {
  145. t.Parallel()
  146. s := newTestServer(t, newTestConfig(t))
  147. subscribeRR := httptest.NewRecorder()
  148. subscribeCancel := subscribe(t, s, "/mytopic/json", subscribeRR)
  149. publishFirstRR := request(t, s, "PUT", "/mytopic", "my first message", nil)
  150. require.Equal(t, 200, publishFirstRR.Code)
  151. time.Sleep(500 * time.Millisecond) // Publishing is done asynchronously, this avoids races
  152. publishSecondRR := request(t, s, "PUT", "/mytopic", "my other message", map[string]string{
  153. "Title": " This is a title ",
  154. "X-Tags": "tag1,tag 2, tag3",
  155. "p": "1",
  156. })
  157. require.Equal(t, 200, publishSecondRR.Code)
  158. subscribeCancel()
  159. messages := toMessages(t, subscribeRR.Body.String())
  160. require.Equal(t, 3, len(messages))
  161. require.Equal(t, openEvent, messages[0].Event)
  162. require.Equal(t, messageEvent, messages[1].Event)
  163. require.Equal(t, "mytopic", messages[1].Topic)
  164. require.Equal(t, "my first message", messages[1].Message)
  165. require.Equal(t, "", messages[1].Title)
  166. require.Equal(t, 0, messages[1].Priority)
  167. require.Nil(t, messages[1].Tags)
  168. require.True(t, time.Now().Add(12*time.Hour-5*time.Second).Unix() < messages[1].Expires)
  169. require.True(t, time.Now().Add(12*time.Hour+5*time.Second).Unix() > messages[1].Expires)
  170. require.Equal(t, messageEvent, messages[2].Event)
  171. require.Equal(t, "mytopic", messages[2].Topic)
  172. require.Equal(t, "my other message", messages[2].Message)
  173. require.Equal(t, "This is a title", messages[2].Title)
  174. require.Equal(t, 1, messages[2].Priority)
  175. require.Equal(t, []string{"tag1", "tag 2", "tag3"}, messages[2].Tags)
  176. }
  177. func TestServer_Publish_Disallowed_Topic(t *testing.T) {
  178. c := newTestConfig(t)
  179. c.DisallowedTopics = []string{"about", "time", "this", "got", "added"}
  180. s := newTestServer(t, c)
  181. rr := request(t, s, "PUT", "/mytopic", "my first message", nil)
  182. require.Equal(t, 200, rr.Code)
  183. rr = request(t, s, "PUT", "/about", "another message", nil)
  184. require.Equal(t, 400, rr.Code)
  185. require.Equal(t, 40010, toHTTPError(t, rr.Body.String()).Code)
  186. }
  187. func TestServer_StaticSites(t *testing.T) {
  188. s := newTestServer(t, newTestConfig(t))
  189. rr := request(t, s, "GET", "/", "", nil)
  190. require.Equal(t, 200, rr.Code)
  191. require.Contains(t, rr.Body.String(), "</html>")
  192. rr = request(t, s, "HEAD", "/", "", nil)
  193. require.Equal(t, 200, rr.Code)
  194. rr = request(t, s, "OPTIONS", "/", "", nil)
  195. require.Equal(t, 200, rr.Code)
  196. rr = request(t, s, "GET", "/does-not-exist.txt", "", nil)
  197. require.Equal(t, 404, rr.Code)
  198. rr = request(t, s, "GET", "/mytopic", "", nil)
  199. require.Equal(t, 200, rr.Code)
  200. require.Contains(t, rr.Body.String(), `<meta name="robots" content="noindex, nofollow" />`)
  201. rr = request(t, s, "GET", "/docs", "", nil)
  202. require.Equal(t, 301, rr.Code)
  203. // Docs test removed, it was failing annoyingly.
  204. }
  205. func TestServer_WebEnabled(t *testing.T) {
  206. conf := newTestConfig(t)
  207. conf.WebRoot = "" // Disable web app
  208. s := newTestServer(t, conf)
  209. rr := request(t, s, "GET", "/", "", nil)
  210. require.Equal(t, 404, rr.Code)
  211. rr = request(t, s, "GET", "/config.js", "", nil)
  212. require.Equal(t, 404, rr.Code)
  213. rr = request(t, s, "GET", "/sw.js", "", nil)
  214. require.Equal(t, 404, rr.Code)
  215. rr = request(t, s, "GET", "/app.html", "", nil)
  216. require.Equal(t, 404, rr.Code)
  217. rr = request(t, s, "GET", "/static/css/home.css", "", nil)
  218. require.Equal(t, 404, rr.Code)
  219. conf2 := newTestConfig(t)
  220. conf2.WebRoot = "/"
  221. s2 := newTestServer(t, conf2)
  222. rr = request(t, s2, "GET", "/", "", nil)
  223. require.Equal(t, 200, rr.Code)
  224. rr = request(t, s2, "GET", "/config.js", "", nil)
  225. require.Equal(t, 200, rr.Code)
  226. rr = request(t, s2, "GET", "/sw.js", "", nil)
  227. require.Equal(t, 200, rr.Code)
  228. rr = request(t, s2, "GET", "/app.html", "", nil)
  229. require.Equal(t, 200, rr.Code)
  230. }
  231. func TestServer_PublishLargeMessage(t *testing.T) {
  232. c := newTestConfig(t)
  233. c.AttachmentCacheDir = "" // Disable attachments
  234. s := newTestServer(t, c)
  235. body := strings.Repeat("this is a large message", 5000)
  236. response := request(t, s, "PUT", "/mytopic", body, nil)
  237. require.Equal(t, 400, response.Code)
  238. }
  239. func TestServer_PublishPriority(t *testing.T) {
  240. s := newTestServer(t, newTestConfig(t))
  241. for prio := 1; prio <= 5; prio++ {
  242. response := request(t, s, "GET", fmt.Sprintf("/mytopic/publish?priority=%d", prio), fmt.Sprintf("priority %d", prio), nil)
  243. msg := toMessage(t, response.Body.String())
  244. require.Equal(t, prio, msg.Priority)
  245. }
  246. response := request(t, s, "GET", "/mytopic/publish?priority=min", "test", nil)
  247. require.Equal(t, 1, toMessage(t, response.Body.String()).Priority)
  248. response = request(t, s, "GET", "/mytopic/send?priority=low", "test", nil)
  249. require.Equal(t, 2, toMessage(t, response.Body.String()).Priority)
  250. response = request(t, s, "GET", "/mytopic/send?priority=default", "test", nil)
  251. require.Equal(t, 3, toMessage(t, response.Body.String()).Priority)
  252. response = request(t, s, "GET", "/mytopic/send?priority=high", "test", nil)
  253. require.Equal(t, 4, toMessage(t, response.Body.String()).Priority)
  254. response = request(t, s, "GET", "/mytopic/send?priority=max", "test", nil)
  255. require.Equal(t, 5, toMessage(t, response.Body.String()).Priority)
  256. response = request(t, s, "GET", "/mytopic/trigger?priority=urgent", "test", nil)
  257. require.Equal(t, 5, toMessage(t, response.Body.String()).Priority)
  258. response = request(t, s, "GET", "/mytopic/trigger?priority=INVALID", "test", nil)
  259. require.Equal(t, 40007, toHTTPError(t, response.Body.String()).Code)
  260. }
  261. func TestServer_PublishPriority_SpecialHTTPHeader(t *testing.T) {
  262. s := newTestServer(t, newTestConfig(t))
  263. response := request(t, s, "POST", "/mytopic", "test", map[string]string{
  264. "Priority": "u=4",
  265. "X-Priority": "5",
  266. })
  267. require.Equal(t, 5, toMessage(t, response.Body.String()).Priority)
  268. response = request(t, s, "POST", "/mytopic?priority=4", "test", map[string]string{
  269. "Priority": "u=9",
  270. })
  271. require.Equal(t, 4, toMessage(t, response.Body.String()).Priority)
  272. response = request(t, s, "POST", "/mytopic", "test", map[string]string{
  273. "p": "2",
  274. "priority": "u=9, i",
  275. })
  276. require.Equal(t, 2, toMessage(t, response.Body.String()).Priority)
  277. }
  278. func TestServer_PublishGETOnlyOneTopic(t *testing.T) {
  279. // This tests a bug that allowed publishing topics with a comma in the name (no ticket)
  280. s := newTestServer(t, newTestConfig(t))
  281. response := request(t, s, "GET", "/mytopic,mytopic2/publish?m=hi", "", nil)
  282. require.Equal(t, 404, response.Code)
  283. }
  284. func TestServer_PublishNoCache(t *testing.T) {
  285. s := newTestServer(t, newTestConfig(t))
  286. response := request(t, s, "PUT", "/mytopic", "this message is not cached", map[string]string{
  287. "Cache": "no",
  288. })
  289. msg := toMessage(t, response.Body.String())
  290. require.NotEmpty(t, msg.ID)
  291. require.Equal(t, "this message is not cached", msg.Message)
  292. require.Equal(t, int64(0), msg.Expires)
  293. response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  294. messages := toMessages(t, response.Body.String())
  295. require.Empty(t, messages)
  296. }
  297. func TestServer_PublishAt(t *testing.T) {
  298. t.Parallel()
  299. s := newTestServer(t, newTestConfig(t))
  300. response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{
  301. "In": "1h",
  302. })
  303. require.Equal(t, 200, response.Code)
  304. response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  305. messages := toMessages(t, response.Body.String())
  306. require.Equal(t, 0, len(messages))
  307. // Update message time to the past
  308. fakeTime := time.Now().Add(-10 * time.Second).Unix()
  309. _, err := s.messageCache.db.Exec(`UPDATE messages SET time=?`, fakeTime)
  310. require.Nil(t, err)
  311. // Trigger delayed message sending
  312. require.Nil(t, s.sendDelayedMessages())
  313. response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  314. messages = toMessages(t, response.Body.String())
  315. require.Equal(t, 1, len(messages))
  316. require.Equal(t, "a message", messages[0].Message)
  317. require.Equal(t, netip.Addr{}, messages[0].Sender) // Never return the sender!
  318. messages, err = s.messageCache.Messages("mytopic", sinceAllMessages, true)
  319. require.Nil(t, err)
  320. require.Equal(t, 1, len(messages))
  321. require.Equal(t, "a message", messages[0].Message)
  322. require.Equal(t, "9.9.9.9", messages[0].Sender.String()) // It's stored in the DB though!
  323. }
  324. func TestServer_PublishAt_FromUser(t *testing.T) {
  325. t.Parallel()
  326. s := newTestServer(t, newTestConfigWithAuthFile(t))
  327. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
  328. response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{
  329. "Authorization": util.BasicAuth("phil", "phil"),
  330. "In": "1h",
  331. })
  332. require.Equal(t, 200, response.Code)
  333. // Message doesn't show up immediately
  334. response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  335. messages := toMessages(t, response.Body.String())
  336. require.Equal(t, 0, len(messages))
  337. // Update message time to the past
  338. fakeTime := time.Now().Add(-10 * time.Second).Unix()
  339. _, err := s.messageCache.db.Exec(`UPDATE messages SET time=?`, fakeTime)
  340. require.Nil(t, err)
  341. // Trigger delayed message sending
  342. require.Nil(t, s.sendDelayedMessages())
  343. response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  344. messages = toMessages(t, response.Body.String())
  345. require.Equal(t, 1, len(messages))
  346. require.Equal(t, fakeTime, messages[0].Time)
  347. require.Equal(t, "a message", messages[0].Message)
  348. messages, err = s.messageCache.Messages("mytopic", sinceAllMessages, true)
  349. require.Nil(t, err)
  350. require.Equal(t, 1, len(messages))
  351. require.Equal(t, "a message", messages[0].Message)
  352. require.True(t, strings.HasPrefix(messages[0].User, "u_"))
  353. }
  354. func TestServer_PublishAt_Expires(t *testing.T) {
  355. s := newTestServer(t, newTestConfig(t))
  356. response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{
  357. "In": "2 days",
  358. })
  359. require.Equal(t, 200, response.Code)
  360. m := toMessage(t, response.Body.String())
  361. require.True(t, m.Expires > time.Now().Add(12*time.Hour+48*time.Hour-time.Minute).Unix())
  362. require.True(t, m.Expires < time.Now().Add(12*time.Hour+48*time.Hour+time.Minute).Unix())
  363. }
  364. func TestServer_PublishAtWithCacheError(t *testing.T) {
  365. s := newTestServer(t, newTestConfig(t))
  366. response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{
  367. "Cache": "no",
  368. "In": "30 min",
  369. })
  370. require.Equal(t, 400, response.Code)
  371. require.Equal(t, errHTTPBadRequestDelayNoCache, toHTTPError(t, response.Body.String()))
  372. }
  373. func TestServer_PublishAtTooShortDelay(t *testing.T) {
  374. s := newTestServer(t, newTestConfig(t))
  375. response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{
  376. "In": "1s",
  377. })
  378. require.Equal(t, 400, response.Code)
  379. }
  380. func TestServer_PublishAtTooLongDelay(t *testing.T) {
  381. s := newTestServer(t, newTestConfig(t))
  382. response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{
  383. "In": "99999999h",
  384. })
  385. require.Equal(t, 400, response.Code)
  386. }
  387. func TestServer_PublishAtInvalidDelay(t *testing.T) {
  388. s := newTestServer(t, newTestConfig(t))
  389. response := request(t, s, "PUT", "/mytopic?delay=INVALID", "a message", nil)
  390. err := toHTTPError(t, response.Body.String())
  391. require.Equal(t, 400, response.Code)
  392. require.Equal(t, 40004, err.Code)
  393. }
  394. func TestServer_PublishAtTooLarge(t *testing.T) {
  395. s := newTestServer(t, newTestConfig(t))
  396. response := request(t, s, "PUT", "/mytopic?x-in=99999h", "a message", nil)
  397. err := toHTTPError(t, response.Body.String())
  398. require.Equal(t, 400, response.Code)
  399. require.Equal(t, 40006, err.Code)
  400. }
  401. func TestServer_PublishAtAndPrune(t *testing.T) {
  402. s := newTestServer(t, newTestConfig(t))
  403. response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{
  404. "In": "1h",
  405. })
  406. require.Equal(t, 200, response.Code)
  407. s.execManager() // Fire pruning
  408. response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil)
  409. messages := toMessages(t, response.Body.String())
  410. require.Equal(t, 1, len(messages)) // Not affected by pruning
  411. require.Equal(t, "a message", messages[0].Message)
  412. time.Sleep(time.Second) // FIXME CI failing not sure why
  413. }
  414. func TestServer_PublishAndMultiPoll(t *testing.T) {
  415. s := newTestServer(t, newTestConfig(t))
  416. response := request(t, s, "PUT", "/mytopic1", "message 1", nil)
  417. msg := toMessage(t, response.Body.String())
  418. require.NotEmpty(t, msg.ID)
  419. require.Equal(t, "mytopic1", msg.Topic)
  420. require.Equal(t, "message 1", msg.Message)
  421. response = request(t, s, "PUT", "/mytopic2", "message 2", nil)
  422. msg = toMessage(t, response.Body.String())
  423. require.NotEmpty(t, msg.ID)
  424. require.Equal(t, "mytopic2", msg.Topic)
  425. require.Equal(t, "message 2", msg.Message)
  426. response = request(t, s, "GET", "/mytopic1/json?poll=1", "", nil)
  427. messages := toMessages(t, response.Body.String())
  428. require.Equal(t, 1, len(messages))
  429. require.Equal(t, "mytopic1", messages[0].Topic)
  430. require.Equal(t, "message 1", messages[0].Message)
  431. response = request(t, s, "GET", "/mytopic1,mytopic2/json?poll=1", "", nil)
  432. messages = toMessages(t, response.Body.String())
  433. require.Equal(t, 2, len(messages))
  434. require.Equal(t, "mytopic1", messages[0].Topic)
  435. require.Equal(t, "message 1", messages[0].Message)
  436. require.Equal(t, "mytopic2", messages[1].Topic)
  437. require.Equal(t, "message 2", messages[1].Message)
  438. }
  439. func TestServer_PublishWithNopCache(t *testing.T) {
  440. c := newTestConfig(t)
  441. c.CacheDuration = 0
  442. s := newTestServer(t, c)
  443. subscribeRR := httptest.NewRecorder()
  444. subscribeCancel := subscribe(t, s, "/mytopic/json", subscribeRR)
  445. publishRR := request(t, s, "PUT", "/mytopic", "my first message", nil)
  446. require.Equal(t, 200, publishRR.Code)
  447. subscribeCancel()
  448. messages := toMessages(t, subscribeRR.Body.String())
  449. require.Equal(t, 2, len(messages))
  450. require.Equal(t, openEvent, messages[0].Event)
  451. require.Equal(t, messageEvent, messages[1].Event)
  452. require.Equal(t, "my first message", messages[1].Message)
  453. response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  454. messages = toMessages(t, response.Body.String())
  455. require.Empty(t, messages)
  456. }
  457. func TestServer_PublishAndPollSince(t *testing.T) {
  458. t.Parallel()
  459. s := newTestServer(t, newTestConfig(t))
  460. request(t, s, "PUT", "/mytopic", "test 1", nil)
  461. time.Sleep(1100 * time.Millisecond)
  462. since := time.Now().Unix()
  463. request(t, s, "PUT", "/mytopic", "test 2", nil)
  464. response := request(t, s, "GET", fmt.Sprintf("/mytopic/json?poll=1&since=%d", since), "", nil)
  465. messages := toMessages(t, response.Body.String())
  466. require.Equal(t, 1, len(messages))
  467. require.Equal(t, "test 2", messages[0].Message)
  468. response = request(t, s, "GET", "/mytopic/json?poll=1&since=10s", "", nil)
  469. messages = toMessages(t, response.Body.String())
  470. require.Equal(t, 2, len(messages))
  471. require.Equal(t, "test 1", messages[0].Message)
  472. response = request(t, s, "GET", "/mytopic/json?poll=1&since=100ms", "", nil)
  473. messages = toMessages(t, response.Body.String())
  474. require.Equal(t, 1, len(messages))
  475. require.Equal(t, "test 2", messages[0].Message)
  476. response = request(t, s, "GET", "/mytopic/json?poll=1&since=latest", "", nil)
  477. messages = toMessages(t, response.Body.String())
  478. require.Equal(t, 1, len(messages))
  479. require.Equal(t, "test 2", messages[0].Message)
  480. response = request(t, s, "GET", "/mytopic/json?poll=1&since=INVALID", "", nil)
  481. require.Equal(t, 40008, toHTTPError(t, response.Body.String()).Code)
  482. }
  483. func newMessageWithTimestamp(topic, message string, timestamp int64) *message {
  484. m := newDefaultMessage(topic, message)
  485. m.Time = timestamp
  486. return m
  487. }
  488. func TestServer_PollSinceID_MultipleTopics(t *testing.T) {
  489. s := newTestServer(t, newTestConfig(t))
  490. require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 1", 1655740277)))
  491. markerMessage := newMessageWithTimestamp("mytopic2", "test 2", 1655740283)
  492. require.Nil(t, s.messageCache.AddMessage(markerMessage))
  493. require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 3", 1655740289)))
  494. require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 4", 1655740293)))
  495. require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 5", 1655740297)))
  496. require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 6", 1655740303)))
  497. response := request(t, s, "GET", fmt.Sprintf("/mytopic1,mytopic2/json?poll=1&since=%s", markerMessage.ID), "", nil)
  498. messages := toMessages(t, response.Body.String())
  499. require.Equal(t, 4, len(messages))
  500. require.Equal(t, "test 3", messages[0].Message)
  501. require.Equal(t, "mytopic1", messages[0].Topic)
  502. require.Equal(t, "test 4", messages[1].Message)
  503. require.Equal(t, "mytopic2", messages[1].Topic)
  504. require.Equal(t, "test 5", messages[2].Message)
  505. require.Equal(t, "mytopic1", messages[2].Topic)
  506. require.Equal(t, "test 6", messages[3].Message)
  507. require.Equal(t, "mytopic2", messages[3].Topic)
  508. }
  509. func TestServer_PollSinceID_MultipleTopics_IDDoesNotMatch(t *testing.T) {
  510. s := newTestServer(t, newTestConfig(t))
  511. require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 3", 1655740289)))
  512. require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 4", 1655740293)))
  513. require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 5", 1655740297)))
  514. require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 6", 1655740303)))
  515. response := request(t, s, "GET", "/mytopic1,mytopic2/json?poll=1&since=NoMatchForID", "", nil)
  516. messages := toMessages(t, response.Body.String())
  517. require.Equal(t, 4, len(messages))
  518. require.Equal(t, "test 3", messages[0].Message)
  519. require.Equal(t, "test 4", messages[1].Message)
  520. require.Equal(t, "test 5", messages[2].Message)
  521. require.Equal(t, "test 6", messages[3].Message)
  522. }
  523. func TestServer_PublishViaGET(t *testing.T) {
  524. s := newTestServer(t, newTestConfig(t))
  525. response := request(t, s, "GET", "/mytopic/trigger", "", nil)
  526. msg := toMessage(t, response.Body.String())
  527. require.NotEmpty(t, msg.ID)
  528. require.Equal(t, "triggered", msg.Message)
  529. response = request(t, s, "GET", "/mytopic/send?message=This+is+a+test&t=This+is+a+title&tags=skull&x-priority=5&delay=24h", "", nil)
  530. msg = toMessage(t, response.Body.String())
  531. require.NotEmpty(t, msg.ID)
  532. require.Equal(t, "This is a test", msg.Message)
  533. require.Equal(t, "This is a title", msg.Title)
  534. require.Equal(t, []string{"skull"}, msg.Tags)
  535. require.Equal(t, 5, msg.Priority)
  536. require.Greater(t, msg.Time, time.Now().Add(23*time.Hour).Unix())
  537. }
  538. func TestServer_PublishMessageInHeaderWithNewlines(t *testing.T) {
  539. s := newTestServer(t, newTestConfig(t))
  540. response := request(t, s, "PUT", "/mytopic", "", map[string]string{
  541. "Message": "Line 1\\nLine 2",
  542. })
  543. msg := toMessage(t, response.Body.String())
  544. require.NotEmpty(t, msg.ID)
  545. require.Equal(t, "Line 1\nLine 2", msg.Message) // \\n -> \n !
  546. }
  547. func TestServer_PublishInvalidTopic(t *testing.T) {
  548. s := newTestServer(t, newTestConfig(t))
  549. s.smtpSender = &testMailer{}
  550. response := request(t, s, "PUT", "/docs", "fail", nil)
  551. require.Equal(t, 40010, toHTTPError(t, response.Body.String()).Code)
  552. }
  553. func TestServer_PublishWithSIDInPath(t *testing.T) {
  554. s := newTestServer(t, newTestConfig(t))
  555. response := request(t, s, "POST", "/mytopic/sid", "message", nil)
  556. msg := toMessage(t, response.Body.String())
  557. require.NotEmpty(t, msg.ID)
  558. require.Equal(t, "sid", msg.SequenceID)
  559. }
  560. func TestServer_PublishWithSIDInHeader(t *testing.T) {
  561. s := newTestServer(t, newTestConfig(t))
  562. response := request(t, s, "POST", "/mytopic", "message", map[string]string{
  563. "sid": "sid",
  564. })
  565. msg := toMessage(t, response.Body.String())
  566. require.NotEmpty(t, msg.ID)
  567. require.Equal(t, "sid", msg.SequenceID)
  568. }
  569. func TestServer_PublishWithSIDInPathAndHeader(t *testing.T) {
  570. s := newTestServer(t, newTestConfig(t))
  571. response := request(t, s, "PUT", "/mytopic/sid1", "message", map[string]string{
  572. "sid": "sid2",
  573. })
  574. msg := toMessage(t, response.Body.String())
  575. require.NotEmpty(t, msg.ID)
  576. require.Equal(t, "sid1", msg.SequenceID) // Sequence ID in path has priority over header
  577. }
  578. func TestServer_PublishWithSIDInQuery(t *testing.T) {
  579. s := newTestServer(t, newTestConfig(t))
  580. response := request(t, s, "PUT", "/mytopic?sid=sid1", "message", nil)
  581. msg := toMessage(t, response.Body.String())
  582. require.NotEmpty(t, msg.ID)
  583. require.Equal(t, "sid1", msg.SequenceID)
  584. }
  585. func TestServer_PublishWithSIDViaGet(t *testing.T) {
  586. s := newTestServer(t, newTestConfig(t))
  587. response := request(t, s, "GET", "/mytopic/publish?sid=sid1", "message", nil)
  588. msg := toMessage(t, response.Body.String())
  589. require.NotEmpty(t, msg.ID)
  590. require.Equal(t, "sid1", msg.SequenceID)
  591. }
  592. func TestServer_PublishAsJSON_WithSequenceID(t *testing.T) {
  593. s := newTestServer(t, newTestConfig(t))
  594. body := `{"topic":"mytopic","message":"A message","sequence_id":"my-sequence-123"}`
  595. response := request(t, s, "PUT", "/", body, nil)
  596. require.Equal(t, 200, response.Code)
  597. msg := toMessage(t, response.Body.String())
  598. require.NotEmpty(t, msg.ID)
  599. require.Equal(t, "my-sequence-123", msg.SequenceID)
  600. }
  601. func TestServer_PublishWithInvalidSIDInPath(t *testing.T) {
  602. s := newTestServer(t, newTestConfig(t))
  603. response := request(t, s, "POST", "/mytopic/.", "message", nil)
  604. require.Equal(t, 404, response.Code)
  605. }
  606. func TestServer_PublishWithInvalidSIDInHeader(t *testing.T) {
  607. s := newTestServer(t, newTestConfig(t))
  608. response := request(t, s, "POST", "/mytopic", "message", map[string]string{
  609. "X-Sequence-ID": "*&?",
  610. })
  611. require.Equal(t, 400, response.Code)
  612. require.Equal(t, 40049, toHTTPError(t, response.Body.String()).Code)
  613. }
  614. func TestServer_PollWithQueryFilters(t *testing.T) {
  615. s := newTestServer(t, newTestConfig(t))
  616. response := request(t, s, "PUT", "/mytopic?priority=1&tags=tag1,tag2", "my first message", nil)
  617. msg := toMessage(t, response.Body.String())
  618. require.NotEmpty(t, msg.ID)
  619. response = request(t, s, "PUT", "/mytopic?title=a+title", "my second message", map[string]string{
  620. "Tags": "tag2,tag3",
  621. })
  622. msg = toMessage(t, response.Body.String())
  623. require.NotEmpty(t, msg.ID)
  624. queriesThatShouldReturnMessageOne := []string{
  625. "/mytopic/json?poll=1&priority=1",
  626. "/mytopic/json?poll=1&priority=min",
  627. "/mytopic/json?poll=1&priority=min,low",
  628. "/mytopic/json?poll=1&priority=1,2",
  629. "/mytopic/json?poll=1&p=2,min",
  630. "/mytopic/json?poll=1&tags=tag1",
  631. "/mytopic/json?poll=1&tags=tag1,tag2",
  632. "/mytopic/json?poll=1&message=my+first+message",
  633. }
  634. for _, query := range queriesThatShouldReturnMessageOne {
  635. response = request(t, s, "GET", query, "", nil)
  636. messages := toMessages(t, response.Body.String())
  637. require.Equal(t, 1, len(messages), "Query failed: "+query)
  638. require.Equal(t, "my first message", messages[0].Message, "Query failed: "+query)
  639. }
  640. queriesThatShouldReturnMessageTwo := []string{
  641. "/mytopic/json?poll=1&x-priority=3", // !
  642. "/mytopic/json?poll=1&priority=3",
  643. "/mytopic/json?poll=1&priority=default",
  644. "/mytopic/json?poll=1&p=3",
  645. "/mytopic/json?poll=1&x-tags=tag2,tag3",
  646. "/mytopic/json?poll=1&tags=tag2,tag3",
  647. "/mytopic/json?poll=1&tag=tag2,tag3",
  648. "/mytopic/json?poll=1&ta=tag2,tag3",
  649. "/mytopic/json?poll=1&x-title=a+title",
  650. "/mytopic/json?poll=1&title=a+title",
  651. "/mytopic/json?poll=1&t=a+title",
  652. "/mytopic/json?poll=1&x-message=my+second+message",
  653. "/mytopic/json?poll=1&message=my+second+message",
  654. "/mytopic/json?poll=1&m=my+second+message",
  655. "/mytopic/json?x-poll=1&m=my+second+message",
  656. "/mytopic/json?po=1&m=my+second+message",
  657. }
  658. for _, query := range queriesThatShouldReturnMessageTwo {
  659. response = request(t, s, "GET", query, "", nil)
  660. messages := toMessages(t, response.Body.String())
  661. require.Equal(t, 1, len(messages), "Query failed: "+query)
  662. require.Equal(t, "my second message", messages[0].Message, "Query failed: "+query)
  663. }
  664. queriesThatShouldReturnNoMessages := []string{
  665. "/mytopic/json?poll=1&priority=4",
  666. "/mytopic/json?poll=1&tags=tag1,tag2,tag3",
  667. "/mytopic/json?poll=1&title=another+title",
  668. "/mytopic/json?poll=1&message=my+third+message",
  669. "/mytopic/json?poll=1&message=my+third+message",
  670. }
  671. for _, query := range queriesThatShouldReturnNoMessages {
  672. response = request(t, s, "GET", query, "", nil)
  673. messages := toMessages(t, response.Body.String())
  674. require.Equal(t, 0, len(messages), "Query failed: "+query)
  675. }
  676. }
  677. func TestServer_SubscribeWithQueryFilters(t *testing.T) {
  678. t.Parallel()
  679. c := newTestConfig(t)
  680. c.KeepaliveInterval = 800 * time.Millisecond
  681. s := newTestServer(t, c)
  682. subscribeResponse := httptest.NewRecorder()
  683. subscribeCancel := subscribe(t, s, "/mytopic/json?tags=zfs-issue", subscribeResponse)
  684. response := request(t, s, "PUT", "/mytopic", "my first message", nil)
  685. require.Equal(t, 200, response.Code)
  686. response = request(t, s, "PUT", "/mytopic", "ZFS scrub failed", map[string]string{
  687. "Tags": "zfs-issue,zfs-scrub",
  688. })
  689. require.Equal(t, 200, response.Code)
  690. time.Sleep(850 * time.Millisecond)
  691. subscribeCancel()
  692. messages := toMessages(t, subscribeResponse.Body.String())
  693. require.Equal(t, 3, len(messages))
  694. require.Equal(t, openEvent, messages[0].Event)
  695. require.Equal(t, messageEvent, messages[1].Event)
  696. require.Equal(t, "ZFS scrub failed", messages[1].Message)
  697. require.Equal(t, keepaliveEvent, messages[2].Event)
  698. }
  699. func TestServer_Auth_Success_Admin(t *testing.T) {
  700. c := newTestConfigWithAuthFile(t)
  701. s := newTestServer(t, c)
  702. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
  703. response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
  704. "Authorization": util.BasicAuth("phil", "phil"),
  705. })
  706. require.Equal(t, 200, response.Code)
  707. require.Equal(t, `{"success":true}`+"\n", response.Body.String())
  708. }
  709. func TestServer_Auth_Success_User(t *testing.T) {
  710. c := newTestConfigWithAuthFile(t)
  711. c.AuthDefault = user.PermissionDenyAll
  712. s := newTestServer(t, c)
  713. require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
  714. require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", user.PermissionReadWrite))
  715. response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
  716. "Authorization": util.BasicAuth("ben", "ben"),
  717. })
  718. require.Equal(t, 200, response.Code)
  719. }
  720. func TestServer_Auth_Success_User_MultipleTopics(t *testing.T) {
  721. c := newTestConfigWithAuthFile(t)
  722. c.AuthDefault = user.PermissionDenyAll
  723. s := newTestServer(t, c)
  724. require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
  725. require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", user.PermissionReadWrite))
  726. require.Nil(t, s.userManager.AllowAccess("ben", "anothertopic", user.PermissionReadWrite))
  727. response := request(t, s, "GET", "/mytopic,anothertopic/auth", "", map[string]string{
  728. "Authorization": util.BasicAuth("ben", "ben"),
  729. })
  730. require.Equal(t, 200, response.Code)
  731. response = request(t, s, "GET", "/mytopic,anothertopic,NOT-THIS-ONE/auth", "", map[string]string{
  732. "Authorization": util.BasicAuth("ben", "ben"),
  733. })
  734. require.Equal(t, 403, response.Code)
  735. }
  736. func TestServer_Auth_Fail_InvalidPass(t *testing.T) {
  737. c := newTestConfig(t)
  738. c.AuthFile = filepath.Join(t.TempDir(), "user.db")
  739. c.AuthDefault = user.PermissionDenyAll
  740. s := newTestServer(t, c)
  741. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
  742. response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
  743. "Authorization": util.BasicAuth("phil", "INVALID"),
  744. })
  745. require.Equal(t, 401, response.Code)
  746. }
  747. func TestServer_Auth_Fail_Unauthorized(t *testing.T) {
  748. c := newTestConfigWithAuthFile(t)
  749. c.AuthDefault = user.PermissionDenyAll
  750. s := newTestServer(t, c)
  751. require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
  752. require.Nil(t, s.userManager.AllowAccess("ben", "sometopic", user.PermissionReadWrite)) // Not mytopic!
  753. response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
  754. "Authorization": util.BasicAuth("ben", "ben"),
  755. })
  756. require.Equal(t, 403, response.Code)
  757. }
  758. func TestServer_Auth_Fail_CannotPublish(t *testing.T) {
  759. c := newTestConfigWithAuthFile(t)
  760. c.AuthDefault = user.PermissionReadWrite // Open by default
  761. s := newTestServer(t, c)
  762. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
  763. require.Nil(t, s.userManager.AllowAccess(user.Everyone, "private", user.PermissionDenyAll))
  764. require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", user.PermissionRead))
  765. response := request(t, s, "PUT", "/mytopic", "test", nil)
  766. require.Equal(t, 200, response.Code)
  767. response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  768. require.Equal(t, 200, response.Code)
  769. response = request(t, s, "PUT", "/announcements", "test", nil)
  770. require.Equal(t, 403, response.Code) // Cannot write as anonymous
  771. response = request(t, s, "PUT", "/announcements", "test", map[string]string{
  772. "Authorization": util.BasicAuth("phil", "phil"),
  773. })
  774. require.Equal(t, 200, response.Code)
  775. response = request(t, s, "GET", "/announcements/json?poll=1", "", nil)
  776. require.Equal(t, 200, response.Code) // Anonymous read allowed
  777. response = request(t, s, "GET", "/private/json?poll=1", "", nil)
  778. require.Equal(t, 403, response.Code) // Anonymous read not allowed
  779. }
  780. func TestServer_Auth_Fail_Rate_Limiting(t *testing.T) {
  781. c := newTestConfigWithAuthFile(t)
  782. c.VisitorAuthFailureLimitBurst = 10
  783. s := newTestServer(t, c)
  784. for i := 0; i < 10; i++ {
  785. response := request(t, s, "PUT", "/announcements", "test", map[string]string{
  786. "Authorization": util.BasicAuth("phil", "phil"),
  787. })
  788. require.Equal(t, 401, response.Code)
  789. }
  790. response := request(t, s, "PUT", "/announcements", "test", map[string]string{
  791. "Authorization": util.BasicAuth("phil", "phil"),
  792. })
  793. require.Equal(t, 429, response.Code)
  794. require.Equal(t, 42909, toHTTPError(t, response.Body.String()).Code)
  795. }
  796. func TestServer_Auth_ViaQuery(t *testing.T) {
  797. c := newTestConfigWithAuthFile(t)
  798. c.AuthDefault = user.PermissionDenyAll
  799. s := newTestServer(t, c)
  800. require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin, false))
  801. u := fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(util.BasicAuth("ben", "some pass"))))
  802. response := request(t, s, "GET", u, "", nil)
  803. require.Equal(t, 200, response.Code)
  804. u = fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(util.BasicAuth("ben", "WRONNNGGGG"))))
  805. response = request(t, s, "GET", u, "", nil)
  806. require.Equal(t, 401, response.Code)
  807. }
  808. func TestServer_Auth_NonBasicHeader(t *testing.T) {
  809. s := newTestServer(t, newTestConfigWithAuthFile(t))
  810. response := request(t, s, "PUT", "/mytopic", "test", map[string]string{
  811. "Authorization": "WebPush not-supported",
  812. })
  813. require.Equal(t, 200, response.Code)
  814. response = request(t, s, "PUT", "/mytopic", "test", map[string]string{
  815. "Authorization": "Bearer supported",
  816. })
  817. require.Equal(t, 401, response.Code)
  818. response = request(t, s, "PUT", "/mytopic", "test", map[string]string{
  819. "Authorization": "basic supported",
  820. })
  821. require.Equal(t, 401, response.Code)
  822. }
  823. func TestServer_StatsResetter(t *testing.T) {
  824. t.Parallel()
  825. // This tests the stats resetter for
  826. // - an anonymous user
  827. // - a user without a tier (treated like the same as the anonymous user)
  828. // - a user with a tier
  829. c := newTestConfigWithAuthFile(t)
  830. c.VisitorStatsResetTime = time.Now().Add(2 * time.Second)
  831. s := newTestServer(t, c)
  832. go s.runStatsResetter()
  833. // Create user with tier (tieruser) and user without tier (phil)
  834. require.Nil(t, s.userManager.AddTier(&user.Tier{
  835. Code: "test",
  836. MessageLimit: 5,
  837. MessageExpiryDuration: -5 * time.Second, // Second, what a hack!
  838. }))
  839. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
  840. require.Nil(t, s.userManager.AddUser("tieruser", "tieruser", user.RoleUser, false))
  841. require.Nil(t, s.userManager.ChangeTier("tieruser", "test"))
  842. // Send an anonymous message
  843. response := request(t, s, "PUT", "/mytopic", "test", nil)
  844. require.Equal(t, 200, response.Code)
  845. // Send messages from user without tier (phil)
  846. for i := 0; i < 5; i++ {
  847. response := request(t, s, "PUT", "/mytopic", "test", map[string]string{
  848. "Authorization": util.BasicAuth("phil", "phil"),
  849. })
  850. require.Equal(t, 200, response.Code)
  851. }
  852. // Send messages from user with tier
  853. for i := 0; i < 2; i++ {
  854. response := request(t, s, "PUT", "/mytopic", "test", map[string]string{
  855. "Authorization": util.BasicAuth("tieruser", "tieruser"),
  856. })
  857. require.Equal(t, 200, response.Code)
  858. }
  859. // User stats show 6 messages (for user without tier)
  860. response = request(t, s, "GET", "/v1/account", "", map[string]string{
  861. "Authorization": util.BasicAuth("phil", "phil"),
  862. })
  863. require.Equal(t, 200, response.Code)
  864. account, err := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body))
  865. require.Nil(t, err)
  866. require.Equal(t, int64(6), account.Stats.Messages)
  867. // User stats show 6 messages (for anonymous visitor)
  868. response = request(t, s, "GET", "/v1/account", "", nil)
  869. require.Equal(t, 200, response.Code)
  870. account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body))
  871. require.Nil(t, err)
  872. require.Equal(t, int64(6), account.Stats.Messages)
  873. // User stats show 2 messages (for user with tier)
  874. response = request(t, s, "GET", "/v1/account", "", map[string]string{
  875. "Authorization": util.BasicAuth("tieruser", "tieruser"),
  876. })
  877. require.Equal(t, 200, response.Code)
  878. account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body))
  879. require.Nil(t, err)
  880. require.Equal(t, int64(2), account.Stats.Messages)
  881. // Wait for stats resetter to run
  882. waitFor(t, func() bool {
  883. response = request(t, s, "GET", "/v1/account", "", map[string]string{
  884. "Authorization": util.BasicAuth("phil", "phil"),
  885. })
  886. require.Equal(t, 200, response.Code)
  887. account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body))
  888. require.Nil(t, err)
  889. return account.Stats.Messages == 0
  890. })
  891. // User stats show 0 messages now!
  892. response = request(t, s, "GET", "/v1/account", "", map[string]string{
  893. "Authorization": util.BasicAuth("phil", "phil"),
  894. })
  895. require.Equal(t, 200, response.Code)
  896. account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body))
  897. require.Nil(t, err)
  898. require.Equal(t, int64(0), account.Stats.Messages)
  899. // Since this is a user without a tier, the anonymous user should have the same stats
  900. response = request(t, s, "GET", "/v1/account", "", nil)
  901. require.Equal(t, 200, response.Code)
  902. account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body))
  903. require.Nil(t, err)
  904. require.Equal(t, int64(0), account.Stats.Messages)
  905. // User stats show 0 messages (for user with tier)
  906. response = request(t, s, "GET", "/v1/account", "", map[string]string{
  907. "Authorization": util.BasicAuth("tieruser", "tieruser"),
  908. })
  909. require.Equal(t, 200, response.Code)
  910. account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body))
  911. require.Nil(t, err)
  912. require.Equal(t, int64(0), account.Stats.Messages)
  913. }
  914. func TestServer_StatsResetter_MessageLimiter_EmailsLimiter(t *testing.T) {
  915. // This tests that the messageLimiter (the only fixed limiter) and the emailsLimiter (token bucket)
  916. // is reset by the stats resetter
  917. c := newTestConfigWithAuthFile(t)
  918. s := newTestServer(t, c)
  919. s.smtpSender = &testMailer{}
  920. // Publish some messages, and check stats
  921. for i := 0; i < 3; i++ {
  922. response := request(t, s, "PUT", "/mytopic", "test", nil)
  923. require.Equal(t, 200, response.Code)
  924. }
  925. response := request(t, s, "PUT", "/mytopic", "test", map[string]string{
  926. "Email": "test@email.com",
  927. })
  928. require.Equal(t, 200, response.Code)
  929. rr := request(t, s, "GET", "/v1/account", "", nil)
  930. require.Equal(t, 200, rr.Code)
  931. account, err := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body))
  932. require.Nil(t, err)
  933. require.Equal(t, int64(4), account.Stats.Messages)
  934. require.Equal(t, int64(1), account.Stats.Emails)
  935. v := s.visitor(netip.MustParseAddr("9.9.9.9"), nil)
  936. require.Equal(t, int64(4), v.Stats().Messages)
  937. require.Equal(t, int64(4), v.messagesLimiter.Value())
  938. require.Equal(t, int64(1), v.Stats().Emails)
  939. require.Equal(t, int64(1), v.emailsLimiter.Value())
  940. // Reset stats and check again
  941. s.resetStats()
  942. rr = request(t, s, "GET", "/v1/account", "", nil)
  943. require.Equal(t, 200, rr.Code)
  944. account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body))
  945. require.Nil(t, err)
  946. require.Equal(t, int64(0), account.Stats.Messages)
  947. require.Equal(t, int64(0), account.Stats.Emails)
  948. v = s.visitor(netip.MustParseAddr("9.9.9.9"), nil)
  949. require.Equal(t, int64(0), v.Stats().Messages)
  950. require.Equal(t, int64(0), v.messagesLimiter.Value())
  951. require.Equal(t, int64(0), v.Stats().Emails)
  952. require.Equal(t, int64(0), v.emailsLimiter.Value())
  953. }
  954. func TestServer_DailyMessageQuotaFromDatabase(t *testing.T) {
  955. t.Parallel()
  956. // This tests that the daily message quota is prefilled originally from the database,
  957. // if the visitor is unknown
  958. c := newTestConfigWithAuthFile(t)
  959. c.AuthStatsQueueWriterInterval = 100 * time.Millisecond
  960. s := newTestServer(t, c)
  961. // Create user, and update it with some message and email stats
  962. require.Nil(t, s.userManager.AddTier(&user.Tier{
  963. Code: "test",
  964. }))
  965. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
  966. require.Nil(t, s.userManager.ChangeTier("phil", "test"))
  967. u, err := s.userManager.User("phil")
  968. require.Nil(t, err)
  969. s.userManager.EnqueueUserStats(u.ID, &user.Stats{
  970. Messages: 123456,
  971. Emails: 999,
  972. })
  973. time.Sleep(400 * time.Millisecond)
  974. // Get account and verify stats are read from the DB, and that the visitor also has these stats
  975. rr := request(t, s, "GET", "/v1/account", "", map[string]string{
  976. "Authorization": util.BasicAuth("phil", "phil"),
  977. })
  978. require.Equal(t, 200, rr.Code)
  979. account, err := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body))
  980. require.Nil(t, err)
  981. require.Equal(t, int64(123456), account.Stats.Messages)
  982. require.Equal(t, int64(999), account.Stats.Emails)
  983. v := s.visitor(netip.MustParseAddr("9.9.9.9"), u)
  984. require.Equal(t, int64(123456), v.Stats().Messages)
  985. require.Equal(t, int64(123456), v.messagesLimiter.Value())
  986. require.Equal(t, int64(999), v.Stats().Emails)
  987. require.Equal(t, int64(999), v.emailsLimiter.Value())
  988. }
  989. type testMailer struct {
  990. count int
  991. mu sync.Mutex
  992. }
  993. func (t *testMailer) Send(v *visitor, m *message, to string) error {
  994. t.mu.Lock()
  995. defer t.mu.Unlock()
  996. t.count++
  997. return nil
  998. }
  999. func (t *testMailer) Counts() (total int64, success int64, failure int64) {
  1000. return 0, 0, 0
  1001. }
  1002. func (t *testMailer) Count() int {
  1003. t.mu.Lock()
  1004. defer t.mu.Unlock()
  1005. return t.count
  1006. }
  1007. func TestServer_PublishTooManyRequests_Defaults(t *testing.T) {
  1008. s := newTestServer(t, newTestConfig(t))
  1009. for i := 0; i < 60; i++ {
  1010. response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil)
  1011. require.Equal(t, 200, response.Code)
  1012. }
  1013. response := request(t, s, "PUT", "/mytopic", "message", nil)
  1014. require.Equal(t, 429, response.Code)
  1015. }
  1016. func TestServer_PublishTooManyRequests_Defaults_IPv6(t *testing.T) {
  1017. s := newTestServer(t, newTestConfig(t))
  1018. overrideRemoteAddr1 := func(r *http.Request) {
  1019. r.RemoteAddr = "[2001:db8:9999:8888:1::1]:1234"
  1020. }
  1021. overrideRemoteAddr2 := func(r *http.Request) {
  1022. r.RemoteAddr = "[2001:db8:9999:8888:2::1]:1234" // Same /64
  1023. }
  1024. for i := 0; i < 30; i++ {
  1025. response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr1)
  1026. require.Equal(t, 200, response.Code)
  1027. }
  1028. for i := 0; i < 30; i++ {
  1029. response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr2)
  1030. require.Equal(t, 200, response.Code)
  1031. }
  1032. response := request(t, s, "PUT", "/mytopic", "message", nil, overrideRemoteAddr1)
  1033. require.Equal(t, 429, response.Code)
  1034. }
  1035. func TestServer_PublishTooManyRequests_IPv6_Slash48(t *testing.T) {
  1036. c := newTestConfig(t)
  1037. c.VisitorRequestLimitBurst = 6
  1038. c.VisitorPrefixBitsIPv6 = 48 // Use /48 for IPv6 prefixes
  1039. s := newTestServer(t, c)
  1040. overrideRemoteAddr1 := func(r *http.Request) {
  1041. r.RemoteAddr = "[2001:db8:9999::1]:1234"
  1042. }
  1043. overrideRemoteAddr2 := func(r *http.Request) {
  1044. r.RemoteAddr = "[2001:db8:9999::2]:1234" // Same /48
  1045. }
  1046. for i := 0; i < 3; i++ {
  1047. response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr1)
  1048. require.Equal(t, 200, response.Code)
  1049. }
  1050. for i := 0; i < 3; i++ {
  1051. response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr2)
  1052. require.Equal(t, 200, response.Code)
  1053. }
  1054. response := request(t, s, "PUT", "/mytopic", "message", nil, overrideRemoteAddr1)
  1055. require.Equal(t, 429, response.Code)
  1056. }
  1057. func TestServer_PublishTooManyRequests_Defaults_ExemptHosts(t *testing.T) {
  1058. c := newTestConfig(t)
  1059. c.VisitorRequestLimitBurst = 3
  1060. c.VisitorRequestExemptPrefixes = []netip.Prefix{netip.MustParsePrefix("9.9.9.9/32")} // see request()
  1061. s := newTestServer(t, c)
  1062. for i := 0; i < 5; i++ { // > 3
  1063. response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil)
  1064. require.Equal(t, 200, response.Code)
  1065. }
  1066. }
  1067. func TestServer_PublishTooManyRequests_Defaults_ExemptHosts_IPv6(t *testing.T) {
  1068. c := newTestConfig(t)
  1069. c.VisitorRequestLimitBurst = 3
  1070. c.VisitorRequestExemptPrefixes = []netip.Prefix{netip.MustParsePrefix("2001:db8:9999::/48")}
  1071. s := newTestServer(t, c)
  1072. overrideRemoteAddr := func(r *http.Request) {
  1073. r.RemoteAddr = "[2001:db8:9999::1]:1234"
  1074. }
  1075. for i := 0; i < 5; i++ { // > 3
  1076. response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr)
  1077. require.Equal(t, 200, response.Code)
  1078. }
  1079. }
  1080. func TestServer_PublishTooManyRequests_Defaults_ExemptHosts_MessageDailyLimit(t *testing.T) {
  1081. c := newTestConfig(t)
  1082. c.VisitorRequestLimitBurst = 10
  1083. c.VisitorMessageDailyLimit = 4
  1084. c.VisitorRequestExemptPrefixes = []netip.Prefix{netip.MustParsePrefix("9.9.9.9/32")} // see request()
  1085. s := newTestServer(t, c)
  1086. for i := 0; i < 8; i++ { // 4
  1087. response := request(t, s, "PUT", "/mytopic", "message", nil)
  1088. require.Equal(t, 200, response.Code)
  1089. }
  1090. }
  1091. func TestServer_PublishTooManyRequests_ShortReplenish(t *testing.T) {
  1092. t.Parallel()
  1093. c := newTestConfig(t)
  1094. c.VisitorRequestLimitBurst = 60
  1095. c.VisitorRequestLimitReplenish = time.Second
  1096. s := newTestServer(t, c)
  1097. for i := 0; i < 60; i++ {
  1098. response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil)
  1099. require.Equal(t, 200, response.Code)
  1100. }
  1101. response := request(t, s, "PUT", "/mytopic", "message", nil)
  1102. require.Equal(t, 429, response.Code)
  1103. time.Sleep(1020 * time.Millisecond)
  1104. response = request(t, s, "PUT", "/mytopic", "message", nil)
  1105. require.Equal(t, 200, response.Code)
  1106. }
  1107. func TestServer_PublishTooManyEmails_Defaults(t *testing.T) {
  1108. s := newTestServer(t, newTestConfig(t))
  1109. s.smtpSender = &testMailer{}
  1110. for i := 0; i < 16; i++ {
  1111. response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), map[string]string{
  1112. "E-Mail": "test@example.com",
  1113. })
  1114. require.Equal(t, 200, response.Code)
  1115. }
  1116. response := request(t, s, "PUT", "/mytopic", "one too many", map[string]string{
  1117. "E-Mail": "test@example.com",
  1118. })
  1119. require.Equal(t, 429, response.Code)
  1120. }
  1121. func TestServer_PublishTooManyEmails_Replenish(t *testing.T) {
  1122. t.Parallel()
  1123. c := newTestConfig(t)
  1124. c.VisitorEmailLimitReplenish = 500 * time.Millisecond
  1125. s := newTestServer(t, c)
  1126. s.smtpSender = &testMailer{}
  1127. for i := 0; i < 16; i++ {
  1128. response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), map[string]string{
  1129. "E-Mail": "test@example.com",
  1130. })
  1131. require.Equal(t, 200, response.Code)
  1132. }
  1133. response := request(t, s, "PUT", "/mytopic", "one too many", map[string]string{
  1134. "E-Mail": "test@example.com",
  1135. })
  1136. require.Equal(t, 429, response.Code)
  1137. time.Sleep(510 * time.Millisecond)
  1138. response = request(t, s, "PUT", "/mytopic", "this should be okay again too many", map[string]string{
  1139. "E-Mail": "test@example.com",
  1140. })
  1141. require.Equal(t, 200, response.Code)
  1142. response = request(t, s, "PUT", "/mytopic", "and bad again", map[string]string{
  1143. "E-Mail": "test@example.com",
  1144. })
  1145. require.Equal(t, 429, response.Code)
  1146. }
  1147. func TestServer_PublishDelayedEmail_Fail(t *testing.T) {
  1148. s := newTestServer(t, newTestConfig(t))
  1149. s.smtpSender = &testMailer{}
  1150. response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{
  1151. "E-Mail": "test@example.com",
  1152. "Delay": "20 min",
  1153. })
  1154. require.Equal(t, 40003, toHTTPError(t, response.Body.String()).Code)
  1155. }
  1156. func TestServer_PublishDelayedCall_Fail(t *testing.T) {
  1157. c := newTestConfigWithAuthFile(t)
  1158. c.TwilioAccount = "AC1234567890"
  1159. c.TwilioAuthToken = "AAEAA1234567890"
  1160. c.TwilioPhoneNumber = "+1234567890"
  1161. s := newTestServer(t, c)
  1162. response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{
  1163. "Call": "yes",
  1164. "Delay": "20 min",
  1165. })
  1166. require.Equal(t, 40037, toHTTPError(t, response.Body.String()).Code)
  1167. }
  1168. func TestServer_PublishEmailNoMailer_Fail(t *testing.T) {
  1169. s := newTestServer(t, newTestConfig(t))
  1170. response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{
  1171. "E-Mail": "test@example.com",
  1172. })
  1173. require.Equal(t, 400, response.Code)
  1174. }
  1175. func TestServer_PublishAndExpungeTopicAfter16Hours(t *testing.T) {
  1176. t.Parallel()
  1177. s := newTestServer(t, newTestConfig(t))
  1178. defer s.messageCache.Close()
  1179. subFn := func(v *visitor, msg *message) error {
  1180. return nil
  1181. }
  1182. // Publish and check last access
  1183. response := request(t, s, "POST", "/mytopic", "test", map[string]string{
  1184. "Cache": "no",
  1185. })
  1186. require.Equal(t, 200, response.Code)
  1187. waitFor(t, func() bool {
  1188. s.mu.Lock()
  1189. tp, exists := s.topics["mytopic"]
  1190. s.mu.Unlock()
  1191. if !exists {
  1192. return false
  1193. }
  1194. // .lastAccess set in t.Publish() -> t.Keepalive() in Goroutine
  1195. tp.mu.RLock()
  1196. defer tp.mu.RUnlock()
  1197. return tp.lastAccess.Unix() >= time.Now().Unix()-2 &&
  1198. tp.lastAccess.Unix() <= time.Now().Unix()+2
  1199. })
  1200. // Hack!
  1201. time.Sleep(time.Second)
  1202. // Topic won't get pruned
  1203. s.execManager()
  1204. require.NotNil(t, s.topics["mytopic"])
  1205. // Fudge with last access, but subscribe, and see that it won't get pruned (because of subscriber)
  1206. subID := s.topics["mytopic"].Subscribe(subFn, "", func() {})
  1207. s.topics["mytopic"].mu.Lock()
  1208. s.topics["mytopic"].lastAccess = time.Now().Add(-17 * time.Hour)
  1209. s.topics["mytopic"].mu.Unlock()
  1210. s.execManager()
  1211. require.NotNil(t, s.topics["mytopic"])
  1212. // It'll finally get pruned now that there are no subscribers and last access is 17 hours ago
  1213. s.topics["mytopic"].Unsubscribe(subID)
  1214. s.execManager()
  1215. require.Nil(t, s.topics["mytopic"])
  1216. }
  1217. func TestServer_TopicKeepaliveOnPoll(t *testing.T) {
  1218. t.Parallel()
  1219. s := newTestServer(t, newTestConfig(t))
  1220. // Create topic by polling once
  1221. response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  1222. require.Equal(t, 200, response.Code)
  1223. // Mess with last access time
  1224. s.topics["mytopic"].lastAccess = time.Now().Add(-17 * time.Hour)
  1225. // Poll again and check keepalive time
  1226. response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  1227. require.Equal(t, 200, response.Code)
  1228. require.True(t, s.topics["mytopic"].lastAccess.Unix() >= time.Now().Unix()-2)
  1229. require.True(t, s.topics["mytopic"].lastAccess.Unix() <= time.Now().Unix()+2)
  1230. }
  1231. func TestServer_UnifiedPushDiscovery(t *testing.T) {
  1232. s := newTestServer(t, newTestConfig(t))
  1233. response := request(t, s, "GET", "/mytopic?up=1", "", nil)
  1234. require.Equal(t, 200, response.Code)
  1235. require.Equal(t, `{"unifiedpush":{"version":1}}`+"\n", response.Body.String())
  1236. }
  1237. func TestServer_PublishUnifiedPushBinary_AndPoll(t *testing.T) {
  1238. b := make([]byte, 12) // Max length
  1239. _, err := rand.Read(b)
  1240. require.Nil(t, err)
  1241. s := newTestServer(t, newTestConfig(t))
  1242. // Register a UnifiedPush subscriber
  1243. response := request(t, s, "GET", "/up123456789012/json?poll=1", "", nil)
  1244. require.Equal(t, 200, response.Code)
  1245. // Publish message to topic
  1246. response = request(t, s, "PUT", "/up123456789012?up=1", string(b), nil)
  1247. require.Equal(t, 200, response.Code)
  1248. m := toMessage(t, response.Body.String())
  1249. require.Equal(t, "base64", m.Encoding)
  1250. b2, err := base64.StdEncoding.DecodeString(m.Message)
  1251. require.Nil(t, err)
  1252. require.Equal(t, b, b2)
  1253. // Retrieve and check published message
  1254. response = request(t, s, "GET", "/up123456789012/json?poll=1", string(b), nil)
  1255. require.Equal(t, 200, response.Code)
  1256. m = toMessage(t, response.Body.String())
  1257. require.Equal(t, "base64", m.Encoding)
  1258. b2, err = base64.StdEncoding.DecodeString(m.Message)
  1259. require.Nil(t, err)
  1260. require.Equal(t, b, b2)
  1261. }
  1262. func TestServer_PublishUnifiedPushBinary_Truncated(t *testing.T) {
  1263. b := make([]byte, 5000) // Longer than max length
  1264. _, err := rand.Read(b)
  1265. require.Nil(t, err)
  1266. s := newTestServer(t, newTestConfig(t))
  1267. // Register a UnifiedPush subscriber
  1268. response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  1269. require.Equal(t, 200, response.Code)
  1270. // Publish message to topic
  1271. response = request(t, s, "PUT", "/mytopic?up=1", string(b), nil)
  1272. require.Equal(t, 200, response.Code)
  1273. m := toMessage(t, response.Body.String())
  1274. require.Equal(t, "base64", m.Encoding)
  1275. b2, err := base64.StdEncoding.DecodeString(m.Message)
  1276. require.Nil(t, err)
  1277. require.Equal(t, 4096, len(b2))
  1278. require.Equal(t, b[:4096], b2)
  1279. }
  1280. func TestServer_PublishUnifiedPushText(t *testing.T) {
  1281. s := newTestServer(t, newTestConfig(t))
  1282. // Register a UnifiedPush subscriber
  1283. response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  1284. require.Equal(t, 200, response.Code)
  1285. // Publish UnifiedPush text message
  1286. response = request(t, s, "PUT", "/mytopic?up=1", "this is a unifiedpush text message", nil)
  1287. require.Equal(t, 200, response.Code)
  1288. m := toMessage(t, response.Body.String())
  1289. require.Equal(t, "", m.Encoding)
  1290. require.Equal(t, "this is a unifiedpush text message", m.Message)
  1291. }
  1292. func TestServer_MatrixGateway_Discovery_Success(t *testing.T) {
  1293. s := newTestServer(t, newTestConfig(t))
  1294. response := request(t, s, "GET", "/_matrix/push/v1/notify", "", nil)
  1295. require.Equal(t, 200, response.Code)
  1296. require.Equal(t, `{"unifiedpush":{"gateway":"matrix"}}`+"\n", response.Body.String())
  1297. }
  1298. func TestServer_MatrixGateway_Discovery_Failure_Unconfigured(t *testing.T) {
  1299. c := newTestConfig(t)
  1300. c.BaseURL = ""
  1301. s := newTestServer(t, c)
  1302. response := request(t, s, "GET", "/_matrix/push/v1/notify", "", nil)
  1303. require.Equal(t, 500, response.Code)
  1304. err := toHTTPError(t, response.Body.String())
  1305. require.Equal(t, 50003, err.Code)
  1306. }
  1307. func TestServer_MatrixGateway_Push_Success(t *testing.T) {
  1308. s := newTestServer(t, newTestConfig(t))
  1309. response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  1310. require.Equal(t, 200, response.Code)
  1311. notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}`
  1312. response = request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil)
  1313. require.Equal(t, 200, response.Code)
  1314. require.Equal(t, `{"rejected":[]}`+"\n", response.Body.String())
  1315. response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  1316. require.Equal(t, 200, response.Code)
  1317. m := toMessage(t, response.Body.String())
  1318. require.Equal(t, notification, m.Message)
  1319. }
  1320. func TestServer_MatrixGateway_Push_Failure_NoSubscriber(t *testing.T) {
  1321. c := newTestConfig(t)
  1322. c.VisitorSubscriberRateLimiting = true
  1323. s := newTestServer(t, c)
  1324. notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}`
  1325. response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil)
  1326. require.Equal(t, 507, response.Code)
  1327. require.Equal(t, 50701, toHTTPError(t, response.Body.String()).Code)
  1328. }
  1329. func TestServer_MatrixGateway_Push_Failure_NoSubscriber_After13Hours(t *testing.T) {
  1330. c := newTestConfig(t)
  1331. c.VisitorSubscriberRateLimiting = true
  1332. s := newTestServer(t, c)
  1333. notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}`
  1334. // No success if no rate visitor set (this also creates the topic in memory)
  1335. response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil)
  1336. require.Equal(t, 507, response.Code)
  1337. require.Equal(t, 50701, toHTTPError(t, response.Body.String()).Code)
  1338. require.Nil(t, s.topics["mytopic"].rateVisitor)
  1339. // Fake: This topic has been around for 13 hours without a rate visitor
  1340. s.topics["mytopic"].lastAccess = time.Now().Add(-13 * time.Hour)
  1341. // Same request should now return HTTP 200 with a rejected pushkey
  1342. response = request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil)
  1343. require.Equal(t, 200, response.Code)
  1344. require.Equal(t, `{"rejected":["http://127.0.0.1:12345/mytopic?up=1"]}`, strings.TrimSpace(response.Body.String()))
  1345. // Slightly unrelated: Test that topic is pruned after 16 hours
  1346. s.topics["mytopic"].lastAccess = time.Now().Add(-17 * time.Hour)
  1347. s.execManager()
  1348. require.Nil(t, s.topics["mytopic"])
  1349. }
  1350. func TestServer_MatrixGateway_Push_Failure_InvalidPushkey(t *testing.T) {
  1351. s := newTestServer(t, newTestConfig(t))
  1352. notification := `{"notification":{"devices":[{"pushkey":"http://wrong-base-url.com/mytopic?up=1"}]}}`
  1353. response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil)
  1354. require.Equal(t, 200, response.Code)
  1355. require.Equal(t, `{"rejected":["http://wrong-base-url.com/mytopic?up=1"]}`+"\n", response.Body.String())
  1356. response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  1357. require.Equal(t, 200, response.Code)
  1358. require.Equal(t, "", response.Body.String()) // Empty!
  1359. }
  1360. func TestServer_MatrixGateway_Push_Failure_EverythingIsWrong(t *testing.T) {
  1361. s := newTestServer(t, newTestConfig(t))
  1362. notification := `{"message":"this is not really a Matrix message"}`
  1363. response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil)
  1364. require.Equal(t, 400, response.Code)
  1365. require.Equal(t, 40019, toHTTPError(t, response.Body.String()).Code)
  1366. notification = `this isn't even JSON'`
  1367. response = request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil)
  1368. require.Equal(t, 400, response.Code)
  1369. require.Equal(t, 40019, toHTTPError(t, response.Body.String()).Code)
  1370. }
  1371. func TestServer_MatrixGateway_Push_Failure_Unconfigured(t *testing.T) {
  1372. c := newTestConfig(t)
  1373. c.BaseURL = ""
  1374. s := newTestServer(t, c)
  1375. notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}`
  1376. response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil)
  1377. require.Equal(t, 500, response.Code)
  1378. require.Equal(t, 50003, toHTTPError(t, response.Body.String()).Code)
  1379. }
  1380. func TestServer_PublishActions_AndPoll(t *testing.T) {
  1381. s := newTestServer(t, newTestConfig(t))
  1382. response := request(t, s, "PUT", "/mytopic", "my message", map[string]string{
  1383. "Actions": "view, Open portal, https://home.nest.com/; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65",
  1384. })
  1385. require.Equal(t, 200, response.Code)
  1386. response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  1387. require.Equal(t, 200, response.Code)
  1388. m := toMessage(t, response.Body.String())
  1389. require.Equal(t, 2, len(m.Actions))
  1390. require.Equal(t, "view", m.Actions[0].Action)
  1391. require.Equal(t, "Open portal", m.Actions[0].Label)
  1392. require.Equal(t, "https://home.nest.com/", m.Actions[0].URL)
  1393. require.Equal(t, "http", m.Actions[1].Action)
  1394. require.Equal(t, "Turn down", m.Actions[1].Label)
  1395. require.Equal(t, "https://api.nest.com/device/XZ1D2", m.Actions[1].URL)
  1396. require.Equal(t, "target_temp_f=65", m.Actions[1].Body)
  1397. }
  1398. func TestServer_PublishMarkdown(t *testing.T) {
  1399. s := newTestServer(t, newTestConfig(t))
  1400. response := request(t, s, "PUT", "/mytopic", "**make this bold**", map[string]string{
  1401. "Content-Type": "text/markdown",
  1402. })
  1403. require.Equal(t, 200, response.Code)
  1404. m := toMessage(t, response.Body.String())
  1405. require.Equal(t, "**make this bold**", m.Message)
  1406. require.Equal(t, "text/markdown", m.ContentType)
  1407. }
  1408. func TestServer_PublishMarkdown_QueryParam(t *testing.T) {
  1409. s := newTestServer(t, newTestConfig(t))
  1410. response := request(t, s, "PUT", "/mytopic?md=1", "**make this bold**", nil)
  1411. require.Equal(t, 200, response.Code)
  1412. m := toMessage(t, response.Body.String())
  1413. require.Equal(t, "**make this bold**", m.Message)
  1414. require.Equal(t, "text/markdown", m.ContentType)
  1415. }
  1416. func TestServer_PublishMarkdown_NotMarkdown(t *testing.T) {
  1417. s := newTestServer(t, newTestConfig(t))
  1418. response := request(t, s, "PUT", "/mytopic", "**make this bold**", map[string]string{
  1419. "Content-Type": "not-markdown",
  1420. })
  1421. require.Equal(t, 200, response.Code)
  1422. m := toMessage(t, response.Body.String())
  1423. require.Equal(t, "", m.ContentType)
  1424. }
  1425. func TestServer_PublishAsJSON(t *testing.T) {
  1426. s := newTestServer(t, newTestConfig(t))
  1427. body := `{"topic":"mytopic","message":"A message","title":"a title\nwith lines","tags":["tag1","tag 2"],` +
  1428. `"not-a-thing":"ok", "attach":"http://google.com","filename":"google.pdf", "click":"http://ntfy.sh","priority":4,` +
  1429. `"icon":"https://ntfy.sh/static/img/ntfy.png", "delay":"30min"}`
  1430. response := request(t, s, "PUT", "/", body, nil)
  1431. require.Equal(t, 200, response.Code)
  1432. m := toMessage(t, response.Body.String())
  1433. require.Equal(t, "mytopic", m.Topic)
  1434. require.Equal(t, "A message", m.Message)
  1435. require.Equal(t, "a title\nwith lines", m.Title)
  1436. require.Equal(t, []string{"tag1", "tag 2"}, m.Tags)
  1437. require.Equal(t, "http://google.com", m.Attachment.URL)
  1438. require.Equal(t, "google.pdf", m.Attachment.Name)
  1439. require.Equal(t, "http://ntfy.sh", m.Click)
  1440. require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon)
  1441. require.Equal(t, "", m.ContentType)
  1442. require.Equal(t, 4, m.Priority)
  1443. require.True(t, m.Time > time.Now().Unix()+29*60)
  1444. require.True(t, m.Time < time.Now().Unix()+31*60)
  1445. }
  1446. func TestServer_PublishAsJSON_Markdown(t *testing.T) {
  1447. s := newTestServer(t, newTestConfig(t))
  1448. body := `{"topic":"mytopic","message":"**This is bold**","markdown":true}`
  1449. response := request(t, s, "PUT", "/", body, nil)
  1450. require.Equal(t, 200, response.Code)
  1451. m := toMessage(t, response.Body.String())
  1452. require.Equal(t, "mytopic", m.Topic)
  1453. require.Equal(t, "**This is bold**", m.Message)
  1454. require.Equal(t, "text/markdown", m.ContentType)
  1455. }
  1456. func TestServer_PublishAsJSON_RateLimit_MessageDailyLimit(t *testing.T) {
  1457. // Publishing as JSON follows a different path. This ensures that rate
  1458. // limiting works for this endpoint as well
  1459. c := newTestConfig(t)
  1460. c.VisitorMessageDailyLimit = 3
  1461. s := newTestServer(t, c)
  1462. for i := 0; i < 3; i++ {
  1463. response := request(t, s, "PUT", "/", `{"topic":"mytopic","message":"A message"}`, nil)
  1464. require.Equal(t, 200, response.Code)
  1465. }
  1466. response := request(t, s, "PUT", "/", `{"topic":"mytopic","message":"A message"}`, nil)
  1467. require.Equal(t, 429, response.Code)
  1468. require.Equal(t, 42908, toHTTPError(t, response.Body.String()).Code)
  1469. }
  1470. func TestServer_PublishAsJSON_WithEmail(t *testing.T) {
  1471. t.Parallel()
  1472. mailer := &testMailer{}
  1473. s := newTestServer(t, newTestConfig(t))
  1474. s.smtpSender = mailer
  1475. body := `{"topic":"mytopic","message":"A message","email":"phil@example.com"}`
  1476. response := request(t, s, "PUT", "/", body, nil)
  1477. require.Equal(t, 200, response.Code)
  1478. time.Sleep(100 * time.Millisecond) // E-Mail publishing happens in a Go routine
  1479. m := toMessage(t, response.Body.String())
  1480. require.Equal(t, "mytopic", m.Topic)
  1481. require.Equal(t, "A message", m.Message)
  1482. require.Equal(t, 1, mailer.Count())
  1483. }
  1484. func TestServer_PublishAsJSON_WithActions(t *testing.T) {
  1485. s := newTestServer(t, newTestConfig(t))
  1486. body := `{
  1487. "topic":"mytopic",
  1488. "message":"A message",
  1489. "actions": [
  1490. {
  1491. "action": "view",
  1492. "label": "Open portal",
  1493. "url": "https://home.nest.com/"
  1494. },
  1495. {
  1496. "action": "http",
  1497. "label": "Turn down",
  1498. "url": "https://api.nest.com/device/XZ1D2",
  1499. "body": "target_temp_f=65"
  1500. }
  1501. ]
  1502. }`
  1503. response := request(t, s, "POST", "/", body, nil)
  1504. require.Equal(t, 200, response.Code)
  1505. m := toMessage(t, response.Body.String())
  1506. require.Equal(t, "mytopic", m.Topic)
  1507. require.Equal(t, "A message", m.Message)
  1508. require.Equal(t, 2, len(m.Actions))
  1509. require.Equal(t, "view", m.Actions[0].Action)
  1510. require.Equal(t, "Open portal", m.Actions[0].Label)
  1511. require.Equal(t, "https://home.nest.com/", m.Actions[0].URL)
  1512. require.Equal(t, "http", m.Actions[1].Action)
  1513. require.Equal(t, "Turn down", m.Actions[1].Label)
  1514. require.Equal(t, "https://api.nest.com/device/XZ1D2", m.Actions[1].URL)
  1515. require.Equal(t, "target_temp_f=65", m.Actions[1].Body)
  1516. }
  1517. func TestServer_PublishAsJSON_NoCache(t *testing.T) {
  1518. s := newTestServer(t, newTestConfig(t))
  1519. body := `{"topic":"mytopic","message": "this message is not cached","cache":"no"}`
  1520. response := request(t, s, "PUT", "/", body, nil)
  1521. msg := toMessage(t, response.Body.String())
  1522. require.NotEmpty(t, msg.ID)
  1523. require.Equal(t, "this message is not cached", msg.Message)
  1524. require.Equal(t, int64(0), msg.Expires)
  1525. response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  1526. messages := toMessages(t, response.Body.String())
  1527. require.Empty(t, messages)
  1528. }
  1529. func TestServer_PublishAsJSON_WithoutFirebase(t *testing.T) {
  1530. sender := newTestFirebaseSender(10)
  1531. s := newTestServer(t, newTestConfig(t))
  1532. s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true})
  1533. body := `{"topic":"mytopic","message": "my first message","firebase":"no"}`
  1534. response := request(t, s, "PUT", "/", body, nil)
  1535. msg1 := toMessage(t, response.Body.String())
  1536. require.NotEmpty(t, msg1.ID)
  1537. require.Equal(t, "my first message", msg1.Message)
  1538. time.Sleep(100 * time.Millisecond) // Firebase publishing happens
  1539. require.Equal(t, 0, len(sender.Messages()))
  1540. }
  1541. func TestServer_PublishAsJSON_Invalid(t *testing.T) {
  1542. s := newTestServer(t, newTestConfig(t))
  1543. body := `{"topic":"mytopic",INVALID`
  1544. response := request(t, s, "PUT", "/", body, nil)
  1545. require.Equal(t, 400, response.Code)
  1546. }
  1547. func TestServer_PublishWithTierBasedMessageLimitAndExpiry(t *testing.T) {
  1548. c := newTestConfigWithAuthFile(t)
  1549. s := newTestServer(t, c)
  1550. // Create tier with certain limits
  1551. require.Nil(t, s.userManager.AddTier(&user.Tier{
  1552. Code: "test",
  1553. MessageLimit: 5,
  1554. MessageExpiryDuration: -5 * time.Second, // Second, what a hack!
  1555. }))
  1556. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
  1557. require.Nil(t, s.userManager.ChangeTier("phil", "test"))
  1558. // Publish to reach message limit
  1559. for i := 0; i < 5; i++ {
  1560. response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("this is message %d", i+1), map[string]string{
  1561. "Authorization": util.BasicAuth("phil", "phil"),
  1562. })
  1563. require.Equal(t, 200, response.Code)
  1564. msg := toMessage(t, response.Body.String())
  1565. require.True(t, msg.Expires < time.Now().Unix()+5)
  1566. }
  1567. response := request(t, s, "PUT", "/mytopic", "this is too much", map[string]string{
  1568. "Authorization": util.BasicAuth("phil", "phil"),
  1569. })
  1570. require.Equal(t, 429, response.Code)
  1571. // Run pruning and see if they are gone
  1572. s.execManager()
  1573. response = request(t, s, "GET", "/mytopic/json?poll=1", "", map[string]string{
  1574. "Authorization": util.BasicAuth("phil", "phil"),
  1575. })
  1576. require.Equal(t, 200, response.Code)
  1577. require.Empty(t, response.Body)
  1578. }
  1579. func TestServer_PublishAttachment(t *testing.T) {
  1580. content := "text file!" + util.RandomString(4990) // > 4096
  1581. s := newTestServer(t, newTestConfig(t))
  1582. response := request(t, s, "PUT", "/mytopic", content, nil)
  1583. msg := toMessage(t, response.Body.String())
  1584. require.Equal(t, "attachment.txt", msg.Attachment.Name)
  1585. require.Equal(t, "text/plain; charset=utf-8", msg.Attachment.Type)
  1586. require.Equal(t, int64(5000), msg.Attachment.Size)
  1587. require.GreaterOrEqual(t, msg.Attachment.Expires, time.Now().Add(179*time.Minute).Unix()) // Almost 3 hours
  1588. require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
  1589. require.Equal(t, netip.Addr{}, msg.Sender) // Should never be returned
  1590. require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID))
  1591. // GET
  1592. path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345")
  1593. response = request(t, s, "GET", path, "", nil)
  1594. require.Equal(t, 200, response.Code)
  1595. require.Equal(t, "5000", response.Header().Get("Content-Length"))
  1596. require.Equal(t, content, response.Body.String())
  1597. // HEAD
  1598. response = request(t, s, "HEAD", path, "", nil)
  1599. require.Equal(t, 200, response.Code)
  1600. require.Equal(t, "5000", response.Header().Get("Content-Length"))
  1601. require.Equal(t, "", response.Body.String())
  1602. // Slightly unrelated cross-test: make sure we add an owner for internal attachments
  1603. size, err := s.messageCache.AttachmentBytesUsedBySender("9.9.9.9") // See request()
  1604. require.Nil(t, err)
  1605. require.Equal(t, int64(5000), size)
  1606. }
  1607. func TestServer_PublishAttachmentShortWithFilename(t *testing.T) {
  1608. c := newTestConfig(t)
  1609. c.BehindProxy = true
  1610. s := newTestServer(t, c)
  1611. content := "this is an ATTACHMENT"
  1612. response := request(t, s, "PUT", "/mytopic?f=myfile.txt", content, map[string]string{
  1613. "X-Forwarded-For": "1.2.3.4",
  1614. })
  1615. msg := toMessage(t, response.Body.String())
  1616. require.Equal(t, "myfile.txt", msg.Attachment.Name)
  1617. require.Equal(t, "text/plain; charset=utf-8", msg.Attachment.Type)
  1618. require.Equal(t, int64(21), msg.Attachment.Size)
  1619. require.GreaterOrEqual(t, msg.Attachment.Expires, time.Now().Add(3*time.Hour).Unix())
  1620. require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
  1621. require.Equal(t, netip.Addr{}, msg.Sender) // Should never be returned
  1622. require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID))
  1623. path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345")
  1624. response = request(t, s, "GET", path, "", nil)
  1625. require.Equal(t, 200, response.Code)
  1626. require.Equal(t, "21", response.Header().Get("Content-Length"))
  1627. require.Equal(t, content, response.Body.String())
  1628. // Slightly unrelated cross-test: make sure we add an owner for internal attachments
  1629. size, err := s.messageCache.AttachmentBytesUsedBySender("1.2.3.4")
  1630. require.Nil(t, err)
  1631. require.Equal(t, int64(21), size)
  1632. }
  1633. func TestServer_PublishAttachmentExternalWithoutFilename(t *testing.T) {
  1634. s := newTestServer(t, newTestConfig(t))
  1635. response := request(t, s, "PUT", "/mytopic", "", map[string]string{
  1636. "Attach": "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg",
  1637. })
  1638. msg := toMessage(t, response.Body.String())
  1639. require.Equal(t, "You received a file: Pink_flower.jpg", msg.Message)
  1640. require.Equal(t, "Pink_flower.jpg", msg.Attachment.Name)
  1641. require.Equal(t, "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg", msg.Attachment.URL)
  1642. require.Equal(t, "", msg.Attachment.Type)
  1643. require.Equal(t, int64(0), msg.Attachment.Size)
  1644. require.Equal(t, int64(0), msg.Attachment.Expires)
  1645. require.Equal(t, netip.Addr{}, msg.Sender)
  1646. // Slightly unrelated cross-test: make sure we don't add an owner for external attachments
  1647. size, err := s.messageCache.AttachmentBytesUsedBySender("127.0.0.1")
  1648. require.Nil(t, err)
  1649. require.Equal(t, int64(0), size)
  1650. }
  1651. func TestServer_PublishAttachmentExternalWithFilename(t *testing.T) {
  1652. s := newTestServer(t, newTestConfig(t))
  1653. response := request(t, s, "PUT", "/mytopic", "This is a custom message", map[string]string{
  1654. "X-Attach": "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg",
  1655. "File": "some file.jpg",
  1656. })
  1657. msg := toMessage(t, response.Body.String())
  1658. require.Equal(t, "This is a custom message", msg.Message)
  1659. require.Equal(t, "some file.jpg", msg.Attachment.Name)
  1660. require.Equal(t, "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg", msg.Attachment.URL)
  1661. require.Equal(t, "", msg.Attachment.Type)
  1662. require.Equal(t, int64(0), msg.Attachment.Size)
  1663. require.Equal(t, int64(0), msg.Attachment.Expires)
  1664. require.Equal(t, netip.Addr{}, msg.Sender)
  1665. }
  1666. func TestServer_PublishAttachmentBadURL(t *testing.T) {
  1667. s := newTestServer(t, newTestConfig(t))
  1668. response := request(t, s, "PUT", "/mytopic?a=not+a+URL", "", nil)
  1669. err := toHTTPError(t, response.Body.String())
  1670. require.Equal(t, 400, response.Code)
  1671. require.Equal(t, 400, err.HTTPCode)
  1672. require.Equal(t, 40013, err.Code)
  1673. }
  1674. func TestServer_PublishAttachmentTooLargeContentLength(t *testing.T) {
  1675. content := util.RandomString(5000) // > 4096
  1676. s := newTestServer(t, newTestConfig(t))
  1677. response := request(t, s, "PUT", "/mytopic", content, map[string]string{
  1678. "Content-Length": "20000000",
  1679. })
  1680. err := toHTTPError(t, response.Body.String())
  1681. require.Equal(t, 413, response.Code)
  1682. require.Equal(t, 413, err.HTTPCode)
  1683. require.Equal(t, 41301, err.Code)
  1684. }
  1685. func TestServer_PublishAttachmentTooLargeBodyAttachmentFileSizeLimit(t *testing.T) {
  1686. content := util.RandomString(5001) // > 5000, see below
  1687. c := newTestConfig(t)
  1688. c.AttachmentFileSizeLimit = 5000
  1689. s := newTestServer(t, c)
  1690. response := request(t, s, "PUT", "/mytopic", content, nil)
  1691. err := toHTTPError(t, response.Body.String())
  1692. require.Equal(t, 413, response.Code)
  1693. require.Equal(t, 413, err.HTTPCode)
  1694. require.Equal(t, 41301, err.Code)
  1695. }
  1696. func TestServer_PublishAttachmentExpiryBeforeDelivery(t *testing.T) {
  1697. c := newTestConfig(t)
  1698. c.AttachmentExpiryDuration = 10 * time.Minute
  1699. s := newTestServer(t, c)
  1700. response := request(t, s, "PUT", "/mytopic", util.RandomString(5000), map[string]string{
  1701. "Delay": "11 min", // > AttachmentExpiryDuration
  1702. })
  1703. err := toHTTPError(t, response.Body.String())
  1704. require.Equal(t, 400, response.Code)
  1705. require.Equal(t, 400, err.HTTPCode)
  1706. require.Equal(t, 40015, err.Code)
  1707. }
  1708. func TestServer_PublishAttachmentTooLargeBodyVisitorAttachmentTotalSizeLimit(t *testing.T) {
  1709. c := newTestConfig(t)
  1710. c.VisitorAttachmentTotalSizeLimit = 10000
  1711. s := newTestServer(t, c)
  1712. response := request(t, s, "PUT", "/mytopic", "text file!"+util.RandomString(4990), nil)
  1713. msg := toMessage(t, response.Body.String())
  1714. require.Equal(t, 200, response.Code)
  1715. require.Equal(t, "You received a file: attachment.txt", msg.Message)
  1716. require.Equal(t, int64(5000), msg.Attachment.Size)
  1717. content := util.RandomString(5001) // 5000+5001 > , see below
  1718. response = request(t, s, "PUT", "/mytopic", content, nil)
  1719. err := toHTTPError(t, response.Body.String())
  1720. require.Equal(t, 413, response.Code)
  1721. require.Equal(t, 413, err.HTTPCode)
  1722. require.Equal(t, 41301, err.Code)
  1723. }
  1724. func TestServer_PublishAttachmentAndExpire(t *testing.T) {
  1725. t.Parallel()
  1726. content := util.RandomString(5000) // > 4096
  1727. c := newTestConfig(t)
  1728. c.AttachmentExpiryDuration = time.Millisecond // Hack
  1729. s := newTestServer(t, c)
  1730. // Publish and make sure we can retrieve it
  1731. response := request(t, s, "PUT", "/mytopic", content, nil)
  1732. msg := toMessage(t, response.Body.String())
  1733. require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
  1734. file := filepath.Join(s.config.AttachmentCacheDir, msg.ID)
  1735. require.FileExists(t, file)
  1736. path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345")
  1737. response = request(t, s, "GET", path, "", nil)
  1738. require.Equal(t, 200, response.Code)
  1739. require.Equal(t, content, response.Body.String())
  1740. // Prune and makes sure it's gone
  1741. waitFor(t, func() bool {
  1742. s.execManager() // May run many times
  1743. return !util.FileExists(file)
  1744. })
  1745. response = request(t, s, "GET", path, "", nil)
  1746. require.Equal(t, 404, response.Code)
  1747. }
  1748. func TestServer_PublishAttachmentWithTierBasedExpiry(t *testing.T) {
  1749. t.Parallel()
  1750. content := util.RandomString(5000) // > 4096
  1751. c := newTestConfigWithAuthFile(t)
  1752. c.AttachmentExpiryDuration = time.Millisecond // Hack
  1753. s := newTestServer(t, c)
  1754. // Create tier with certain limits
  1755. sevenDays := time.Duration(604800) * time.Second
  1756. require.Nil(t, s.userManager.AddTier(&user.Tier{
  1757. Code: "test",
  1758. MessageLimit: 10,
  1759. MessageExpiryDuration: sevenDays,
  1760. AttachmentFileSizeLimit: 50_000,
  1761. AttachmentTotalSizeLimit: 200_000,
  1762. AttachmentExpiryDuration: sevenDays, // 7 days
  1763. AttachmentBandwidthLimit: 100000,
  1764. }))
  1765. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
  1766. require.Nil(t, s.userManager.ChangeTier("phil", "test"))
  1767. // Publish and make sure we can retrieve it
  1768. response := request(t, s, "PUT", "/mytopic", content, map[string]string{
  1769. "Authorization": util.BasicAuth("phil", "phil"),
  1770. })
  1771. require.Equal(t, 200, response.Code)
  1772. msg := toMessage(t, response.Body.String())
  1773. require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
  1774. require.True(t, msg.Attachment.Expires > time.Now().Add(sevenDays-30*time.Second).Unix())
  1775. require.True(t, msg.Expires > time.Now().Add(sevenDays-30*time.Second).Unix())
  1776. file := filepath.Join(s.config.AttachmentCacheDir, msg.ID)
  1777. require.FileExists(t, file)
  1778. path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345")
  1779. response = request(t, s, "GET", path, "", nil)
  1780. require.Equal(t, 200, response.Code)
  1781. require.Equal(t, content, response.Body.String())
  1782. // Prune and makes sure it's still there
  1783. time.Sleep(time.Second) // Sigh ...
  1784. s.execManager()
  1785. require.FileExists(t, file)
  1786. response = request(t, s, "GET", path, "", nil)
  1787. require.Equal(t, 200, response.Code)
  1788. }
  1789. func TestServer_PublishAttachmentWithTierBasedBandwidthLimit(t *testing.T) {
  1790. content := util.RandomString(5000) // > 4096
  1791. c := newTestConfigWithAuthFile(t)
  1792. c.VisitorAttachmentDailyBandwidthLimit = 1000 // Much lower than tier bandwidth!
  1793. s := newTestServer(t, c)
  1794. // Create tier with certain limits
  1795. require.Nil(t, s.userManager.AddTier(&user.Tier{
  1796. Code: "test",
  1797. MessageLimit: 10,
  1798. MessageExpiryDuration: time.Hour,
  1799. AttachmentFileSizeLimit: 50_000,
  1800. AttachmentTotalSizeLimit: 200_000,
  1801. AttachmentExpiryDuration: time.Hour,
  1802. AttachmentBandwidthLimit: 14000, // < 3x5000 bytes -> enough for one upload, one download
  1803. }))
  1804. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
  1805. require.Nil(t, s.userManager.ChangeTier("phil", "test"))
  1806. // Publish and make sure we can retrieve it
  1807. rr := request(t, s, "PUT", "/mytopic", content, map[string]string{
  1808. "Authorization": util.BasicAuth("phil", "phil"),
  1809. })
  1810. require.Equal(t, 200, rr.Code)
  1811. msg := toMessage(t, rr.Body.String())
  1812. // Retrieve it (first time succeeds)
  1813. rr = request(t, s, "GET", "/file/"+msg.ID, content, nil) // File downloads do not send auth headers!!
  1814. require.Equal(t, 200, rr.Code)
  1815. require.Equal(t, content, rr.Body.String())
  1816. // Retrieve it AGAIN (fails, due to bandwidth limit)
  1817. rr = request(t, s, "GET", "/file/"+msg.ID, content, nil)
  1818. require.Equal(t, 429, rr.Code)
  1819. }
  1820. func TestServer_PublishAttachmentWithTierBasedLimits(t *testing.T) {
  1821. smallFile := util.RandomString(20_000)
  1822. largeFile := util.RandomString(50_000)
  1823. c := newTestConfigWithAuthFile(t)
  1824. c.AttachmentFileSizeLimit = 20_000
  1825. c.VisitorAttachmentTotalSizeLimit = 40_000
  1826. s := newTestServer(t, c)
  1827. // Create tier with certain limits
  1828. require.Nil(t, s.userManager.AddTier(&user.Tier{
  1829. Code: "test",
  1830. MessageLimit: 100,
  1831. AttachmentFileSizeLimit: 50_000,
  1832. AttachmentTotalSizeLimit: 200_000,
  1833. AttachmentExpiryDuration: 30 * time.Second,
  1834. AttachmentBandwidthLimit: 1000000,
  1835. }))
  1836. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
  1837. require.Nil(t, s.userManager.ChangeTier("phil", "test"))
  1838. // Publish small file as anonymous
  1839. response := request(t, s, "PUT", "/mytopic", smallFile, nil)
  1840. msg := toMessage(t, response.Body.String())
  1841. require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
  1842. require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID))
  1843. // Publish large file as anonymous
  1844. response = request(t, s, "PUT", "/mytopic", largeFile, nil)
  1845. require.Equal(t, 413, response.Code)
  1846. require.Equal(t, 41301, toHTTPError(t, response.Body.String()).Code)
  1847. // Publish too large file as phil
  1848. response = request(t, s, "PUT", "/mytopic", largeFile+" a few more bytes", map[string]string{
  1849. "Authorization": util.BasicAuth("phil", "phil"),
  1850. })
  1851. require.Equal(t, 413, response.Code)
  1852. require.Equal(t, 41301, toHTTPError(t, response.Body.String()).Code)
  1853. // Publish large file as phil (4x)
  1854. for i := 0; i < 4; i++ {
  1855. response = request(t, s, "PUT", "/mytopic", largeFile, map[string]string{
  1856. "Authorization": util.BasicAuth("phil", "phil"),
  1857. })
  1858. require.Equal(t, 200, response.Code)
  1859. msg = toMessage(t, response.Body.String())
  1860. require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
  1861. require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID))
  1862. }
  1863. response = request(t, s, "PUT", "/mytopic", largeFile, map[string]string{
  1864. "Authorization": util.BasicAuth("phil", "phil"),
  1865. })
  1866. require.Equal(t, 413, response.Code)
  1867. require.Equal(t, 41301, toHTTPError(t, response.Body.String()).Code)
  1868. }
  1869. func TestServer_PublishAttachmentBandwidthLimit(t *testing.T) {
  1870. content := util.RandomString(5000) // > 4096
  1871. c := newTestConfig(t)
  1872. c.VisitorAttachmentDailyBandwidthLimit = 5*5000 + 123 // A little more than 1 upload and 3 downloads
  1873. s := newTestServer(t, c)
  1874. // Publish attachment
  1875. response := request(t, s, "PUT", "/mytopic", content, nil)
  1876. msg := toMessage(t, response.Body.String())
  1877. require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
  1878. // Value it 4 times successfully
  1879. path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345")
  1880. for i := 1; i <= 4; i++ { // 4 successful downloads
  1881. response = request(t, s, "GET", path, "", nil)
  1882. require.Equal(t, 200, response.Code)
  1883. require.Equal(t, content, response.Body.String())
  1884. }
  1885. // And then fail with a 429
  1886. response = request(t, s, "GET", path, "", nil)
  1887. err := toHTTPError(t, response.Body.String())
  1888. require.Equal(t, 429, response.Code)
  1889. require.Equal(t, 42905, err.Code)
  1890. }
  1891. func TestServer_PublishAttachmentBandwidthLimitUploadOnly(t *testing.T) {
  1892. content := util.RandomString(5000) // > 4096
  1893. c := newTestConfig(t)
  1894. c.VisitorAttachmentDailyBandwidthLimit = 5*5000 + 500 // 5 successful uploads
  1895. s := newTestServer(t, c)
  1896. // 5 successful uploads
  1897. for i := 1; i <= 5; i++ {
  1898. response := request(t, s, "PUT", "/mytopic", content, nil)
  1899. msg := toMessage(t, response.Body.String())
  1900. require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
  1901. }
  1902. // And a failed one
  1903. response := request(t, s, "PUT", "/mytopic", content, nil)
  1904. err := toHTTPError(t, response.Body.String())
  1905. require.Equal(t, 413, response.Code)
  1906. require.Equal(t, 41301, err.Code)
  1907. }
  1908. func TestServer_PublishAttachmentAndImmediatelyGetItWithCacheTimeout(t *testing.T) {
  1909. // This tests the awkward util.Retry in handleFile: Due to the async persisting of messages,
  1910. // the message is not immediately available when attempting to download it.
  1911. c := newTestConfig(t)
  1912. c.CacheBatchTimeout = 500 * time.Millisecond
  1913. c.CacheBatchSize = 10
  1914. s := newTestServer(t, c)
  1915. content := "this is an ATTACHMENT"
  1916. rr := request(t, s, "PUT", "/mytopic?f=myfile.txt", content, nil)
  1917. m := toMessage(t, rr.Body.String())
  1918. require.Equal(t, "myfile.txt", m.Attachment.Name)
  1919. path := strings.TrimPrefix(m.Attachment.URL, "http://127.0.0.1:12345")
  1920. rr = request(t, s, "GET", path, "", nil)
  1921. require.Equal(t, 200, rr.Code) // Not 404!
  1922. require.Equal(t, content, rr.Body.String())
  1923. }
  1924. func TestServer_PublishAttachmentAccountStats(t *testing.T) {
  1925. content := util.RandomString(4999) // > 4096
  1926. c := newTestConfig(t)
  1927. c.AttachmentFileSizeLimit = 5000
  1928. c.VisitorAttachmentTotalSizeLimit = 6000
  1929. s := newTestServer(t, c)
  1930. // Upload one attachment
  1931. response := request(t, s, "PUT", "/mytopic", content, nil)
  1932. msg := toMessage(t, response.Body.String())
  1933. require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
  1934. // User stats
  1935. response = request(t, s, "GET", "/v1/account", "", nil)
  1936. require.Equal(t, 200, response.Code)
  1937. account, err := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body))
  1938. require.Nil(t, err)
  1939. require.Equal(t, int64(5000), account.Limits.AttachmentFileSize)
  1940. require.Equal(t, int64(6000), account.Limits.AttachmentTotalSize)
  1941. require.Equal(t, int64(4999), account.Stats.AttachmentTotalSize)
  1942. require.Equal(t, int64(1001), account.Stats.AttachmentTotalSizeRemaining)
  1943. require.Equal(t, int64(1), account.Stats.Messages)
  1944. }
  1945. func TestServer_Visitor_XForwardedFor_None(t *testing.T) {
  1946. c := newTestConfig(t)
  1947. c.BehindProxy = true
  1948. s := newTestServer(t, c)
  1949. r, _ := http.NewRequest("GET", "/bla", nil)
  1950. r.RemoteAddr = "8.9.10.11:1234"
  1951. r.Header.Set("X-Forwarded-For", " ") // Spaces, not empty!
  1952. v, err := s.maybeAuthenticate(r)
  1953. require.Nil(t, err)
  1954. require.Equal(t, "8.9.10.11", v.ip.String())
  1955. }
  1956. func TestServer_Visitor_XForwardedFor_Single(t *testing.T) {
  1957. c := newTestConfig(t)
  1958. c.BehindProxy = true
  1959. s := newTestServer(t, c)
  1960. r, _ := http.NewRequest("GET", "/bla", nil)
  1961. r.RemoteAddr = "8.9.10.11:1234"
  1962. r.Header.Set("X-Forwarded-For", "1.1.1.1")
  1963. v, err := s.maybeAuthenticate(r)
  1964. require.Nil(t, err)
  1965. require.Equal(t, "1.1.1.1", v.ip.String())
  1966. }
  1967. func TestServer_Visitor_XForwardedFor_Multiple(t *testing.T) {
  1968. c := newTestConfig(t)
  1969. c.BehindProxy = true
  1970. s := newTestServer(t, c)
  1971. r, _ := http.NewRequest("GET", "/bla", nil)
  1972. r.RemoteAddr = "8.9.10.11:1234"
  1973. r.Header.Set("X-Forwarded-For", "1.2.3.4 , 2.4.4.2,234.5.2.1 ")
  1974. v, err := s.maybeAuthenticate(r)
  1975. require.Nil(t, err)
  1976. require.Equal(t, "234.5.2.1", v.ip.String())
  1977. }
  1978. func TestServer_Visitor_Custom_ClientIP_Header(t *testing.T) {
  1979. c := newTestConfig(t)
  1980. c.BehindProxy = true
  1981. c.ProxyForwardedHeader = "X-Client-IP"
  1982. s := newTestServer(t, c)
  1983. r, _ := http.NewRequest("GET", "/bla", nil)
  1984. r.RemoteAddr = "8.9.10.11:1234"
  1985. r.Header.Set("X-Client-IP", "1.2.3.4")
  1986. v, err := s.maybeAuthenticate(r)
  1987. require.Nil(t, err)
  1988. require.Equal(t, "1.2.3.4", v.ip.String())
  1989. }
  1990. func TestServer_Visitor_Custom_ClientIP_Header_IPv6(t *testing.T) {
  1991. c := newTestConfig(t)
  1992. c.BehindProxy = true
  1993. c.ProxyForwardedHeader = "X-Client-IP"
  1994. s := newTestServer(t, c)
  1995. r, _ := http.NewRequest("GET", "/bla", nil)
  1996. r.RemoteAddr = "[2001:db8:9999::1]:1234"
  1997. r.Header.Set("X-Client-IP", "2001:db8:7777::1")
  1998. v, err := s.maybeAuthenticate(r)
  1999. require.Nil(t, err)
  2000. require.Equal(t, "2001:db8:7777::1", v.ip.String())
  2001. }
  2002. func TestServer_Visitor_Custom_Forwarded_Header(t *testing.T) {
  2003. c := newTestConfig(t)
  2004. c.BehindProxy = true
  2005. c.ProxyForwardedHeader = "Forwarded"
  2006. c.ProxyTrustedPrefixes = []netip.Prefix{netip.MustParsePrefix("1.2.3.0/24")}
  2007. s := newTestServer(t, c)
  2008. r, _ := http.NewRequest("GET", "/bla", nil)
  2009. r.RemoteAddr = "8.9.10.11:1234"
  2010. r.Header.Set("Forwarded", " for=5.6.7.8, by=example.com;for=1.2.3.4")
  2011. v, err := s.maybeAuthenticate(r)
  2012. require.Nil(t, err)
  2013. require.Equal(t, "5.6.7.8", v.ip.String())
  2014. }
  2015. func TestServer_Visitor_Custom_Forwarded_Header_IPv6(t *testing.T) {
  2016. c := newTestConfig(t)
  2017. c.BehindProxy = true
  2018. c.ProxyForwardedHeader = "Forwarded"
  2019. c.ProxyTrustedPrefixes = []netip.Prefix{netip.MustParsePrefix("2001:db8:1111::/64")}
  2020. s := newTestServer(t, c)
  2021. r, _ := http.NewRequest("GET", "/bla", nil)
  2022. r.RemoteAddr = "[2001:db8:2222::1]:1234"
  2023. r.Header.Set("Forwarded", " for=[2001:db8:1111::1], by=example.com;for=[2001:db8:3333::1]")
  2024. v, err := s.maybeAuthenticate(r)
  2025. require.Nil(t, err)
  2026. require.Equal(t, "2001:db8:3333::1", v.ip.String())
  2027. }
  2028. func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) {
  2029. t.Parallel()
  2030. count := 50000
  2031. c := newTestConfig(t)
  2032. c.TotalTopicLimit = 50001
  2033. c.CacheStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;"
  2034. s := newTestServer(t, c)
  2035. // Add lots of messages
  2036. log.Info("Adding %d messages", count)
  2037. start := time.Now()
  2038. messages := make([]*message, 0)
  2039. for i := 0; i < count; i++ {
  2040. topicID := fmt.Sprintf("topic%d", i)
  2041. _, err := s.topicsFromIDs(topicID) // Add topic to internal s.topics array
  2042. require.Nil(t, err)
  2043. messages = append(messages, newDefaultMessage(topicID, "some message"))
  2044. }
  2045. require.Nil(t, s.messageCache.addMessages(messages))
  2046. log.Info("Done: Adding %d messages; took %s", count, time.Since(start).Round(time.Millisecond))
  2047. // Update stats
  2048. statsChan := make(chan bool)
  2049. go func() {
  2050. log.Info("Updating stats")
  2051. start := time.Now()
  2052. s.execManager()
  2053. log.Info("Done: Updating stats; took %s", time.Since(start).Round(time.Millisecond))
  2054. statsChan <- true
  2055. }()
  2056. time.Sleep(50 * time.Millisecond) // Make sure it starts first
  2057. // Publish message (during stats update)
  2058. log.Info("Publishing message")
  2059. start = time.Now()
  2060. response := request(t, s, "PUT", "/mytopic", "some body", nil)
  2061. m := toMessage(t, response.Body.String())
  2062. require.Equal(t, "some body", m.Message)
  2063. require.True(t, time.Since(start) < 100*time.Millisecond)
  2064. log.Info("Done: Publishing message; took %s", time.Since(start).Round(time.Millisecond))
  2065. // Wait for all goroutines
  2066. select {
  2067. case <-statsChan:
  2068. case <-time.After(10 * time.Second):
  2069. t.Fatal("Timed out waiting for Go routines")
  2070. }
  2071. log.Info("Done: Waiting for all locks")
  2072. }
  2073. func TestServer_AnonymousUser_And_NonTierUser_Are_Same_Visitor(t *testing.T) {
  2074. conf := newTestConfigWithAuthFile(t)
  2075. s := newTestServer(t, conf)
  2076. defer s.closeDatabases()
  2077. // Create user without tier
  2078. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
  2079. // Publish a message (anonymous user)
  2080. rr := request(t, s, "POST", "/mytopic", "hi", nil)
  2081. require.Equal(t, 200, rr.Code)
  2082. // Publish a message (non-tier user)
  2083. rr = request(t, s, "POST", "/mytopic", "hi", map[string]string{
  2084. "Authorization": util.BasicAuth("phil", "phil"),
  2085. })
  2086. require.Equal(t, 200, rr.Code)
  2087. // User stats (anonymous user)
  2088. rr = request(t, s, "GET", "/v1/account", "", nil)
  2089. account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body))
  2090. require.Equal(t, int64(2), account.Stats.Messages)
  2091. // User stats (non-tier user)
  2092. rr = request(t, s, "GET", "/v1/account", "", map[string]string{
  2093. "Authorization": util.BasicAuth("phil", "phil"),
  2094. })
  2095. account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body))
  2096. require.Equal(t, int64(2), account.Stats.Messages)
  2097. }
  2098. func TestServer_SubscriberRateLimiting_Success(t *testing.T) {
  2099. c := newTestConfigWithAuthFile(t)
  2100. c.VisitorRequestLimitBurst = 3
  2101. c.VisitorSubscriberRateLimiting = true
  2102. s := newTestServer(t, c)
  2103. // "Register" visitor 1.2.3.4 to topic "upAAAAAAAAAAAA" as a rate limit visitor
  2104. subscriber1Fn := func(r *http.Request) {
  2105. r.RemoteAddr = "1.2.3.4:1234"
  2106. }
  2107. rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriber1Fn)
  2108. require.Equal(t, 200, rr.Code)
  2109. require.Equal(t, "", rr.Body.String())
  2110. require.Equal(t, "1.2.3.4", s.topics["upAAAAAAAAAAAA"].rateVisitor.ip.String())
  2111. // "Register" visitor 8.7.7.1 to topic "up012345678912" as a rate limit visitor (implicitly via topic name)
  2112. subscriber2Fn := func(r *http.Request) {
  2113. r.RemoteAddr = "8.7.7.1:1234"
  2114. }
  2115. rr = request(t, s, "GET", "/up012345678912/json?poll=1", "", nil, subscriber2Fn)
  2116. require.Equal(t, 200, rr.Code)
  2117. require.Equal(t, "", rr.Body.String())
  2118. require.Equal(t, "8.7.7.1", s.topics["up012345678912"].rateVisitor.ip.String())
  2119. // Publish 2 messages to "subscriber1topic" as visitor 9.9.9.9. It'd be 3 normally, but the
  2120. // GET request before is also counted towards the request limiter.
  2121. for i := 0; i < 2; i++ {
  2122. rr := request(t, s, "PUT", "/upAAAAAAAAAAAA", "some message", nil)
  2123. require.Equal(t, 200, rr.Code)
  2124. }
  2125. rr = request(t, s, "PUT", "/upAAAAAAAAAAAA", "some message", nil)
  2126. require.Equal(t, 429, rr.Code)
  2127. // Publish another 2 messages to "up012345678912" as visitor 9.9.9.9
  2128. for i := 0; i < 2; i++ {
  2129. rr := request(t, s, "PUT", "/up012345678912", "some message", nil)
  2130. require.Equal(t, 200, rr.Code) // If we fail here, handlePublish is using the wrong visitor!
  2131. }
  2132. rr = request(t, s, "PUT", "/up012345678912", "some message", nil)
  2133. require.Equal(t, 429, rr.Code)
  2134. // Hurray! At this point, visitor 9.9.9.9 has published 4 messages, even though
  2135. // VisitorRequestLimitBurst is 3. That means it's working.
  2136. // Now let's confirm that so far we haven't used up any of visitor 9.9.9.9's request limiter
  2137. // by publishing another 3 requests from it.
  2138. for i := 0; i < 3; i++ {
  2139. rr := request(t, s, "PUT", "/some-other-topic", "some message", nil)
  2140. require.Equal(t, 200, rr.Code)
  2141. }
  2142. rr = request(t, s, "PUT", "/some-other-topic", "some message", nil)
  2143. require.Equal(t, 429, rr.Code)
  2144. }
  2145. func TestServer_SubscriberRateLimiting_NotWrongTopic(t *testing.T) {
  2146. c := newTestConfigWithAuthFile(t)
  2147. c.VisitorSubscriberRateLimiting = true
  2148. s := newTestServer(t, c)
  2149. subscriberFn := func(r *http.Request) {
  2150. r.RemoteAddr = "1.2.3.4:1234"
  2151. }
  2152. rr := request(t, s, "GET", "/alerts,upAAAAAAAAAAAA,upBBBBBBBBBBBB/json?poll=1", "", nil, subscriberFn)
  2153. require.Equal(t, 200, rr.Code)
  2154. require.Equal(t, "", rr.Body.String())
  2155. require.Nil(t, s.topics["alerts"].rateVisitor)
  2156. require.Equal(t, "1.2.3.4", s.topics["upAAAAAAAAAAAA"].rateVisitor.ip.String())
  2157. require.Equal(t, "1.2.3.4", s.topics["upBBBBBBBBBBBB"].rateVisitor.ip.String())
  2158. }
  2159. func TestServer_SubscriberRateLimiting_NotEnabled_Failed(t *testing.T) {
  2160. c := newTestConfigWithAuthFile(t)
  2161. c.VisitorRequestLimitBurst = 3
  2162. c.VisitorSubscriberRateLimiting = false
  2163. s := newTestServer(t, c)
  2164. // Subscriber rate limiting is disabled!
  2165. // Registering visitor 1.2.3.4 to topic has no effect
  2166. rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, func(r *http.Request) {
  2167. r.RemoteAddr = "1.2.3.4:1234"
  2168. })
  2169. require.Equal(t, 200, rr.Code)
  2170. require.Equal(t, "", rr.Body.String())
  2171. require.Nil(t, s.topics["upAAAAAAAAAAAA"].rateVisitor)
  2172. // Registering visitor 8.7.7.1 to topic has no effect
  2173. rr = request(t, s, "GET", "/up012345678912/json?poll=1", "", nil, func(r *http.Request) {
  2174. r.RemoteAddr = "8.7.7.1:1234"
  2175. })
  2176. require.Equal(t, 200, rr.Code)
  2177. require.Equal(t, "", rr.Body.String())
  2178. require.Nil(t, s.topics["up012345678912"].rateVisitor)
  2179. // Publish 3 messages to "upAAAAAAAAAAAA" as visitor 9.9.9.9
  2180. for i := 0; i < 3; i++ {
  2181. rr := request(t, s, "PUT", "/subscriber1topic", "some message", nil)
  2182. require.Equal(t, 200, rr.Code)
  2183. }
  2184. rr = request(t, s, "PUT", "/subscriber1topic", "some message", nil)
  2185. require.Equal(t, 429, rr.Code)
  2186. rr = request(t, s, "PUT", "/up012345678912", "some message", nil)
  2187. require.Equal(t, 429, rr.Code)
  2188. }
  2189. func TestServer_SubscriberRateLimiting_UP_Only(t *testing.T) {
  2190. c := newTestConfigWithAuthFile(t)
  2191. c.VisitorRequestLimitBurst = 3
  2192. c.VisitorSubscriberRateLimiting = true
  2193. s := newTestServer(t, c)
  2194. // "Register" 5 different UnifiedPush visitors
  2195. for i := 0; i < 5; i++ {
  2196. subscriberFn := func(r *http.Request) {
  2197. r.RemoteAddr = fmt.Sprintf("1.2.3.%d:1234", i+1)
  2198. }
  2199. rr := request(t, s, "GET", fmt.Sprintf("/up12345678901%d/json?poll=1", i), "", nil, subscriberFn)
  2200. require.Equal(t, 200, rr.Code)
  2201. }
  2202. // Publish 2 messages per topic
  2203. for i := 0; i < 5; i++ {
  2204. for j := 0; j < 2; j++ {
  2205. rr := request(t, s, "PUT", fmt.Sprintf("/up12345678901%d?up=1", i), "some message", nil)
  2206. require.Equal(t, 200, rr.Code)
  2207. }
  2208. }
  2209. }
  2210. func TestServer_Matrix_SubscriberRateLimiting_UP_Only(t *testing.T) {
  2211. c := newTestConfig(t)
  2212. c.VisitorRequestLimitBurst = 3
  2213. c.VisitorSubscriberRateLimiting = true
  2214. s := newTestServer(t, c)
  2215. // "Register" 5 different UnifiedPush visitors
  2216. for i := 0; i < 5; i++ {
  2217. rr := request(t, s, "GET", fmt.Sprintf("/up12345678901%d/json?poll=1", i), "", nil, func(r *http.Request) {
  2218. r.RemoteAddr = fmt.Sprintf("1.2.3.%d:1234", i+1)
  2219. })
  2220. require.Equal(t, 200, rr.Code)
  2221. }
  2222. // Publish 2 messages per topic
  2223. for i := 0; i < 5; i++ {
  2224. notification := fmt.Sprintf(`{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/up12345678901%d?up=1"}]}}`, i)
  2225. for j := 0; j < 2; j++ {
  2226. response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil)
  2227. require.Equal(t, 200, response.Code)
  2228. require.Equal(t, `{"rejected":[]}`+"\n", response.Body.String())
  2229. }
  2230. response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil)
  2231. require.Equal(t, 429, response.Code, notification)
  2232. require.Equal(t, 42901, toHTTPError(t, response.Body.String()).Code)
  2233. }
  2234. }
  2235. func TestServer_SubscriberRateLimiting_VisitorExpiration(t *testing.T) {
  2236. c := newTestConfig(t)
  2237. c.VisitorRequestLimitBurst = 3
  2238. c.VisitorSubscriberRateLimiting = true
  2239. s := newTestServer(t, c)
  2240. // "Register" rate visitor
  2241. subscriberFn := func(r *http.Request) {
  2242. r.RemoteAddr = "1.2.3.4:1234"
  2243. }
  2244. rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriberFn)
  2245. require.Equal(t, 200, rr.Code)
  2246. require.Equal(t, "1.2.3.4", s.topics["upAAAAAAAAAAAA"].rateVisitor.ip.String())
  2247. require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["upAAAAAAAAAAAA"].rateVisitor)
  2248. // Publish message, observe rate visitor tokens being decreased
  2249. response := request(t, s, "POST", "/upAAAAAAAAAAAA", "some message", nil)
  2250. require.Equal(t, 200, response.Code)
  2251. require.Equal(t, int64(0), s.visitors["ip:9.9.9.9"].messagesLimiter.Value())
  2252. require.Equal(t, int64(1), s.topics["upAAAAAAAAAAAA"].rateVisitor.messagesLimiter.Value())
  2253. require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["upAAAAAAAAAAAA"].rateVisitor)
  2254. // Expire visitor
  2255. s.visitors["ip:1.2.3.4"].seen = time.Now().Add(-1 * 25 * time.Hour)
  2256. s.pruneVisitors()
  2257. // Publish message again, observe that rateVisitor is not used anymore and is reset
  2258. response = request(t, s, "POST", "/upAAAAAAAAAAAA", "some message", nil)
  2259. require.Equal(t, 200, response.Code)
  2260. require.Equal(t, int64(1), s.visitors["ip:9.9.9.9"].messagesLimiter.Value())
  2261. require.Nil(t, s.topics["upAAAAAAAAAAAA"].rateVisitor)
  2262. require.Nil(t, s.visitors["ip:1.2.3.4"])
  2263. }
  2264. func TestServer_SubscriberRateLimiting_ProtectedTopics_WithDefaultReadWrite(t *testing.T) {
  2265. c := newTestConfigWithAuthFile(t)
  2266. c.AuthDefault = user.PermissionReadWrite
  2267. c.VisitorSubscriberRateLimiting = true
  2268. s := newTestServer(t, c)
  2269. // Create some ACLs
  2270. require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", user.PermissionRead))
  2271. // Set rate visitor as ip:1.2.3.4 on topic
  2272. // - "up123456789012": Allowed, because no ACLs and nobody owns the topic
  2273. // - "announcements": NOT allowed, because it has read-only permissions for everyone
  2274. rr := request(t, s, "GET", "/up123456789012,announcements/json?poll=1", "", nil, func(r *http.Request) {
  2275. r.RemoteAddr = "1.2.3.4:1234"
  2276. })
  2277. require.Equal(t, 200, rr.Code)
  2278. require.Equal(t, "1.2.3.4", s.topics["up123456789012"].rateVisitor.ip.String())
  2279. require.Nil(t, s.topics["announcements"].rateVisitor)
  2280. }
  2281. func TestServer_MessageHistoryAndStatsEndpoint(t *testing.T) {
  2282. c := newTestConfig(t)
  2283. c.ManagerInterval = 2 * time.Second
  2284. s := newTestServer(t, c)
  2285. // Publish some messages, and get stats
  2286. for i := 0; i < 5; i++ {
  2287. response := request(t, s, "POST", "/mytopic", "some message", nil)
  2288. require.Equal(t, 200, response.Code)
  2289. }
  2290. require.Equal(t, int64(5), s.messages)
  2291. require.Equal(t, []int64{0}, s.messagesHistory)
  2292. response := request(t, s, "GET", "/v1/stats", "", nil)
  2293. require.Equal(t, 200, response.Code)
  2294. require.Equal(t, `{"messages":5,"messages_rate":0}`+"\n", response.Body.String())
  2295. // Run manager and see message history update
  2296. s.execManager()
  2297. require.Equal(t, []int64{0, 5}, s.messagesHistory)
  2298. response = request(t, s, "GET", "/v1/stats", "", nil)
  2299. require.Equal(t, 200, response.Code)
  2300. require.Equal(t, `{"messages":5,"messages_rate":2.5}`+"\n", response.Body.String()) // 5 messages in 2 seconds = 2.5 messages per second
  2301. // Publish some more messages
  2302. for i := 0; i < 10; i++ {
  2303. response := request(t, s, "POST", "/mytopic", "some message", nil)
  2304. require.Equal(t, 200, response.Code)
  2305. }
  2306. require.Equal(t, int64(15), s.messages)
  2307. require.Equal(t, []int64{0, 5}, s.messagesHistory)
  2308. response = request(t, s, "GET", "/v1/stats", "", nil)
  2309. require.Equal(t, 200, response.Code)
  2310. require.Equal(t, `{"messages":15,"messages_rate":2.5}`+"\n", response.Body.String()) // Rate did not update yet
  2311. // Run manager and see message history update
  2312. s.execManager()
  2313. require.Equal(t, []int64{0, 5, 15}, s.messagesHistory)
  2314. response = request(t, s, "GET", "/v1/stats", "", nil)
  2315. require.Equal(t, 200, response.Code)
  2316. require.Equal(t, `{"messages":15,"messages_rate":3.75}`+"\n", response.Body.String()) // 15 messages in 4 seconds = 3.75 messages per second
  2317. }
  2318. func TestServer_MessageHistoryMaxSize(t *testing.T) {
  2319. t.Parallel()
  2320. s := newTestServer(t, newTestConfig(t))
  2321. for i := 0; i < 20; i++ {
  2322. s.messages = int64(i)
  2323. s.execManager()
  2324. }
  2325. require.Equal(t, []int64{10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, s.messagesHistory)
  2326. }
  2327. func TestServer_MessageCountPersistence(t *testing.T) {
  2328. t.Parallel()
  2329. c := newTestConfig(t)
  2330. s := newTestServer(t, c)
  2331. s.messages = 1234
  2332. s.execManager()
  2333. waitFor(t, func() bool {
  2334. messages, err := s.messageCache.Stats()
  2335. require.Nil(t, err)
  2336. return messages == 1234
  2337. })
  2338. s = newTestServer(t, c)
  2339. require.Equal(t, int64(1234), s.messages)
  2340. }
  2341. func TestServer_PublishWithUTF8MimeHeader(t *testing.T) {
  2342. t.Parallel()
  2343. s := newTestServer(t, newTestConfig(t))
  2344. response := request(t, s, "POST", "/mytopic", "some attachment", map[string]string{
  2345. "X-Filename": "some =?UTF-8?q?=C3=A4?=ttachment.txt",
  2346. "X-Message": "=?UTF-8?B?8J+HqfCfh6o=?=",
  2347. "X-Title": "=?UTF-8?B?bnRmeSDlvojmo5I=?=, no really I mean it! =?UTF-8?Q?This is q=C3=BC=C3=B6ted-print=C3=A4ble.?=",
  2348. "X-Tags": "=?UTF-8?B?8J+HqfCfh6o=?=, =?UTF-8?B?bnRmeSDlvojmo5I=?=",
  2349. "X-Click": "=?uTf-8?b?aHR0cHM6Ly/wn5KpLmxh?=",
  2350. "X-Actions": "http, \"=?utf-8?q?Mettre =C3=A0 jour?=\", \"https://my.tld/webhook/netbird-update\"; =?utf-8?b?aHR0cCwg6L+Z5piv5LiA5Liq5qCH562+LCBodHRwczovL/CfkqkubGE=?=",
  2351. })
  2352. require.Equal(t, 200, response.Code)
  2353. m := toMessage(t, response.Body.String())
  2354. require.Equal(t, "🇩🇪", m.Message)
  2355. require.Equal(t, "ntfy 很棒, no really I mean it! This is qüöted-printäble.", m.Title)
  2356. require.Equal(t, "some ättachment.txt", m.Attachment.Name)
  2357. require.Equal(t, "🇩🇪", m.Tags[0])
  2358. require.Equal(t, "ntfy 很棒", m.Tags[1])
  2359. require.Equal(t, "https://💩.la", m.Click)
  2360. require.Equal(t, "Mettre à jour", m.Actions[0].Label)
  2361. require.Equal(t, "http", m.Actions[1].Action)
  2362. require.Equal(t, "这是一个标签", m.Actions[1].Label)
  2363. require.Equal(t, "https://💩.la", m.Actions[1].URL)
  2364. }
  2365. func TestServer_UpstreamBaseURL_Success(t *testing.T) {
  2366. t.Parallel()
  2367. var pollID atomic.Pointer[string]
  2368. upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2369. body, err := io.ReadAll(r.Body)
  2370. require.Nil(t, err)
  2371. require.Equal(t, "/87c9cddf7b0105f5fe849bf084c6e600be0fde99be3223335199b4965bd7b735", r.URL.Path)
  2372. require.Equal(t, "", string(body))
  2373. require.NotEmpty(t, r.Header.Get("X-Poll-ID"))
  2374. pollID.Store(util.String(r.Header.Get("X-Poll-ID")))
  2375. }))
  2376. defer upstreamServer.Close()
  2377. c := newTestConfigWithAuthFile(t)
  2378. c.BaseURL = "http://myserver.internal"
  2379. c.UpstreamBaseURL = upstreamServer.URL
  2380. s := newTestServer(t, c)
  2381. // Send message, and wait for upstream server to receive it
  2382. response := request(t, s, "PUT", "/mytopic", `hi there`, nil)
  2383. require.Equal(t, 200, response.Code)
  2384. m := toMessage(t, response.Body.String())
  2385. require.NotEmpty(t, m.ID)
  2386. require.Equal(t, "hi there", m.Message)
  2387. waitFor(t, func() bool {
  2388. pID := pollID.Load()
  2389. return pID != nil && *pID == m.ID
  2390. })
  2391. }
  2392. func TestServer_UpstreamBaseURL_With_Access_Token_Success(t *testing.T) {
  2393. t.Parallel()
  2394. var pollID atomic.Pointer[string]
  2395. upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2396. body, err := io.ReadAll(r.Body)
  2397. require.Nil(t, err)
  2398. require.Equal(t, "/a1c72bcb4daf5af54d13ef86aea8f76c11e8b88320d55f1811d5d7b173bcc1df", r.URL.Path)
  2399. require.Equal(t, "Bearer tk_1234567890", r.Header.Get("Authorization"))
  2400. require.Equal(t, "", string(body))
  2401. require.NotEmpty(t, r.Header.Get("X-Poll-ID"))
  2402. pollID.Store(util.String(r.Header.Get("X-Poll-ID")))
  2403. }))
  2404. defer upstreamServer.Close()
  2405. c := newTestConfigWithAuthFile(t)
  2406. c.BaseURL = "http://myserver.internal"
  2407. c.UpstreamBaseURL = upstreamServer.URL
  2408. c.UpstreamAccessToken = "tk_1234567890"
  2409. s := newTestServer(t, c)
  2410. // Send message, and wait for upstream server to receive it
  2411. response := request(t, s, "PUT", "/mytopic1", `hi there`, nil)
  2412. require.Equal(t, 200, response.Code)
  2413. m := toMessage(t, response.Body.String())
  2414. require.NotEmpty(t, m.ID)
  2415. require.Equal(t, "hi there", m.Message)
  2416. waitFor(t, func() bool {
  2417. pID := pollID.Load()
  2418. return pID != nil && *pID == m.ID
  2419. })
  2420. }
  2421. func TestServer_UpstreamBaseURL_DoNotForwardUnifiedPush(t *testing.T) {
  2422. t.Parallel()
  2423. upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2424. t.Fatal("UnifiedPush messages should not be forwarded")
  2425. }))
  2426. defer upstreamServer.Close()
  2427. c := newTestConfigWithAuthFile(t)
  2428. c.BaseURL = "http://myserver.internal"
  2429. c.UpstreamBaseURL = upstreamServer.URL
  2430. s := newTestServer(t, c)
  2431. // Send UP message, this should not forward to upstream server
  2432. response := request(t, s, "PUT", "/mytopic?up=1", `hi there`, nil)
  2433. require.Equal(t, 200, response.Code)
  2434. m := toMessage(t, response.Body.String())
  2435. require.NotEmpty(t, m.ID)
  2436. require.Equal(t, "hi there", m.Message)
  2437. // Forwarding is done asynchronously, so wait a bit.
  2438. // This ensures that the t.Fatal above is actually not triggered.
  2439. time.Sleep(500 * time.Millisecond)
  2440. }
  2441. func TestServer_MessageTemplate(t *testing.T) {
  2442. t.Parallel()
  2443. s := newTestServer(t, newTestConfig(t))
  2444. response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{
  2445. "X-Message": "{{.foo}}",
  2446. "X-Title": "{{.nested.title}}",
  2447. "X-Template": "1",
  2448. })
  2449. require.Equal(t, 200, response.Code)
  2450. m := toMessage(t, response.Body.String())
  2451. require.Equal(t, "bar", m.Message)
  2452. require.Equal(t, "here", m.Title)
  2453. }
  2454. func TestServer_MessageTemplate_RepeatPlaceholder(t *testing.T) {
  2455. t.Parallel()
  2456. s := newTestServer(t, newTestConfig(t))
  2457. response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{
  2458. "Message": "{{.foo}} is {{.foo}}",
  2459. "Title": "{{.nested.title}} is {{.nested.title}}",
  2460. "Template": "1",
  2461. })
  2462. require.Equal(t, 200, response.Code)
  2463. m := toMessage(t, response.Body.String())
  2464. require.Equal(t, "bar is bar", m.Message)
  2465. require.Equal(t, "here is here", m.Title)
  2466. }
  2467. func TestServer_MessageTemplate_JSONBody(t *testing.T) {
  2468. t.Parallel()
  2469. s := newTestServer(t, newTestConfig(t))
  2470. body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"}}"}`
  2471. response := request(t, s, "PUT", "/", body, map[string]string{
  2472. "m": "{{.foo}}",
  2473. "t": "{{.nested.title}}",
  2474. "tpl": "1",
  2475. })
  2476. require.Equal(t, 200, response.Code)
  2477. m := toMessage(t, response.Body.String())
  2478. require.Equal(t, "bar", m.Message)
  2479. require.Equal(t, "here", m.Title)
  2480. }
  2481. func TestServer_MessageTemplate_MalformedJSONBody(t *testing.T) {
  2482. t.Parallel()
  2483. s := newTestServer(t, newTestConfig(t))
  2484. body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"INVALID"}`
  2485. response := request(t, s, "PUT", "/", body, map[string]string{
  2486. "X-Message": "{{.foo}}",
  2487. "X-Title": "{{.nested.title}}",
  2488. "X-Template": "1",
  2489. })
  2490. require.Equal(t, 400, response.Code)
  2491. require.Equal(t, 40042, toHTTPError(t, response.Body.String()).Code)
  2492. }
  2493. func TestServer_MessageTemplate_PlaceholderTypo(t *testing.T) {
  2494. t.Parallel()
  2495. s := newTestServer(t, newTestConfig(t))
  2496. response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{
  2497. "X-Message": "{{.food}}",
  2498. "X-Title": "{{.neste.title}}",
  2499. "X-Template": "1",
  2500. })
  2501. require.Equal(t, 200, response.Code)
  2502. m := toMessage(t, response.Body.String())
  2503. require.Equal(t, "<no value>", m.Message)
  2504. require.Equal(t, "<no value>", m.Title)
  2505. }
  2506. func TestServer_MessageTemplate_MultiplePlaceholders(t *testing.T) {
  2507. t.Parallel()
  2508. s := newTestServer(t, newTestConfig(t))
  2509. response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{
  2510. "X-Message": "{{.foo}} is {{.nested.title}}",
  2511. "X-Template": "1",
  2512. })
  2513. require.Equal(t, 200, response.Code)
  2514. m := toMessage(t, response.Body.String())
  2515. require.Equal(t, "bar is here", m.Message)
  2516. }
  2517. func TestServer_MessageTemplate_Range(t *testing.T) {
  2518. t.Parallel()
  2519. s := newTestServer(t, newTestConfig(t))
  2520. jsonBody := `{"foo": "bar", "errors": [{"level": "severe", "url": "https://severe1.com"},{"level": "warning", "url": "https://warning.com"},{"level": "severe", "url": "https://severe2.com"}]}`
  2521. response := request(t, s, "PUT", "/mytopic", jsonBody, map[string]string{
  2522. "X-Message": `Severe URLs:\n{{range .errors}}{{if eq .level "severe"}}- {{.url}}\n{{end}}{{end}}`,
  2523. "X-Template": "1",
  2524. })
  2525. require.Equal(t, 200, response.Code)
  2526. m := toMessage(t, response.Body.String())
  2527. require.Equal(t, "Severe URLs:\n- https://severe1.com\n- https://severe2.com", m.Message)
  2528. }
  2529. func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageOK(t *testing.T) {
  2530. t.Parallel()
  2531. c := newTestConfig(t)
  2532. c.MessageSizeLimit = 25 // 25 < len(HTTP body) < 32k, and len(m.Message) < 25
  2533. s := newTestServer(t, c)
  2534. response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{
  2535. "X-Message": "{{.foo}}",
  2536. "X-Title": "{{.nested.title}}",
  2537. "X-Template": "yes",
  2538. })
  2539. require.Equal(t, 200, response.Code)
  2540. m := toMessage(t, response.Body.String())
  2541. require.Equal(t, "bar", m.Message)
  2542. require.Equal(t, "here", m.Title)
  2543. }
  2544. func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageTooLong(t *testing.T) {
  2545. t.Parallel()
  2546. c := newTestConfig(t)
  2547. c.MessageSizeLimit = 21 // 21 < len(HTTP body) < 32k, but !len(m.Message) < 21
  2548. s := newTestServer(t, c)
  2549. response := request(t, s, "PUT", "/mytopic", `{"foo":"This is a long message"}`, map[string]string{
  2550. "X-Message": "{{.foo}}",
  2551. "X-Template": "1",
  2552. })
  2553. require.Equal(t, 400, response.Code)
  2554. require.Equal(t, 40041, toHTTPError(t, response.Body.String()).Code)
  2555. }
  2556. func TestServer_MessageTemplate_Grafana(t *testing.T) {
  2557. t.Parallel()
  2558. s := newTestServer(t, newTestConfig(t))
  2559. body := `{"receiver":"ntfy\\.example\\.com/alerts","status":"resolved","alerts":[{"status":"resolved","labels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts","instance":"10.108.0.2:9100","job":"node-exporter"},"annotations":{"summary":"15m load average too high"},"startsAt":"2024-03-15T02:28:00Z","endsAt":"2024-03-15T02:42:00Z","generatorURL":"localhost:3000/alerting/grafana/NW9oDw-4z/view","fingerprint":"becbfb94bd81ef48","silenceURL":"localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter","dashboardURL":"","panelURL":"","values":{"B":18.98211314475876,"C":0},"valueString":"[ var='B' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=18.98211314475876 ], [ var='C' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=0 ]"}],"groupLabels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts"},"commonLabels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts","instance":"10.108.0.2:9100","job":"node-exporter"},"commonAnnotations":{"summary":"15m load average too high"},"externalURL":"localhost:3000/","version":"1","groupKey":"{}:{alertname=\"Load avg 15m too high\", grafana_folder=\"Node alerts\"}","truncatedAlerts":0,"orgId":1,"title":"[RESOLVED] Load avg 15m too high Node alerts (10.108.0.2:9100 node-exporter)","state":"ok","message":"**Resolved**\n\nValue: B=18.98211314475876, C=0\nLabels:\n - alertname = Load avg 15m too high\n - grafana_folder = Node alerts\n - instance = 10.108.0.2:9100\n - job = node-exporter\nAnnotations:\n - summary = 15m load average too high\nSource: localhost:3000/alerting/grafana/NW9oDw-4z/view\nSilence: localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter\n"}`
  2560. response := request(t, s, "PUT", "/mytopic?tpl=yes&title=Grafana+alert:+{{.title}}&message={{.message}}", body, nil)
  2561. require.Equal(t, 200, response.Code)
  2562. m := toMessage(t, response.Body.String())
  2563. require.Equal(t, "Grafana alert: [RESOLVED] Load avg 15m too high Node alerts (10.108.0.2:9100 node-exporter)", m.Title)
  2564. require.Equal(t, `**Resolved**
  2565. Value: B=18.98211314475876, C=0
  2566. Labels:
  2567. - alertname = Load avg 15m too high
  2568. - grafana_folder = Node alerts
  2569. - instance = 10.108.0.2:9100
  2570. - job = node-exporter
  2571. Annotations:
  2572. - summary = 15m load average too high
  2573. Source: localhost:3000/alerting/grafana/NW9oDw-4z/view
  2574. Silence: localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter`, m.Message)
  2575. }
  2576. func TestServer_MessageTemplate_GitHub(t *testing.T) {
  2577. t.Parallel()
  2578. s := newTestServer(t, newTestConfig(t))
  2579. body := `{"action":"opened","number":1,"pull_request":{"url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1","id":1783420972,"node_id":"PR_kwDOHAbdo85qTNgs","html_url":"https://github.com/binwiederhier/dabble/pull/1","diff_url":"https://github.com/binwiederhier/dabble/pull/1.diff","patch_url":"https://github.com/binwiederhier/dabble/pull/1.patch","issue_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1","number":1,"state":"open","locked":false,"title":"A sample PR from Phil","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"body":null,"created_at":"2024-03-21T02:52:09Z","updated_at":"2024-03-21T02:52:09Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/commits","review_comments_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments","review_comment_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b","head":{"label":"binwiederhier:aa","ref":"aa","sha":"5703842cc5715ed1e358d23ebb693db09747ae9b","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"delete_branch_on_merge":false,"allow_update_branch":false,"use_squash_pr_title_as_default":false,"squash_merge_commit_message":"COMMIT_MESSAGES","squash_merge_commit_title":"COMMIT_OR_PR_TITLE","merge_commit_message":"PR_TITLE","merge_commit_title":"MERGE_MESSAGE"}},"base":{"label":"binwiederhier:main","ref":"main","sha":"72d931a20bb83d123ab45accaf761150c8b01211","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"delete_branch_on_merge":false,"allow_update_branch":false,"use_squash_pr_title_as_default":false,"squash_merge_commit_message":"COMMIT_MESSAGES","squash_merge_commit_title":"COMMIT_OR_PR_TITLE","merge_commit_message":"PR_TITLE","merge_commit_title":"MERGE_MESSAGE"}},"_links":{"self":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1"},"html":{"href":"https://github.com/binwiederhier/dabble/pull/1"},"issue":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1"},"comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":false,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":null,"comments":0,"review_comments":0,"maintainer_can_modify":false,"commits":1,"additions":1,"deletions":1,"changed_files":1},"repository":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"},"sender":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false}}`
  2580. response := request(t, s, "PUT", `/mytopic?tpl=yes&message=[{{.pull_request.head.repo.full_name}}]+Pull+request+{{if+eq+.action+"opened"}}OPENED{{else}}CLOSED{{end}}:+{{.pull_request.title}}`, body, nil)
  2581. require.Equal(t, 200, response.Code)
  2582. m := toMessage(t, response.Body.String())
  2583. require.Equal(t, "", m.Title)
  2584. require.Equal(t, `[binwiederhier/dabble] Pull request OPENED: A sample PR from Phil`, m.Message)
  2585. }
  2586. func TestServer_MessageTemplate_GitHub2(t *testing.T) {
  2587. t.Parallel()
  2588. s := newTestServer(t, newTestConfig(t))
  2589. body := `{"action":"opened","number":1,"pull_request":{"url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1","id":1783420972,"node_id":"PR_kwDOHAbdo85qTNgs","html_url":"https://github.com/binwiederhier/dabble/pull/1","diff_url":"https://github.com/binwiederhier/dabble/pull/1.diff","patch_url":"https://github.com/binwiederhier/dabble/pull/1.patch","issue_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1","number":1,"state":"open","locked":false,"title":"A sample PR from Phil","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"body":null,"created_at":"2024-03-21T02:52:09Z","updated_at":"2024-03-21T02:52:09Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/commits","review_comments_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments","review_comment_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b","head":{"label":"binwiederhier:aa","ref":"aa","sha":"5703842cc5715ed1e358d23ebb693db09747ae9b","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"delete_branch_on_merge":false,"allow_update_branch":false,"use_squash_pr_title_as_default":false,"squash_merge_commit_message":"COMMIT_MESSAGES","squash_merge_commit_title":"COMMIT_OR_PR_TITLE","merge_commit_message":"PR_TITLE","merge_commit_title":"MERGE_MESSAGE"}},"base":{"label":"binwiederhier:main","ref":"main","sha":"72d931a20bb83d123ab45accaf761150c8b01211","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"delete_branch_on_merge":false,"allow_update_branch":false,"use_squash_pr_title_as_default":false,"squash_merge_commit_message":"COMMIT_MESSAGES","squash_merge_commit_title":"COMMIT_OR_PR_TITLE","merge_commit_message":"PR_TITLE","merge_commit_title":"MERGE_MESSAGE"}},"_links":{"self":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1"},"html":{"href":"https://github.com/binwiederhier/dabble/pull/1"},"issue":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1"},"comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":false,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":null,"comments":0,"review_comments":0,"maintainer_can_modify":false,"commits":1,"additions":1,"deletions":1,"changed_files":1},"repository":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"},"sender":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false}}`
  2590. response := request(t, s, "PUT", `/mytopic?tpl=yes&title={{if+eq+.action+"opened"}}New+PR:+%23{{.number}}+by+{{.pull_request.user.login}}{{else}}[{{.action}}]+PR:+%23{{.number}}+by+{{.pull_request.user.login}}{{end}}&message={{.pull_request.title}}+in+{{.repository.full_name}}.+View+more+at+{{.pull_request.html_url}}`, body, nil)
  2591. require.Equal(t, 200, response.Code)
  2592. m := toMessage(t, response.Body.String())
  2593. require.Equal(t, `New PR: #1 by binwiederhier`, m.Title)
  2594. require.Equal(t, `A sample PR from Phil in binwiederhier/dabble. View more at https://github.com/binwiederhier/dabble/pull/1`, m.Message)
  2595. }
  2596. func TestServer_MessageTemplate_DisallowedCalls(t *testing.T) {
  2597. t.Parallel()
  2598. s := newTestServer(t, newTestConfig(t))
  2599. disallowedTemplates := []string{
  2600. `{{template ""}}`,
  2601. `{{- template ""}}`,
  2602. `{{-
  2603. template ""}}`,
  2604. `{{ call abc}}`,
  2605. `{{ define "aa"}}`,
  2606. `We cannot {{define "aa"}}`,
  2607. `We cannot {{ call "aa"}}`,
  2608. `We cannot {{- template "aa"}}`,
  2609. }
  2610. for _, disallowedTemplate := range disallowedTemplates {
  2611. messageTemplate := disallowedTemplate
  2612. t.Run(disallowedTemplate, func(t *testing.T) {
  2613. t.Parallel()
  2614. response := request(t, s, "PUT", `/mytopic`, `{}`, map[string]string{
  2615. "Template": "yes",
  2616. "Message": messageTemplate,
  2617. })
  2618. require.Equal(t, 400, response.Code)
  2619. require.Equal(t, 40044, toHTTPError(t, response.Body.String()).Code)
  2620. })
  2621. }
  2622. }
  2623. func TestServer_MessageTemplate_SprigFunctions(t *testing.T) {
  2624. t.Parallel()
  2625. s := newTestServer(t, newTestConfig(t))
  2626. bodies := []string{
  2627. `{"foo":"bar","nested":{"title":"here"}}`,
  2628. `{"topic":"ntfy-test"}`,
  2629. `{"topic":"another-topic"}`,
  2630. }
  2631. templates := []string{
  2632. `{{.foo | upper}} is {{.nested.title | repeat 3}}`,
  2633. `{{if hasPrefix "ntfy-" .topic}}Topic: {{trimPrefix "ntfy-" .topic}}{{ else }}Topic: {{.topic}}{{end}}`,
  2634. `{{if hasPrefix "ntfy-" .topic}}Topic: {{trimPrefix "ntfy-" .topic}}{{ else }}Topic: {{.topic}}{{end}}`,
  2635. }
  2636. targets := []string{
  2637. `BAR is hereherehere`,
  2638. `Topic: test`,
  2639. `Topic: another-topic`,
  2640. }
  2641. for i, body := range bodies {
  2642. template := templates[i]
  2643. target := targets[i]
  2644. t.Run(template, func(t *testing.T) {
  2645. response := request(t, s, "PUT", `/mytopic`, body, map[string]string{
  2646. "Template": "yes",
  2647. "Message": template,
  2648. })
  2649. require.Equal(t, 200, response.Code)
  2650. m := toMessage(t, response.Body.String())
  2651. require.Equal(t, target, m.Message)
  2652. })
  2653. }
  2654. }
  2655. func TestServer_MessageTemplate_UnsafeSprigFunctions(t *testing.T) {
  2656. t.Parallel()
  2657. s := newTestServer(t, newTestConfig(t))
  2658. response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{
  2659. "X-Message": `{{ env "PATH" }}`,
  2660. "X-Template": "1",
  2661. })
  2662. require.Equal(t, 400, response.Code)
  2663. require.Equal(t, 40043, toHTTPError(t, response.Body.String()).Code)
  2664. }
  2665. func TestServer_MessageTemplate_InlineNewlines(t *testing.T) {
  2666. t.Parallel()
  2667. s := newTestServer(t, newTestConfig(t))
  2668. response := request(t, s, "PUT", "/mytopic", `{}`, map[string]string{
  2669. "X-Message": `{{"New\nlines"}}`,
  2670. "X-Title": `{{"New\nlines"}}`,
  2671. "X-Template": "1",
  2672. })
  2673. require.Equal(t, 200, response.Code)
  2674. m := toMessage(t, response.Body.String())
  2675. require.Equal(t, `New
  2676. lines`, m.Message)
  2677. require.Equal(t, `New
  2678. lines`, m.Title)
  2679. }
  2680. func TestServer_MessageTemplate_InlineNewlinesOutsideOfTemplate(t *testing.T) {
  2681. t.Parallel()
  2682. s := newTestServer(t, newTestConfig(t))
  2683. response := request(t, s, "PUT", "/mytopic", `{"foo":"bar","food":"bag"}`, map[string]string{
  2684. "X-Message": `{{.foo}}{{"\n"}}{{.food}}`,
  2685. "X-Title": `{{.food}}{{"\n"}}{{.foo}}`,
  2686. "X-Template": "1",
  2687. })
  2688. require.Equal(t, 200, response.Code)
  2689. m := toMessage(t, response.Body.String())
  2690. require.Equal(t, `bar
  2691. bag`, m.Message)
  2692. require.Equal(t, `bag
  2693. bar`, m.Title)
  2694. }
  2695. func TestServer_MessageTemplate_TemplateFileNewlines(t *testing.T) {
  2696. t.Parallel()
  2697. c := newTestConfig(t)
  2698. c.TemplateDir = t.TempDir()
  2699. require.NoError(t, os.WriteFile(filepath.Join(c.TemplateDir, "newline.yml"), []byte(`
  2700. title: |
  2701. {{.food}}{{"\n"}}{{.foo}}
  2702. message: |
  2703. {{.foo}}{{"\n"}}{{.food}}
  2704. `), 0644))
  2705. s := newTestServer(t, c)
  2706. response := request(t, s, "POST", "/mytopic?template=newline", `{"foo":"bar","food":"bag"}`, nil)
  2707. fmt.Println(response.Body.String())
  2708. require.Equal(t, 200, response.Code)
  2709. m := toMessage(t, response.Body.String())
  2710. require.Equal(t, `bar
  2711. bag`, m.Message)
  2712. require.Equal(t, `bag
  2713. bar`, m.Title)
  2714. }
  2715. var (
  2716. //go:embed testdata/webhook_github_comment_created.json
  2717. githubCommentCreatedJSON string
  2718. //go:embed testdata/webhook_github_issue_opened.json
  2719. githubIssueOpenedJSON string
  2720. )
  2721. func TestServer_MessageTemplate_FromNamedTemplate_GitHubCommentCreated(t *testing.T) {
  2722. t.Parallel()
  2723. s := newTestServer(t, newTestConfig(t))
  2724. response := request(t, s, "POST", "/mytopic?template=github", githubCommentCreatedJSON, nil)
  2725. require.Equal(t, 200, response.Code)
  2726. m := toMessage(t, response.Body.String())
  2727. require.Equal(t, "💬 New comment on issue #1389 instant alerts without Pull to refresh", m.Title)
  2728. require.Equal(t, `Commenter: https://github.com/wunter8
  2729. Repository: https://github.com/binwiederhier/ntfy
  2730. Comment link: https://github.com/binwiederhier/ntfy/issues/1389#issuecomment-3078214289
  2731. Comment:
  2732. These are the things you need to do to get iOS push notifications to work:
  2733. 1. open a browser to the web app of your ntfy instance and copy the URL (including "http://" or "https://", your domain or IP address, and any ports, and excluding any trailing slashes)
  2734. 2. put the URL you copied in the ntfy `+"`"+`base-url`+"`"+` config in server.yml or NTFY_BASE_URL in env variables
  2735. 3. put the URL you copied in the default server URL setting in the iOS ntfy app
  2736. 4. set `+"`"+`upstream-base-url`+"`"+` in server.yml or NTFY_UPSTREAM_BASE_URL in env variables to "https://ntfy.sh" (without a trailing slash)`, m.Message)
  2737. }
  2738. func TestServer_MessageTemplate_FromNamedTemplate_GitHubIssueOpened(t *testing.T) {
  2739. t.Parallel()
  2740. s := newTestServer(t, newTestConfig(t))
  2741. response := request(t, s, "POST", "/mytopic?template=github", githubIssueOpenedJSON, nil)
  2742. require.Equal(t, 200, response.Code)
  2743. m := toMessage(t, response.Body.String())
  2744. require.Equal(t, "🐛 Issue opened: #1391 http 500 error (ntfy error 50001)", m.Title)
  2745. require.Equal(t, `Opened by: https://github.com/TheUser-dev
  2746. Repository: https://github.com/binwiederhier/ntfy
  2747. Issue link: https://github.com/binwiederhier/ntfy/issues/1391
  2748. Labels: 🪲 bug
  2749. Description:
  2750. :lady_beetle: **Describe the bug**
  2751. When sending a notification (especially when it happens with multiple requests) this error occurs
  2752. :computer: **Components impacted**
  2753. ntfy server 2.13.0 in docker, debian 12 arm64
  2754. :bulb: **Screenshots and/or logs**
  2755. `+"```"+`
  2756. closed with HTTP 500 (ntfy error 50001) (error=database table is locked, http_method=POST, http_path=/_matrix/push/v1/notify, tag=http, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=30, visitor_id=ip:<edited>, visitor_ip=<edited>, visitor_messages=448, visitor_messages_limit=17280, visitor_messages_remaining=16832, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=57.049697891799994, visitor_seen=2025-07-16T15:06:35.429Z)
  2757. `+"```"+`
  2758. :crystal_ball: **Additional context**
  2759. Looks like this has already been fixed by #498, regression?`, m.Message)
  2760. }
  2761. func TestServer_MessageTemplate_FromNamedTemplate_GitHubIssueOpened_OverrideConfigTemplate(t *testing.T) {
  2762. t.Parallel()
  2763. c := newTestConfig(t)
  2764. c.TemplateDir = t.TempDir()
  2765. require.NoError(t, os.WriteFile(filepath.Join(c.TemplateDir, "github.yml"), []byte(`
  2766. title: |
  2767. Custom title: action={{ .action }} trunctitle={{ .issue.title | trunc 10 }}
  2768. message: |
  2769. Custom message {{ .issue.number }}
  2770. `), 0644))
  2771. s := newTestServer(t, c)
  2772. response := request(t, s, "POST", "/mytopic?template=github", githubIssueOpenedJSON, nil)
  2773. fmt.Println(response.Body.String())
  2774. require.Equal(t, 200, response.Code)
  2775. m := toMessage(t, response.Body.String())
  2776. require.Equal(t, "Custom title: action=opened trunctitle=http 500 e", m.Title)
  2777. require.Equal(t, "Custom message 1391", m.Message)
  2778. }
  2779. func TestServer_MessageTemplate_Repeat9999_TooLarge(t *testing.T) {
  2780. t.Parallel()
  2781. s := newTestServer(t, newTestConfig(t))
  2782. response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{
  2783. "X-Message": `{{ repeat 9999 "mystring" }}`,
  2784. "X-Template": "1",
  2785. })
  2786. require.Equal(t, 400, response.Code)
  2787. require.Equal(t, 40041, toHTTPError(t, response.Body.String()).Code)
  2788. require.Contains(t, toHTTPError(t, response.Body.String()).Message, "message or title is too large after replacing template")
  2789. }
  2790. func TestServer_MessageTemplate_Repeat10001_TooLarge(t *testing.T) {
  2791. t.Parallel()
  2792. s := newTestServer(t, newTestConfig(t))
  2793. response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{
  2794. "X-Message": `{{ repeat 10001 "mystring" }}`,
  2795. "X-Template": "1",
  2796. })
  2797. require.Equal(t, 400, response.Code)
  2798. require.Equal(t, 40045, toHTTPError(t, response.Body.String()).Code)
  2799. require.Contains(t, toHTTPError(t, response.Body.String()).Message, "repeat count 10001 exceeds limit of 10000")
  2800. }
  2801. func TestServer_MessageTemplate_Until100_000(t *testing.T) {
  2802. t.Parallel()
  2803. s := newTestServer(t, newTestConfig(t))
  2804. response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{
  2805. "X-Message": `{{ range $i, $e := until 100_000 }}{{end}}`,
  2806. "X-Template": "1",
  2807. })
  2808. require.Equal(t, 400, response.Code)
  2809. require.Equal(t, 40045, toHTTPError(t, response.Body.String()).Code)
  2810. require.Contains(t, toHTTPError(t, response.Body.String()).Message, "too many iterations")
  2811. }
  2812. func TestServer_DeleteMessage(t *testing.T) {
  2813. t.Parallel()
  2814. s := newTestServer(t, newTestConfig(t))
  2815. // Publish a message with a sequence ID
  2816. response := request(t, s, "PUT", "/mytopic/seq123", "original message", nil)
  2817. require.Equal(t, 200, response.Code)
  2818. msg := toMessage(t, response.Body.String())
  2819. require.Equal(t, "seq123", msg.SequenceID)
  2820. require.Equal(t, "message", msg.Event)
  2821. // Delete the message using DELETE method
  2822. response = request(t, s, "DELETE", "/mytopic/seq123", "", nil)
  2823. require.Equal(t, 200, response.Code)
  2824. deleteMsg := toMessage(t, response.Body.String())
  2825. require.Equal(t, "seq123", deleteMsg.SequenceID)
  2826. require.Equal(t, "message_delete", deleteMsg.Event)
  2827. // Poll and verify both messages are returned
  2828. response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  2829. require.Equal(t, 200, response.Code)
  2830. lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n")
  2831. require.Equal(t, 2, len(lines))
  2832. msg1 := toMessage(t, lines[0])
  2833. msg2 := toMessage(t, lines[1])
  2834. require.Equal(t, "message", msg1.Event)
  2835. require.Equal(t, "message_delete", msg2.Event)
  2836. require.Equal(t, "seq123", msg1.SequenceID)
  2837. require.Equal(t, "seq123", msg2.SequenceID)
  2838. }
  2839. func TestServer_ClearMessage(t *testing.T) {
  2840. t.Parallel()
  2841. s := newTestServer(t, newTestConfig(t))
  2842. // Publish a message with a sequence ID
  2843. response := request(t, s, "PUT", "/mytopic/seq456", "original message", nil)
  2844. require.Equal(t, 200, response.Code)
  2845. msg := toMessage(t, response.Body.String())
  2846. require.Equal(t, "seq456", msg.SequenceID)
  2847. require.Equal(t, "message", msg.Event)
  2848. // Clear the message using PUT /topic/seq/clear
  2849. response = request(t, s, "PUT", "/mytopic/seq456/clear", "", nil)
  2850. require.Equal(t, 200, response.Code)
  2851. clearMsg := toMessage(t, response.Body.String())
  2852. require.Equal(t, "seq456", clearMsg.SequenceID)
  2853. require.Equal(t, "message_clear", clearMsg.Event)
  2854. // Poll and verify both messages are returned
  2855. response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  2856. require.Equal(t, 200, response.Code)
  2857. lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n")
  2858. require.Equal(t, 2, len(lines))
  2859. msg1 := toMessage(t, lines[0])
  2860. msg2 := toMessage(t, lines[1])
  2861. require.Equal(t, "message", msg1.Event)
  2862. require.Equal(t, "message_clear", msg2.Event)
  2863. require.Equal(t, "seq456", msg1.SequenceID)
  2864. require.Equal(t, "seq456", msg2.SequenceID)
  2865. }
  2866. func TestServer_ClearMessage_ReadEndpoint(t *testing.T) {
  2867. // Test that /topic/seq/read also works
  2868. t.Parallel()
  2869. s := newTestServer(t, newTestConfig(t))
  2870. // Publish a message
  2871. response := request(t, s, "PUT", "/mytopic/seq789", "original message", nil)
  2872. require.Equal(t, 200, response.Code)
  2873. // Clear using /read endpoint
  2874. response = request(t, s, "PUT", "/mytopic/seq789/read", "", nil)
  2875. require.Equal(t, 200, response.Code)
  2876. clearMsg := toMessage(t, response.Body.String())
  2877. require.Equal(t, "seq789", clearMsg.SequenceID)
  2878. require.Equal(t, "message_clear", clearMsg.Event)
  2879. }
  2880. func TestServer_UpdateMessage(t *testing.T) {
  2881. t.Parallel()
  2882. s := newTestServer(t, newTestConfig(t))
  2883. // Publish original message
  2884. response := request(t, s, "PUT", "/mytopic/update-seq", "original message", nil)
  2885. require.Equal(t, 200, response.Code)
  2886. msg1 := toMessage(t, response.Body.String())
  2887. require.Equal(t, "update-seq", msg1.SequenceID)
  2888. require.Equal(t, "original message", msg1.Message)
  2889. // Update the message (same sequence ID, new content)
  2890. response = request(t, s, "PUT", "/mytopic/update-seq", "updated message", nil)
  2891. require.Equal(t, 200, response.Code)
  2892. msg2 := toMessage(t, response.Body.String())
  2893. require.Equal(t, "update-seq", msg2.SequenceID)
  2894. require.Equal(t, "updated message", msg2.Message)
  2895. require.NotEqual(t, msg1.ID, msg2.ID) // Different message IDs
  2896. // Poll and verify both versions are returned
  2897. response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  2898. require.Equal(t, 200, response.Code)
  2899. lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n")
  2900. require.Equal(t, 2, len(lines))
  2901. polledMsg1 := toMessage(t, lines[0])
  2902. polledMsg2 := toMessage(t, lines[1])
  2903. require.Equal(t, "original message", polledMsg1.Message)
  2904. require.Equal(t, "updated message", polledMsg2.Message)
  2905. require.Equal(t, "update-seq", polledMsg1.SequenceID)
  2906. require.Equal(t, "update-seq", polledMsg2.SequenceID)
  2907. }
  2908. func TestServer_UpdateMessage_UsingMessageID(t *testing.T) {
  2909. t.Parallel()
  2910. s := newTestServer(t, newTestConfig(t))
  2911. // Publish original message without a sequence ID
  2912. response := request(t, s, "PUT", "/mytopic", "original message", nil)
  2913. require.Equal(t, 200, response.Code)
  2914. msg1 := toMessage(t, response.Body.String())
  2915. require.NotEmpty(t, msg1.ID)
  2916. require.Empty(t, msg1.SequenceID) // No sequence ID provided
  2917. require.Equal(t, "original message", msg1.Message)
  2918. // Update the message using the message ID as the sequence ID
  2919. response = request(t, s, "PUT", "/mytopic/"+msg1.ID, "updated message", nil)
  2920. require.Equal(t, 200, response.Code)
  2921. msg2 := toMessage(t, response.Body.String())
  2922. require.Equal(t, msg1.ID, msg2.SequenceID) // Message ID is now used as sequence ID
  2923. require.Equal(t, "updated message", msg2.Message)
  2924. require.NotEqual(t, msg1.ID, msg2.ID) // Different message IDs
  2925. // Poll and verify both versions are returned
  2926. response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
  2927. require.Equal(t, 200, response.Code)
  2928. lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n")
  2929. require.Equal(t, 2, len(lines))
  2930. polledMsg1 := toMessage(t, lines[0])
  2931. polledMsg2 := toMessage(t, lines[1])
  2932. require.Equal(t, "original message", polledMsg1.Message)
  2933. require.Equal(t, "updated message", polledMsg2.Message)
  2934. require.Empty(t, polledMsg1.SequenceID) // Original has no sequence ID
  2935. require.Equal(t, msg1.ID, polledMsg2.SequenceID) // Update uses original message ID as sequence ID
  2936. }
  2937. func TestServer_DeleteAndClear_InvalidSequenceID(t *testing.T) {
  2938. t.Parallel()
  2939. s := newTestServer(t, newTestConfig(t))
  2940. // Test invalid sequence ID for delete (returns 404 because route doesn't match)
  2941. response := request(t, s, "DELETE", "/mytopic/invalid*seq", "", nil)
  2942. require.Equal(t, 404, response.Code)
  2943. // Test invalid sequence ID for clear (returns 404 because route doesn't match)
  2944. response = request(t, s, "PUT", "/mytopic/invalid*seq/clear", "", nil)
  2945. require.Equal(t, 404, response.Code)
  2946. }
  2947. func TestServer_DeleteMessage_WithFirebase(t *testing.T) {
  2948. sender := newTestFirebaseSender(10)
  2949. s := newTestServer(t, newTestConfig(t))
  2950. s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true})
  2951. // Publish a message
  2952. response := request(t, s, "PUT", "/mytopic/firebase-seq", "test message", nil)
  2953. require.Equal(t, 200, response.Code)
  2954. time.Sleep(100 * time.Millisecond) // Firebase publishing happens
  2955. require.Equal(t, 1, len(sender.Messages()))
  2956. require.Equal(t, "message", sender.Messages()[0].Data["event"])
  2957. // Delete the message
  2958. response = request(t, s, "DELETE", "/mytopic/firebase-seq", "", nil)
  2959. require.Equal(t, 200, response.Code)
  2960. time.Sleep(100 * time.Millisecond) // Firebase publishing happens
  2961. require.Equal(t, 2, len(sender.Messages()))
  2962. require.Equal(t, "message_delete", sender.Messages()[1].Data["event"])
  2963. require.Equal(t, "firebase-seq", sender.Messages()[1].Data["sequence_id"])
  2964. }
  2965. func TestServer_ClearMessage_WithFirebase(t *testing.T) {
  2966. sender := newTestFirebaseSender(10)
  2967. s := newTestServer(t, newTestConfig(t))
  2968. s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true})
  2969. // Publish a message
  2970. response := request(t, s, "PUT", "/mytopic/firebase-clear-seq", "test message", nil)
  2971. require.Equal(t, 200, response.Code)
  2972. time.Sleep(100 * time.Millisecond)
  2973. require.Equal(t, 1, len(sender.Messages()))
  2974. // Clear the message
  2975. response = request(t, s, "PUT", "/mytopic/firebase-clear-seq/clear", "", nil)
  2976. require.Equal(t, 200, response.Code)
  2977. time.Sleep(100 * time.Millisecond)
  2978. require.Equal(t, 2, len(sender.Messages()))
  2979. require.Equal(t, "message_clear", sender.Messages()[1].Data["event"])
  2980. require.Equal(t, "firebase-clear-seq", sender.Messages()[1].Data["sequence_id"])
  2981. }
  2982. func TestServer_UpdateScheduledMessage(t *testing.T) {
  2983. t.Parallel()
  2984. s := newTestServer(t, newTestConfig(t))
  2985. // Publish a scheduled message (future delivery)
  2986. response := request(t, s, "PUT", "/mytopic/sched-seq?delay=1h", "original scheduled message", nil)
  2987. require.Equal(t, 200, response.Code)
  2988. msg1 := toMessage(t, response.Body.String())
  2989. require.Equal(t, "sched-seq", msg1.SequenceID)
  2990. require.Equal(t, "original scheduled message", msg1.Message)
  2991. // Verify scheduled message exists
  2992. response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil)
  2993. require.Equal(t, 200, response.Code)
  2994. messages := toMessages(t, response.Body.String())
  2995. require.Equal(t, 1, len(messages))
  2996. require.Equal(t, "original scheduled message", messages[0].Message)
  2997. // Update the scheduled message (same sequence ID, new content)
  2998. response = request(t, s, "PUT", "/mytopic/sched-seq?delay=2h", "updated scheduled message", nil)
  2999. require.Equal(t, 200, response.Code)
  3000. msg2 := toMessage(t, response.Body.String())
  3001. require.Equal(t, "sched-seq", msg2.SequenceID)
  3002. require.Equal(t, "updated scheduled message", msg2.Message)
  3003. require.NotEqual(t, msg1.ID, msg2.ID)
  3004. // Verify only the updated message exists (old scheduled was deleted)
  3005. response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil)
  3006. require.Equal(t, 200, response.Code)
  3007. messages = toMessages(t, response.Body.String())
  3008. require.Equal(t, 1, len(messages))
  3009. require.Equal(t, "updated scheduled message", messages[0].Message)
  3010. require.Equal(t, msg2.ID, messages[0].ID)
  3011. }
  3012. func TestServer_DeleteScheduledMessage(t *testing.T) {
  3013. t.Parallel()
  3014. s := newTestServer(t, newTestConfig(t))
  3015. // Publish a scheduled message (future delivery)
  3016. response := request(t, s, "PUT", "/mytopic/delete-sched-seq?delay=1h", "scheduled message to delete", nil)
  3017. require.Equal(t, 200, response.Code)
  3018. msg := toMessage(t, response.Body.String())
  3019. require.Equal(t, "delete-sched-seq", msg.SequenceID)
  3020. // Verify scheduled message exists
  3021. response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil)
  3022. require.Equal(t, 200, response.Code)
  3023. messages := toMessages(t, response.Body.String())
  3024. require.Equal(t, 1, len(messages))
  3025. require.Equal(t, "scheduled message to delete", messages[0].Message)
  3026. // Delete the scheduled message
  3027. response = request(t, s, "DELETE", "/mytopic/delete-sched-seq", "", nil)
  3028. require.Equal(t, 200, response.Code)
  3029. deleteMsg := toMessage(t, response.Body.String())
  3030. require.Equal(t, "delete-sched-seq", deleteMsg.SequenceID)
  3031. require.Equal(t, "message_delete", deleteMsg.Event)
  3032. // Verify scheduled message was deleted, only delete event remains
  3033. response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil)
  3034. require.Equal(t, 200, response.Code)
  3035. messages = toMessages(t, response.Body.String())
  3036. require.Equal(t, 1, len(messages))
  3037. require.Equal(t, "message_delete", messages[0].Event)
  3038. require.Equal(t, "delete-sched-seq", messages[0].SequenceID)
  3039. }
  3040. func TestServer_UpdateScheduledMessage_TopicScoped(t *testing.T) {
  3041. t.Parallel()
  3042. s := newTestServer(t, newTestConfig(t))
  3043. // Publish scheduled messages with same sequence ID in different topics
  3044. response := request(t, s, "PUT", "/topic1/shared-seq?delay=1h", "topic1 scheduled", nil)
  3045. require.Equal(t, 200, response.Code)
  3046. response = request(t, s, "PUT", "/topic2/shared-seq?delay=1h", "topic2 scheduled", nil)
  3047. require.Equal(t, 200, response.Code)
  3048. // Update scheduled message in topic1 only
  3049. response = request(t, s, "PUT", "/topic1/shared-seq?delay=2h", "topic1 updated", nil)
  3050. require.Equal(t, 200, response.Code)
  3051. // Verify topic1 has only the updated message
  3052. response = request(t, s, "GET", "/topic1/json?poll=1&scheduled=1", "", nil)
  3053. require.Equal(t, 200, response.Code)
  3054. messages := toMessages(t, response.Body.String())
  3055. require.Equal(t, 1, len(messages))
  3056. require.Equal(t, "topic1 updated", messages[0].Message)
  3057. // Verify topic2 still has its original scheduled message (not affected)
  3058. response = request(t, s, "GET", "/topic2/json?poll=1&scheduled=1", "", nil)
  3059. require.Equal(t, 200, response.Code)
  3060. messages = toMessages(t, response.Body.String())
  3061. require.Equal(t, 1, len(messages))
  3062. require.Equal(t, "topic2 scheduled", messages[0].Message)
  3063. }
  3064. func TestServer_UpdateScheduledMessage_WithAttachment(t *testing.T) {
  3065. t.Parallel()
  3066. s := newTestServer(t, newTestConfig(t))
  3067. // Publish a scheduled message with an attachment
  3068. content := util.RandomString(5000) // > 4096 to trigger attachment
  3069. response := request(t, s, "PUT", "/mytopic/attach-seq?delay=1h", content, nil)
  3070. require.Equal(t, 200, response.Code)
  3071. msg1 := toMessage(t, response.Body.String())
  3072. require.Equal(t, "attach-seq", msg1.SequenceID)
  3073. require.NotNil(t, msg1.Attachment)
  3074. // Verify attachment file exists
  3075. attachmentFile1 := filepath.Join(s.config.AttachmentCacheDir, msg1.ID)
  3076. require.FileExists(t, attachmentFile1)
  3077. // Update the scheduled message with a new attachment
  3078. newContent := util.RandomString(5000)
  3079. response = request(t, s, "PUT", "/mytopic/attach-seq?delay=2h", newContent, nil)
  3080. require.Equal(t, 200, response.Code)
  3081. msg2 := toMessage(t, response.Body.String())
  3082. require.Equal(t, "attach-seq", msg2.SequenceID)
  3083. require.NotEqual(t, msg1.ID, msg2.ID)
  3084. // Verify old attachment file was deleted
  3085. require.NoFileExists(t, attachmentFile1)
  3086. // Verify new attachment file exists
  3087. attachmentFile2 := filepath.Join(s.config.AttachmentCacheDir, msg2.ID)
  3088. require.FileExists(t, attachmentFile2)
  3089. }
  3090. func TestServer_DeleteScheduledMessage_WithAttachment(t *testing.T) {
  3091. t.Parallel()
  3092. s := newTestServer(t, newTestConfig(t))
  3093. // Publish a scheduled message with an attachment
  3094. content := util.RandomString(5000) // > 4096 to trigger attachment
  3095. response := request(t, s, "PUT", "/mytopic/delete-attach-seq?delay=1h", content, nil)
  3096. require.Equal(t, 200, response.Code)
  3097. msg := toMessage(t, response.Body.String())
  3098. require.Equal(t, "delete-attach-seq", msg.SequenceID)
  3099. require.NotNil(t, msg.Attachment)
  3100. // Verify attachment file exists
  3101. attachmentFile := filepath.Join(s.config.AttachmentCacheDir, msg.ID)
  3102. require.FileExists(t, attachmentFile)
  3103. // Delete the scheduled message
  3104. response = request(t, s, "DELETE", "/mytopic/delete-attach-seq", "", nil)
  3105. require.Equal(t, 200, response.Code)
  3106. deleteMsg := toMessage(t, response.Body.String())
  3107. require.Equal(t, "message_delete", deleteMsg.Event)
  3108. // Verify attachment file was deleted
  3109. require.NoFileExists(t, attachmentFile)
  3110. }
  3111. func newTestConfig(t *testing.T) *Config {
  3112. conf := NewConfig()
  3113. conf.BaseURL = "http://127.0.0.1:12345"
  3114. conf.CacheFile = filepath.Join(t.TempDir(), "cache.db")
  3115. conf.CacheStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;"
  3116. conf.AttachmentCacheDir = t.TempDir()
  3117. conf.TemplateDir = t.TempDir()
  3118. return conf
  3119. }
  3120. func configureAuth(t *testing.T, conf *Config) *Config {
  3121. conf.AuthFile = filepath.Join(t.TempDir(), "user.db")
  3122. conf.AuthStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;"
  3123. conf.AuthBcryptCost = bcrypt.MinCost // This speeds up tests a lot
  3124. return conf
  3125. }
  3126. func newTestConfigWithAuthFile(t *testing.T) *Config {
  3127. conf := newTestConfig(t)
  3128. conf = configureAuth(t, conf)
  3129. return conf
  3130. }
  3131. func newTestServer(t *testing.T, config *Config) *Server {
  3132. server, err := New(config)
  3133. require.Nil(t, err)
  3134. return server
  3135. }
  3136. func request(t *testing.T, s *Server, method, url, body string, headers map[string]string, fn ...func(r *http.Request)) *httptest.ResponseRecorder {
  3137. rr := httptest.NewRecorder()
  3138. r, err := http.NewRequest(method, url, strings.NewReader(body))
  3139. if err != nil {
  3140. t.Fatal(err)
  3141. }
  3142. r.RemoteAddr = "9.9.9.9:1234" // Used for tests
  3143. for k, v := range headers {
  3144. r.Header.Set(k, v)
  3145. }
  3146. for _, f := range fn {
  3147. f(r)
  3148. }
  3149. s.handle(rr, r)
  3150. return rr
  3151. }
  3152. func subscribe(t *testing.T, s *Server, url string, rr *httptest.ResponseRecorder) context.CancelFunc {
  3153. ctx, cancel := context.WithCancel(context.Background())
  3154. req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
  3155. if err != nil {
  3156. t.Fatal(err)
  3157. }
  3158. done := make(chan bool)
  3159. go func() {
  3160. s.handle(rr, req)
  3161. done <- true
  3162. }()
  3163. cancelAndWaitForDone := func() {
  3164. time.Sleep(200 * time.Millisecond)
  3165. cancel()
  3166. <-done
  3167. }
  3168. time.Sleep(200 * time.Millisecond)
  3169. return cancelAndWaitForDone
  3170. }
  3171. func toMessages(t *testing.T, s string) []*message {
  3172. messages := make([]*message, 0)
  3173. scanner := bufio.NewScanner(strings.NewReader(s))
  3174. for scanner.Scan() {
  3175. messages = append(messages, toMessage(t, scanner.Text()))
  3176. }
  3177. return messages
  3178. }
  3179. func toMessage(t *testing.T, s string) *message {
  3180. var m message
  3181. require.Nil(t, json.NewDecoder(strings.NewReader(s)).Decode(&m))
  3182. return &m
  3183. }
  3184. func toHTTPError(t *testing.T, s string) *errHTTP {
  3185. var e errHTTP
  3186. require.Nil(t, json.NewDecoder(strings.NewReader(s)).Decode(&e))
  3187. return &e
  3188. }
  3189. func readAll(t *testing.T, rc io.ReadCloser) string {
  3190. b, err := io.ReadAll(rc)
  3191. if err != nil {
  3192. t.Fatal(err)
  3193. }
  3194. return string(b)
  3195. }
  3196. func waitFor(t *testing.T, f func() bool) {
  3197. waitForWithMaxWait(t, 5*time.Second, f)
  3198. }
  3199. func waitForWithMaxWait(t *testing.T, maxWait time.Duration, f func() bool) {
  3200. start := time.Now()
  3201. for time.Since(start) < maxWait {
  3202. if f() {
  3203. return
  3204. }
  3205. time.Sleep(50 * time.Millisecond)
  3206. }
  3207. t.Fatalf("Function f did not succeed after %v: %v", maxWait, string(debug.Stack()))
  3208. }