X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/48d7bce28b18ecef4c52c98821675605464cecb9..98d137c3e945a09f76e85bd18238558c668fdca4:/telegram_fake_helper_test.go?ds=sidebyside diff --git a/telegram_fake_helper_test.go b/telegram_fake_helper_test.go index f6ccd84..5ed4e2d 100644 --- a/telegram_fake_helper_test.go +++ b/telegram_fake_helper_test.go @@ -2,8 +2,10 @@ package main import ( "encoding/json" + "errors" "fmt" "io" + "mime/multipart" "net/http" "net/http/httptest" "strings" @@ -48,67 +50,41 @@ type TelegramFake struct { images map[string][]byte } -// Add makes update available to getUpdates. -func (f *TelegramFake) Add(update TelegramUpdate) { - f.mutex.Lock() - - f.updates = append(f.updates, update) - f.mutex.Unlock() +// newTelegramFake starts a fake Bot API server that stops at the end of the +// test. +func newTelegramFake(t *testing.T) *TelegramFake { + t.Helper() - select { - case f.added <- struct{}{}: - default: + f := &TelegramFake{ + added: make(chan struct{}, 1), + stopped: make(chan struct{}), } -} - -// Calls returns the calls so far. -func (f *TelegramFake) Calls() []TelegramFakeCall { - f.mutex.Lock() - defer f.mutex.Unlock() - - return append([]TelegramFakeCall(nil), f.calls...) -} - -// Sent returns the texts of the sendMessage calls and the Markdown of the -// sendRichMessage calls that succeeded, in order. -func (f *TelegramFake) Sent() []string { - var texts []string - for _, call := range f.Calls() { - switch { - case !call.ok: - case call.method == "sendMessage": - texts = append(texts, call.params["text"].(string)) - case call.method == "sendRichMessage": - texts = append(texts, call.params["rich_message"].(map[string]any)["markdown"].(string)) - } - } + f.Server = httptest.NewServer(http.HandlerFunc(f.serve)) + t.Cleanup(f.Close) + t.Cleanup(func() { + close(f.stopped) + }) - return texts + return f } -func (f *TelegramFake) Image(id string, data []byte) { - f.mutex.Lock() - defer f.mutex.Unlock() +// newTelegramLocalFake returns the same fake API without network goroutines. +// Its RoundTrip handles requests directly for tests with fake time. +func newTelegramLocalFake(t *testing.T) *TelegramFake { + t.Helper() - if f.images == nil { - f.images = make(map[string][]byte) + f := &TelegramFake{ + Server: &httptest.Server{URL: "https://example.com"}, + added: make(chan struct{}, 1), + stopped: make(chan struct{}), } - f.images[id] = data -} - -// RoundTrip handles a request in memory, so synctest can control its timers. -func (f *TelegramFake) RoundTrip(request *http.Request) (*http.Response, error) { - response := httptest.NewRecorder() - f.serve(response, request) - - ctx := request.Context() - if ctx.Err() != nil { - return nil, ctx.Err() - } + t.Cleanup(func() { + close(f.stopped) + }) - return response.Result(), nil + return f } func (f *TelegramFake) serve(w http.ResponseWriter, r *http.Request) { @@ -125,7 +101,9 @@ func (f *TelegramFake) serve(w http.ResponseWriter, r *http.Request) { return } - _, _ = w.Write(data) + // The client can cancel a media request before this reply is + // written. + w.Write(data) // nolint: errcheck return } @@ -139,7 +117,38 @@ func (f *TelegramFake) serve(w http.ResponseWriter, r *http.Request) { var params map[string]any - err := json.NewDecoder(r.Body).Decode(¶ms) + var err error + + if strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data;") { + err = r.ParseMultipartForm(imageMaxBytes + (1 << 20)) + if err == nil { + params = make(map[string]any) + for key, values := range r.MultipartForm.Value { + params[key] = values[0] + } + + for key, files := range r.MultipartForm.File { + var file multipart.File + + file, err = files[0].Open() + if err != nil { + break + } + + params[key], err = io.ReadAll(file) + err = errors.Join(err, file.Close()) + params[key+"_name"] = files[0].Filename + if err != nil { + break + } + } + + err = errors.Join(err, r.MultipartForm.RemoveAll()) + } + } else { + err = json.NewDecoder(r.Body).Decode(¶ms) + } + if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -181,7 +190,9 @@ func (f *TelegramFake) serve(w http.ResponseWriter, r *http.Request) { wait = 1 } - _, _ = fmt.Fprintf(w, `{"ok":false,"error_code":%d,"description":%q,"parameters":{"retry_after":%d}}`, status, description, wait) + // A test can cancel the request after the handler selects its + // reply. + fmt.Fprintf(w, `{"ok":false,"error_code":%d,"description":%q,"parameters":{"retry_after":%d}}`, status, description, wait) // nolint: errcheck return } @@ -190,7 +201,8 @@ func (f *TelegramFake) serve(w http.ResponseWriter, r *http.Request) { f.record(call) if method == "getFile" { - _ = json.NewEncoder(w).Encode(map[string]any{ + // A cancelled client does not need a response body. + json.NewEncoder(w).Encode(map[string]any{ // nolint: errcheck "ok": true, "result": map[string]any{"file_path": params["file_id"]}, }) @@ -199,7 +211,9 @@ func (f *TelegramFake) serve(w http.ResponseWriter, r *http.Request) { } if method != "getUpdates" { - _, _ = io.WriteString(w, `{"ok":true,"result":true}`) + // A cancelled client does not need a response body. + io.WriteString(w, `{"ok":true,"result":true}`) // nolint: errcheck + return } @@ -215,7 +229,8 @@ func (f *TelegramFake) serve(w http.ResponseWriter, r *http.Request) { updates = f.from(offset) } - _ = json.NewEncoder(w).Encode(map[string]any{ + // A cancelled client does not need a response body. + json.NewEncoder(w).Encode(map[string]any{ // nolint: errcheck "ok": true, "result": updates, }) @@ -250,40 +265,67 @@ func (f *TelegramFake) from(offset int64) []TelegramUpdate { return updates } -// newTelegramFake starts a fake Bot API server that stops at the end of the -// test. -func newTelegramFake(t *testing.T) *TelegramFake { - t.Helper() +// Add makes update available to getUpdates. +func (f *TelegramFake) Add(update TelegramUpdate) { + f.mutex.Lock() - f := &TelegramFake{ - added: make(chan struct{}, 1), - stopped: make(chan struct{}), + f.updates = append(f.updates, update) + f.mutex.Unlock() + + select { + case f.added <- struct{}{}: + default: } +} - f.Server = httptest.NewServer(http.HandlerFunc(f.serve)) - t.Cleanup(f.Close) - t.Cleanup(func() { - close(f.stopped) - }) +// Sent returns the texts of the sendMessage calls and the Markdown of the +// sendRichMessage calls that succeeded, in order. +func (f *TelegramFake) Sent() []string { + var texts []string - return f + for _, call := range f.Calls() { + switch { + case !call.ok: + case call.method == "sendMessage": + texts = append(texts, call.params["text"].(string)) + case call.method == "sendRichMessage": + texts = append(texts, call.params["rich_message"].(map[string]any)["markdown"].(string)) + } + } + + return texts } -// newTelegramLocalFake returns the same fake API without network goroutines. -// Its RoundTrip handles requests directly for tests with fake time. -func newTelegramLocalFake(t *testing.T) *TelegramFake { - t.Helper() +// Calls returns the calls so far. +func (f *TelegramFake) Calls() []TelegramFakeCall { + f.mutex.Lock() + defer f.mutex.Unlock() - f := &TelegramFake{ - Server: &httptest.Server{URL: "https://example.com"}, - added: make(chan struct{}, 1), - stopped: make(chan struct{}), + return append([]TelegramFakeCall(nil), f.calls...) +} + +func (f *TelegramFake) Image(id string, data []byte) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if f.images == nil { + f.images = make(map[string][]byte) } - t.Cleanup(func() { - close(f.stopped) - }) - return f + f.images[id] = data +} + +// RoundTrip handles a request in memory, so synctest can control its timers. +func (f *TelegramFake) RoundTrip(request *http.Request) (*http.Response, error) { + response := httptest.NewRecorder() + f.serve(response, request) + + ctx := request.Context() + if ctx.Err() != nil { + return nil, ctx.Err() + } + + return response.Result(), nil } // telegramOwnerMessage returns a text message of the owner in the private chat.