package tgapi import ( "encoding/json" "io" "mime" "mime/multipart" "reflect" "strings" "testing" ) func TestRich103RoundTrip(t *testing.T) { for _, tc := range []struct { name, data string want any }{ {"inline button", `{"blocks":[{"type":"paragraph","text":{"type":"button","button":{"text":["Go",{"type":"custom_emoji","custom_emoji_id":"123","alternative_text":"!"}],"switch_inline_query":""}}}]}`, RichBlockWrap{}}, {"button row", `{"blocks":[{"type":"buttons","align":"right","buttons":[{"text":"Go","switch_inline_query_current_chat":""},{"text":"Choose","switch_inline_query_chosen_chat":{"query":"x","allow_user_chats":true}}]}]}`, RichBlockButtons{}}, {"expandable quote", `{"blocks":[{"type":"expandable_blockquote","text":{"type":"bold","text":"quote"},"credit":"source"}]}`, RichBlockExpandableBlockQuotation{}}, {"document", `{"blocks":[{"type":"document","document":{"file_id":"file","file_unique_id":"unique","file_name":"notes.txt"},"caption":{"text":"Notes","credit":"Author"}}]}`, RichBlockDocument{}}, {"compact table", `{"blocks":[{"type":"table","cells":[],"is_compact":true}]}`, RichBlockTable{}}, } { t.Run(tc.name, func(t *testing.T) { msg, err := UnmarshalRichMessage([]byte(tc.data)) if err != nil { t.Fatal(err) } if reflect.TypeOf(msg.Blocks[0]) != reflect.TypeOf(tc.want) { t.Fatalf("got %T", msg.Blocks[0]) } data, err := json.Marshal(msg) if err != nil { t.Fatal(err) } // Compare the second decode to avoid unrelated omitted zero-value media fields. again, err := UnmarshalRichMessage(data) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(msg, again) { t.Fatalf("round trip lost data: %s", data) } if strings.Contains(tc.data, `"switch_inline_query":""`) && !strings.Contains(string(data), `"switch_inline_query":""`) { t.Fatalf("lost empty query: %s", data) } }) } } func TestRichButtonActionsRoundTrip(t *testing.T) { for _, action := range []string{ `"url":"https://example.com"`, `"callback_data":"callback","style":"link"`, `"web_app":{"url":"https://example.com"}`, `"login_url":{"url":"https://example.com","forward_text":"Forward","request_write_access":true}`, `"switch_inline_query":""`, `"switch_inline_query_current_chat":""`, `"switch_inline_query_chosen_chat":{"allow_user_chats":true}`, `"copy_text":{"text":"Copy"}`, `"disabled":{}`, } { raw := `{"text":"Button",` + action + `}` var button RichMessageButton if err := json.Unmarshal([]byte(raw), &button); err != nil { t.Fatal(err) } encoded, err := json.Marshal(button) if err != nil { t.Fatal(err) } var expected, actual any if err := json.Unmarshal([]byte(raw), &expected); err != nil { t.Fatal(err) } if err := json.Unmarshal(encoded, &actual); err != nil { t.Fatal(err) } if !reflect.DeepEqual(expected, actual) { t.Fatalf("wire fields changed: %s -> %s", raw, encoded) } var again RichMessageButton if err := json.Unmarshal(encoded, &again); err != nil { t.Fatal(err) } if !reflect.DeepEqual(button, again) { t.Fatalf("action lost: %s", encoded) } if button.Text != RichTextPlain("Button") { t.Fatalf("label lost: %#v", button) } } } func TestRich103Malformed(t *testing.T) { for _, raw := range []string{ `{"type":"button","button":{"text":null,"disabled":{}}}`, } { if _, err := UnmarshalRichText([]byte(raw)); err == nil { t.Fatalf("accepted %s", raw) } } for _, raw := range []string{ `{"type":"buttons","buttons":[{"text":null,"disabled":{}}]}`, `{"type":"document","document":42}`, } { if _, err := UnmarshalRichBlock([]byte(raw)); err == nil { t.Fatalf("accepted %s", raw) } } raw := `"x"` for range 70 { raw = `{"type":"button","button":{"text":` + raw + `,"disabled":{}}}` } if _, err := UnmarshalRichText([]byte(raw)); err == nil { t.Fatal("accepted excessive nesting") } } func TestRichDocumentMultipart(t *testing.T) { params := SendRichMessage{ChatID: 42, RichMessage: InputRichMessage{ HTML: ``, Media: []InputRichMessageMedia{{ID: "notes", Media: InputMedia{Type: InputMediaTypeDocument, Media: "attach://notes"}}}, }} body, contentType := prepareMultipartStream([]UploaderFile{NewUploaderFile("notes.txt", []byte("hello")).SetAttachName("notes")}, params) defer func() { _ = body.Close() }() _, attrs, err := mime.ParseMediaType(contentType) if err != nil { t.Fatal(err) } reader := multipart.NewReader(body, attrs["boundary"]) gotFile, gotMessage := false, false for { part, err := reader.NextPart() if err == io.EOF { break } if err != nil { t.Fatal(err) } data, err := io.ReadAll(part) if err != nil { t.Fatal(err) } switch part.FormName() { case "notes": gotFile = true if part.FileName() != "notes.txt" || string(data) != "hello" { t.Fatalf("bad file part: %s", data) } case "rich_message": gotMessage = true if !strings.Contains(string(data), `"media":"attach://notes"`) || !strings.Contains(string(data), `"type":"document"`) { t.Fatalf("bad rich payload: %s", data) } } } if !gotFile || !gotMessage { t.Fatal("missing multipart parts") } }