github.com/line/line-bot-sdk-go/v7@v7.21.0/linebot/validate_message_test.go (about)

     1  // Copyright 2022 LINE Corporation
     2  //
     3  // LINE Corporation licenses this file to you under the Apache License,
     4  // version 2.0 (the "License"); you may not use this file except in compliance
     5  // with the License. You may obtain a copy of the License at:
     6  //
     7  //   http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    11  // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    12  // License for the specific language governing permissions and limitations
    13  // under the License.
    14  
    15  package linebot
    16  
    17  import (
    18  	"io"
    19  	"net/http"
    20  	"net/http/httptest"
    21  	"reflect"
    22  	"strconv"
    23  	"testing"
    24  )
    25  
    26  func TestValidatePushMessages(t *testing.T) {
    27  	type want struct {
    28  		RequestBody []byte
    29  		Response    *BasicResponse
    30  		Error       error
    31  	}
    32  	testCases := []struct {
    33  		Label        string
    34  		Messages     []SendingMessage
    35  		Response     []byte
    36  		ResponseCode int
    37  		Want         want
    38  	}{
    39  		{
    40  			Label:        "A text message",
    41  			Messages:     []SendingMessage{NewTextMessage("Hello, world")},
    42  			ResponseCode: 200,
    43  			Response:     []byte(`{}`),
    44  			Want: want{
    45  				RequestBody: []byte(`{"messages":[{"type":"text","text":"Hello, world"}]}` + "\n"),
    46  				Response:    &BasicResponse{},
    47  			},
    48  		},
    49  		{
    50  			Label:        "A image message",
    51  			Messages:     []SendingMessage{NewImageMessage("https://example.com/original.jpg", "https://example.com/preview.jpg")},
    52  			ResponseCode: 200,
    53  			Response:     []byte(`{}`),
    54  			Want: want{
    55  				RequestBody: []byte(`{"messages":[{"type":"image","originalContentUrl":"https://example.com/original.jpg","previewImageUrl":"https://example.com/preview.jpg"}]}` + "\n"),
    56  				Response:    &BasicResponse{},
    57  			},
    58  		},
    59  		{
    60  			Label:        "A video message",
    61  			Messages:     []SendingMessage{NewVideoMessage("https://example.com/original.mp4", "https://example.com/preview.jpg")},
    62  			ResponseCode: 200,
    63  			Response:     []byte(`{}`),
    64  			Want: want{
    65  				RequestBody: []byte(`{"messages":[{"type":"video","originalContentUrl":"https://example.com/original.mp4","previewImageUrl":"https://example.com/preview.jpg"}]}` + "\n"),
    66  				Response:    &BasicResponse{},
    67  			},
    68  		},
    69  		{
    70  			Label:        "A audio message",
    71  			Messages:     []SendingMessage{NewAudioMessage("https://example.com/original.m4a", 1000)},
    72  			ResponseCode: 200,
    73  			Response:     []byte(`{}`),
    74  			Want: want{
    75  				RequestBody: []byte(`{"messages":[{"type":"audio","originalContentUrl":"https://example.com/original.m4a","duration":1000}]}` + "\n"),
    76  				Response:    &BasicResponse{},
    77  			},
    78  		},
    79  		{
    80  			Label:        "A location message",
    81  			Messages:     []SendingMessage{NewLocationMessage("title", "address", 35.65910807942215, 139.70372892916203)},
    82  			ResponseCode: 200,
    83  			Response:     []byte(`{}`),
    84  			Want: want{
    85  				RequestBody: []byte(`{"messages":[{"type":"location","title":"title","address":"address","latitude":35.65910807942215,"longitude":139.70372892916203}]}` + "\n"),
    86  				Response:    &BasicResponse{},
    87  			},
    88  		},
    89  		{
    90  			Label:        "A sticker message",
    91  			Messages:     []SendingMessage{NewStickerMessage("1", "1")},
    92  			ResponseCode: 200,
    93  			Response:     []byte(`{}`),
    94  			Want: want{
    95  				RequestBody: []byte(`{"messages":[{"type":"sticker","packageId":"1","stickerId":"1"}]}` + "\n"),
    96  				Response:    &BasicResponse{},
    97  			},
    98  		},
    99  		{
   100  			Label: "A buttons template message",
   101  			Messages: []SendingMessage{
   102  				NewTemplateMessage(
   103  					"this is a buttons template",
   104  					NewButtonsTemplate(
   105  						"https://example.com/bot/images/image.jpg",
   106  						"Menu",
   107  						"Please select",
   108  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "displayText", "", ""),
   109  						NewPostbackAction("Buy", "action=buy&itemid=123", "text", "", "", ""),
   110  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "", InputOptionCloseRichMenu, ""),
   111  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "", InputOptionOpenKeyboard, "text"),
   112  						NewURIAction("View detail", "https://example.com/page/123"),
   113  					),
   114  				),
   115  			},
   116  			ResponseCode: 200,
   117  			Response:     []byte(`{}`),
   118  			Want: want{
   119  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a buttons template","template":{"type":"buttons","thumbnailImageUrl":"https://example.com/bot/images/image.jpg","title":"Menu","text":"Please select","actions":[{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","displayText":"displayText"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","text":"text"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","inputOption":"closeRichMenu"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","inputOption":"openKeyboard","fillInText":"text"},{"type":"uri","label":"View detail","uri":"https://example.com/page/123"}]}}]}` + "\n"),
   120  				Response:    &BasicResponse{},
   121  			},
   122  		},
   123  		{
   124  			Label: "A buttons template message with datetimepicker action",
   125  			Messages: []SendingMessage{
   126  				NewTemplateMessage(
   127  					"this is a buttons template",
   128  					NewButtonsTemplate(
   129  						"https://example.com/bot/images/image.jpg",
   130  						"Menu",
   131  						"Please select a date, time or datetime",
   132  						NewDatetimePickerAction("Date", "action=sel&only=date", "date", "2017-09-01", "2017-09-03", ""),
   133  						NewDatetimePickerAction("Time", "action=sel&only=time", "time", "", "23:59", "00:00"),
   134  						NewDatetimePickerAction("DateTime", "action=sel", "datetime", "2017-09-01T12:00", "", ""),
   135  					),
   136  				),
   137  			},
   138  			ResponseCode: 200,
   139  			Response:     []byte(`{}`),
   140  			Want: want{
   141  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a buttons template","template":{"type":"buttons","thumbnailImageUrl":"https://example.com/bot/images/image.jpg","title":"Menu","text":"Please select a date, time or datetime","actions":[{"type":"datetimepicker","label":"Date","data":"action=sel\u0026only=date","mode":"date","initial":"2017-09-01","max":"2017-09-03"},{"type":"datetimepicker","label":"Time","data":"action=sel\u0026only=time","mode":"time","max":"23:59","min":"00:00"},{"type":"datetimepicker","label":"DateTime","data":"action=sel","mode":"datetime","initial":"2017-09-01T12:00"}]}}]}` + "\n"),
   142  				Response:    &BasicResponse{},
   143  			},
   144  		},
   145  		{
   146  			Label: "A buttons template message without thumbnailImageURL",
   147  			Messages: []SendingMessage{
   148  				NewTemplateMessage(
   149  					"this is a buttons template",
   150  					NewButtonsTemplate(
   151  						"",
   152  						"Menu",
   153  						"Please select",
   154  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "displayText", "", ""),
   155  						NewPostbackAction("Buy", "action=buy&itemid=123", "text", "", "", ""),
   156  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "", InputOptionOpenRichMenu, ""),
   157  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "", InputOptionOpenKeyboard, "text"),
   158  						NewURIAction("View detail", "https://example.com/page/123"),
   159  					),
   160  				),
   161  			},
   162  			ResponseCode: 200,
   163  			Response:     []byte(`{}`),
   164  			Want: want{
   165  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a buttons template","template":{"type":"buttons","title":"Menu","text":"Please select","actions":[{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","displayText":"displayText"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","text":"text"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","inputOption":"openRichMenu"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","inputOption":"openKeyboard","fillInText":"text"},{"type":"uri","label":"View detail","uri":"https://example.com/page/123"}]}}]}` + "\n"),
   166  				Response:    &BasicResponse{},
   167  			},
   168  		},
   169  		{
   170  			Label: "A buttons template message without title",
   171  			Messages: []SendingMessage{
   172  				NewTemplateMessage(
   173  					"this is a buttons template",
   174  					NewButtonsTemplate(
   175  						"https://example.com/bot/images/image.jpg",
   176  						"",
   177  						"Please select",
   178  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "displayText", "", ""),
   179  						NewPostbackAction("Buy", "action=buy&itemid=123", "text", "", "", ""),
   180  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "", InputOptionOpenVoice, ""),
   181  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "", InputOptionOpenKeyboard, "text"),
   182  						NewURIAction("View detail", "https://example.com/page/123"),
   183  					),
   184  				),
   185  			},
   186  			ResponseCode: 200,
   187  			Response:     []byte(`{}`),
   188  			Want: want{
   189  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a buttons template","template":{"type":"buttons","thumbnailImageUrl":"https://example.com/bot/images/image.jpg","text":"Please select","actions":[{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","displayText":"displayText"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","text":"text"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","inputOption":"openVoice"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","inputOption":"openKeyboard","fillInText":"text"},{"type":"uri","label":"View detail","uri":"https://example.com/page/123"}]}}]}` + "\n"),
   190  				Response:    &BasicResponse{},
   191  			},
   192  		},
   193  		{
   194  			Label: "A buttons template message without title, with image options",
   195  			Messages: []SendingMessage{
   196  				NewTemplateMessage(
   197  					"this is a buttons template",
   198  					NewButtonsTemplate(
   199  						"https://example.com/bot/images/image.jpg",
   200  						"",
   201  						"Please select",
   202  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "displayText", "", ""),
   203  						NewPostbackAction("Buy", "action=buy&itemid=123", "text", "", "", ""),
   204  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "", InputOptionOpenKeyboard, ""),
   205  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "", InputOptionOpenKeyboard, "text"),
   206  						NewURIAction("View detail", "https://example.com/page/123"),
   207  					).WithImageOptions("rectangle", "cover", "#FFFFFF"),
   208  				),
   209  			},
   210  			ResponseCode: 200,
   211  			Response:     []byte(`{}`),
   212  			Want: want{
   213  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a buttons template","template":{"type":"buttons","thumbnailImageUrl":"https://example.com/bot/images/image.jpg","imageAspectRatio":"rectangle","imageSize":"cover","imageBackgroundColor":"#FFFFFF","text":"Please select","actions":[{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","displayText":"displayText"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","text":"text"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","inputOption":"openKeyboard"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","inputOption":"openKeyboard","fillInText":"text"},{"type":"uri","label":"View detail","uri":"https://example.com/page/123"}]}}]}` + "\n"),
   214  				Response:    &BasicResponse{},
   215  			},
   216  		},
   217  		{
   218  			Label: "A buttons template message without thumbnailImageURL and title",
   219  			Messages: []SendingMessage{
   220  				NewTemplateMessage(
   221  					"this is a buttons template",
   222  					NewButtonsTemplate(
   223  						"",
   224  						"",
   225  						"Please select",
   226  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "displayText", "", ""),
   227  						NewPostbackAction("Buy", "action=buy&itemid=123", "text", "", "", ""),
   228  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "", InputOptionOpenKeyboard, "text"),
   229  						NewURIAction("View detail", "https://example.com/page/123"),
   230  					),
   231  				),
   232  			},
   233  			ResponseCode: 200,
   234  			Response:     []byte(`{}`),
   235  			Want: want{
   236  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a buttons template","template":{"type":"buttons","text":"Please select","actions":[{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","displayText":"displayText"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","text":"text"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","inputOption":"openKeyboard","fillInText":"text"},{"type":"uri","label":"View detail","uri":"https://example.com/page/123"}]}}]}` + "\n"),
   237  				Response:    &BasicResponse{},
   238  			},
   239  		},
   240  		{
   241  			Label: "A confirm template message",
   242  			Messages: []SendingMessage{
   243  				NewTemplateMessage(
   244  					"this is a confirm template",
   245  					NewConfirmTemplate(
   246  						"Are you sure?",
   247  						NewMessageAction("Yes", "yes"),
   248  						NewMessageAction("No", "no"),
   249  					),
   250  				),
   251  			},
   252  			ResponseCode: 200,
   253  			Response:     []byte(`{}`),
   254  			Want: want{
   255  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a confirm template","template":{"type":"confirm","text":"Are you sure?","actions":[{"type":"message","label":"Yes","text":"yes"},{"type":"message","label":"No","text":"no"}]}}]}` + "\n"),
   256  				Response:    &BasicResponse{},
   257  			},
   258  		},
   259  		{
   260  			Label: "A carousel template message",
   261  			Messages: []SendingMessage{
   262  				NewTemplateMessage(
   263  					"this is a carousel template",
   264  					NewCarouselTemplate(
   265  						NewCarouselColumn(
   266  							"https://example.com/bot/images/item1.jpg",
   267  							"this is menu",
   268  							"description",
   269  							NewPostbackAction("Buy", "action=buy&itemid=111", "", "", "", ""),
   270  							NewPostbackAction("Add to cart", "action=add&itemid=111", "", "", "", ""),
   271  							NewPostbackAction("Buy", "action=buy&itemid=123", "", "", InputOptionOpenKeyboard, "text"),
   272  							NewURIAction("View detail", "https://example.com/page/111"),
   273  						),
   274  					),
   275  				),
   276  			},
   277  			ResponseCode: 200,
   278  			Response:     []byte(`{}`),
   279  			Want: want{
   280  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a carousel template","template":{"type":"carousel","columns":[{"thumbnailImageUrl":"https://example.com/bot/images/item1.jpg","title":"this is menu","text":"description","actions":[{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=111"},{"type":"postback","label":"Add to cart","data":"action=add\u0026itemid=111"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","inputOption":"openKeyboard","fillInText":"text"},{"type":"uri","label":"View detail","uri":"https://example.com/page/111"}]}]}}]}` + "\n"),
   281  				Response:    &BasicResponse{},
   282  			},
   283  		},
   284  		{
   285  			Label: "A carousel template message, with new image options",
   286  			Messages: []SendingMessage{
   287  				NewTemplateMessage(
   288  					"this is a carousel template with imageAspectRatio, imageSize and imageBackgroundColor",
   289  					NewCarouselTemplate(
   290  						NewCarouselColumn(
   291  							"https://example.com/bot/images/item1.jpg",
   292  							"this is menu",
   293  							"description",
   294  							NewPostbackAction("Buy", "action=buy&itemid=111", "", "", "", ""),
   295  							NewPostbackAction("Add to cart", "action=add&itemid=111", "", "", "", ""),
   296  							NewPostbackAction("Buy", "action=buy&itemid=123", "", "", InputOptionOpenKeyboard, "text"),
   297  							NewURIAction("View detail", "https://example.com/page/111"),
   298  						).WithImageOptions("#FFFFFF"),
   299  					).WithImageOptions("rectangle", "cover"),
   300  				),
   301  			},
   302  			ResponseCode: 200,
   303  			Response:     []byte(`{}`),
   304  			Want: want{
   305  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a carousel template with imageAspectRatio, imageSize and imageBackgroundColor","template":{"type":"carousel","columns":[{"thumbnailImageUrl":"https://example.com/bot/images/item1.jpg","imageBackgroundColor":"#FFFFFF","title":"this is menu","text":"description","actions":[{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=111"},{"type":"postback","label":"Add to cart","data":"action=add\u0026itemid=111"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","inputOption":"openKeyboard","fillInText":"text"},{"type":"uri","label":"View detail","uri":"https://example.com/page/111"}]}],"imageAspectRatio":"rectangle","imageSize":"cover"}}]}` + "\n"),
   306  				Response:    &BasicResponse{},
   307  			},
   308  		},
   309  		{
   310  			Label: "A imagecarousel template message",
   311  			Messages: []SendingMessage{
   312  				NewTemplateMessage(
   313  					"this is a image carousel template",
   314  					NewImageCarouselTemplate(
   315  						NewImageCarouselColumn(
   316  							"https://example.com/bot/images/item1.jpg",
   317  							NewURIAction("View detail", "https://example.com/page/111"),
   318  						),
   319  					),
   320  				),
   321  			},
   322  			ResponseCode: 200,
   323  			Response:     []byte(`{}`),
   324  			Want: want{
   325  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a image carousel template","template":{"type":"image_carousel","columns":[{"imageUrl":"https://example.com/bot/images/item1.jpg","action":{"type":"uri","label":"View detail","uri":"https://example.com/page/111"}}]}}]}` + "\n"),
   326  				Response:    &BasicResponse{},
   327  			},
   328  		},
   329  		{
   330  			Label: "A imagemap message",
   331  			Messages: []SendingMessage{
   332  				NewImagemapMessage(
   333  					"https://example.com/bot/images/rm001",
   334  					"this is an imagemap",
   335  					ImagemapBaseSize{1040, 1040},
   336  					NewURIImagemapAction("example", "https://example.com/", ImagemapArea{520, 0, 520, 1040}),
   337  					NewMessageImagemapAction("hello", "hello", ImagemapArea{520, 0, 520, 1040}),
   338  				),
   339  			},
   340  			ResponseCode: 200,
   341  			Response:     []byte(`{}`),
   342  			Want: want{
   343  				RequestBody: []byte(`{"messages":[{"type":"imagemap","baseUrl":"https://example.com/bot/images/rm001","altText":"this is an imagemap","baseSize":{"width":1040,"height":1040},"actions":[{"type":"uri","label":"example","linkUri":"https://example.com/","area":{"x":520,"y":0,"width":520,"height":1040}},{"type":"message","label":"hello","text":"hello","area":{"x":520,"y":0,"width":520,"height":1040}}]}]}` + "\n"),
   344  				Response:    &BasicResponse{},
   345  			},
   346  		},
   347  		{
   348  			Label: "A imagemap messages with video 1",
   349  			Messages: []SendingMessage{
   350  				NewImagemapMessage(
   351  					"https://example.com/bot/images/rm001",
   352  					"this is an imagemap with video",
   353  					ImagemapBaseSize{1040, 1040},
   354  					NewURIImagemapAction("example", "https://example.com/", ImagemapArea{520, 0, 520, 1040}),
   355  					NewMessageImagemapAction("hello", "hello", ImagemapArea{520, 0, 520, 1040}),
   356  				).WithVideo(&ImagemapVideo{
   357  					OriginalContentURL: "https://example.com/original.mp4",
   358  					PreviewImageURL:    "https://example.com/preview.jpg",
   359  					Area:               ImagemapArea{10, 10, 100, 200},
   360  				}),
   361  			},
   362  			ResponseCode: 200,
   363  			Response:     []byte(`{}`),
   364  			Want: want{
   365  				RequestBody: []byte(`{"messages":[{"type":"imagemap","baseUrl":"https://example.com/bot/images/rm001","altText":"this is an imagemap with video","baseSize":{"width":1040,"height":1040},"actions":[{"type":"uri","label":"example","linkUri":"https://example.com/","area":{"x":520,"y":0,"width":520,"height":1040}},{"type":"message","label":"hello","text":"hello","area":{"x":520,"y":0,"width":520,"height":1040}}],"video":{"originalContentUrl":"https://example.com/original.mp4","previewImageUrl":"https://example.com/preview.jpg","area":{"x":10,"y":10,"width":100,"height":200}}}]}` + "\n"),
   366  				Response:    &BasicResponse{},
   367  			},
   368  		},
   369  		{
   370  			Label: "A imagemap messages with video 2",
   371  			Messages: []SendingMessage{
   372  				NewImagemapMessage(
   373  					"https://example.com/bot/images/rm001",
   374  					"this is an imagemap with video and external link",
   375  					ImagemapBaseSize{1040, 1040},
   376  					NewURIImagemapAction("example", "https://example.com/", ImagemapArea{520, 0, 520, 1040}),
   377  					NewMessageImagemapAction("hello", "hello", ImagemapArea{520, 0, 520, 1040}),
   378  				).WithVideo(&ImagemapVideo{
   379  					OriginalContentURL: "https://example.com/original.mp4",
   380  					PreviewImageURL:    "https://example.com/preview.jpg",
   381  					Area:               ImagemapArea{10, 10, 100, 200},
   382  					ExternalLink: &ImagemapVideoExternalLink{
   383  						LinkURI: "https://example.com/",
   384  						Label:   "external link",
   385  					},
   386  				}),
   387  			},
   388  			ResponseCode: 200,
   389  			Response:     []byte(`{}`),
   390  			Want: want{
   391  				RequestBody: []byte(`{"messages":[{"type":"imagemap","baseUrl":"https://example.com/bot/images/rm001","altText":"this is an imagemap with video and external link","baseSize":{"width":1040,"height":1040},"actions":[{"type":"uri","label":"example","linkUri":"https://example.com/","area":{"x":520,"y":0,"width":520,"height":1040}},{"type":"message","label":"hello","text":"hello","area":{"x":520,"y":0,"width":520,"height":1040}}],"video":{"originalContentUrl":"https://example.com/original.mp4","previewImageUrl":"https://example.com/preview.jpg","area":{"x":10,"y":10,"width":100,"height":200},"externalLink":{"linkUri":"https://example.com/","label":"external link"}}}]}` + "\n"),
   392  				Response:    &BasicResponse{},
   393  			},
   394  		},
   395  		{
   396  			Label: "A flex message",
   397  			Messages: []SendingMessage{
   398  				NewFlexMessage(
   399  					"this is a flex message",
   400  					&BubbleContainer{
   401  						Type: FlexContainerTypeBubble,
   402  						Body: &BoxComponent{
   403  							Type:   FlexComponentTypeBox,
   404  							Layout: FlexBoxLayoutTypeVertical,
   405  							Contents: []FlexComponent{
   406  								&TextComponent{
   407  									Type: FlexComponentTypeText,
   408  									Text: "hello",
   409  								},
   410  								&TextComponent{
   411  									Type: FlexComponentTypeText,
   412  									Text: "world",
   413  									Flex: IntPtr(0),
   414  								},
   415  								&SpacerComponent{
   416  									Type: FlexComponentTypeSpacer,
   417  								},
   418  							},
   419  						},
   420  					},
   421  				),
   422  			},
   423  			ResponseCode: 200,
   424  			Response:     []byte(`{}`),
   425  			Want: want{
   426  				RequestBody: []byte(`{"messages":[{"type":"flex","altText":"this is a flex message","contents":{"type":"bubble","body":{"type":"box","layout":"vertical","contents":[{"type":"text","text":"hello"},{"type":"text","text":"world","flex":0},{"type":"spacer"}]}}}]}` + "\n"),
   427  				Response:    &BasicResponse{},
   428  			},
   429  		},
   430  		{
   431  			Label: "A text message with quick replies",
   432  			Messages: []SendingMessage{
   433  				NewTextMessage(
   434  					"Select your favorite food category or send me your location!",
   435  				).WithQuickReplies(
   436  					NewQuickReplyItems(
   437  						NewQuickReplyButton("https://example.com/sushi.png", NewMessageAction("Sushi", "Sushi")),
   438  						NewQuickReplyButton("https://example.com/tempura.png", NewMessageAction("Tempura", "Tempura")),
   439  						NewQuickReplyButton("", NewLocationAction("Send location")),
   440  						NewQuickReplyButton("", NewCameraAction("Open camera")),
   441  						NewQuickReplyButton("", NewURIAction("Phone order", "tel:09001234567")),
   442  					),
   443  				),
   444  			},
   445  			ResponseCode: 200,
   446  			Response:     []byte(`{}`),
   447  			Want: want{
   448  				RequestBody: []byte(`{"messages":[{"type":"text","text":"Select your favorite food category or send me your location!","quickReply":{"items":[{"type":"action","imageUrl":"https://example.com/sushi.png","action":{"type":"message","label":"Sushi","text":"Sushi"}},{"type":"action","imageUrl":"https://example.com/tempura.png","action":{"type":"message","label":"Tempura","text":"Tempura"}},{"type":"action","action":{"type":"location","label":"Send location"}},{"type":"action","action":{"type":"camera","label":"Open camera"}},{"type":"action","action":{"type":"uri","label":"Phone order","uri":"tel:09001234567"}}]}}]}` + "\n"),
   449  				Response:    &BasicResponse{},
   450  			},
   451  		},
   452  		{
   453  			Label:        "Multiple messages",
   454  			Messages:     []SendingMessage{NewTextMessage("Hello, world1"), NewTextMessage("Hello, world2")},
   455  			ResponseCode: 200,
   456  			Response:     []byte(`{}`),
   457  			Want: want{
   458  				RequestBody: []byte(`{"messages":[{"type":"text","text":"Hello, world1"},{"type":"text","text":"Hello, world2"}]}` + "\n"),
   459  				Response:    &BasicResponse{},
   460  			},
   461  		},
   462  		{
   463  			Label:        "Bad request",
   464  			Messages:     []SendingMessage{NewTextMessage(""), NewTextMessage("")},
   465  			ResponseCode: 400,
   466  			Response:     []byte(`{"message":"Request body has 2 error(s).","details":[{"message":"may not be empty","property":"messages[0].text"},{"message":"may not be empty","property":"messages[1].text"}]}`),
   467  			Want: want{
   468  				RequestBody: []byte(`{"messages":[{"type":"text","text":""},{"type":"text","text":""}]}` + "\n"),
   469  				Error: &APIError{
   470  					Code: 400,
   471  					Response: &ErrorResponse{
   472  						Message: "Request body has 2 error(s).",
   473  						Details: []errorResponseDetail{
   474  							{
   475  								Message:  "may not be empty",
   476  								Property: "messages[0].text",
   477  							},
   478  							{
   479  								Message:  "may not be empty",
   480  								Property: "messages[1].text",
   481  							},
   482  						},
   483  					},
   484  				},
   485  			},
   486  		},
   487  	}
   488  
   489  	var currentTestIdx int
   490  	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   491  		defer r.Body.Close()
   492  		tc := testCases[currentTestIdx]
   493  		if r.Method != http.MethodPost {
   494  			t.Errorf("Method %s; want %s", r.Method, http.MethodPost)
   495  		}
   496  		if r.URL.Path != APIEndpointValidatePushMessage {
   497  			t.Errorf("URLPath %s; want %s", r.URL.Path, APIEndpointValidatePushMessage)
   498  		}
   499  		body, err := io.ReadAll(r.Body)
   500  		if err != nil {
   501  			t.Fatal(err)
   502  		}
   503  		if !reflect.DeepEqual(body, tc.Want.RequestBody) {
   504  			t.Errorf("RequestBody %s; want %s", body, tc.Want.RequestBody)
   505  		}
   506  		w.WriteHeader(tc.ResponseCode)
   507  		w.Write(tc.Response)
   508  	}))
   509  	defer server.Close()
   510  
   511  	dataServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   512  		defer r.Body.Close()
   513  		t.Error("Unexpected data API call")
   514  		w.WriteHeader(404)
   515  		w.Write([]byte(`{"message":"Not found"}`))
   516  	}))
   517  	defer dataServer.Close()
   518  
   519  	client, err := mockClient(server, dataServer)
   520  	if err != nil {
   521  		t.Fatal(err)
   522  	}
   523  	for i, tc := range testCases {
   524  		currentTestIdx = i
   525  		t.Run(strconv.Itoa(i)+"/"+tc.Label, func(t *testing.T) {
   526  			res, err := client.ValidatePushMessage(tc.Messages...).Do()
   527  			if tc.Want.Error != nil {
   528  				if !reflect.DeepEqual(err, tc.Want.Error) {
   529  					t.Errorf("Error %v; want %v", err, tc.Want.Error)
   530  				}
   531  			} else {
   532  				if err != nil {
   533  					t.Error(err)
   534  				}
   535  			}
   536  			if tc.Want.Response != nil {
   537  				if !reflect.DeepEqual(res, tc.Want.Response) {
   538  					t.Errorf("Response %v; want %v", res, tc.Want.Response)
   539  				}
   540  			}
   541  		})
   542  	}
   543  }
   544  
   545  func TestValidateReplyMessages(t *testing.T) {
   546  	type want struct {
   547  		RequestBody []byte
   548  		Response    *BasicResponse
   549  		Error       error
   550  	}
   551  	testCases := []struct {
   552  		Label        string
   553  		Messages     []SendingMessage
   554  		Response     []byte
   555  		ResponseCode int
   556  		Want         want
   557  	}{
   558  		{
   559  			Label:        "A text message",
   560  			Messages:     []SendingMessage{NewTextMessage("Hello, world")},
   561  			ResponseCode: 200,
   562  			Response:     []byte(`{}`),
   563  			Want: want{
   564  				RequestBody: []byte(`{"messages":[{"type":"text","text":"Hello, world"}]}` + "\n"),
   565  				Response:    &BasicResponse{},
   566  			},
   567  		},
   568  		{
   569  			Label:        "A location message",
   570  			Messages:     []SendingMessage{NewLocationMessage("title", "address", 35.65910807942215, 139.70372892916203)},
   571  			ResponseCode: 200,
   572  			Response:     []byte(`{}`),
   573  			Want: want{
   574  				RequestBody: []byte(`{"messages":[{"type":"location","title":"title","address":"address","latitude":35.65910807942215,"longitude":139.70372892916203}]}` + "\n"),
   575  				Response:    &BasicResponse{},
   576  			},
   577  		},
   578  		{
   579  			Label:        "A image message",
   580  			Messages:     []SendingMessage{NewImageMessage("https://example.com/original.jpg", "https://example.com/preview.jpg")},
   581  			ResponseCode: 200,
   582  			Response:     []byte(`{}`),
   583  			Want: want{
   584  				RequestBody: []byte(`{"messages":[{"type":"image","originalContentUrl":"https://example.com/original.jpg","previewImageUrl":"https://example.com/preview.jpg"}]}` + "\n"),
   585  				Response:    &BasicResponse{},
   586  			},
   587  		},
   588  		{
   589  			Label:        "A sticker message",
   590  			Messages:     []SendingMessage{NewStickerMessage("1", "1")},
   591  			ResponseCode: 200,
   592  			Response:     []byte(`{}`),
   593  			Want: want{
   594  				RequestBody: []byte(`{"messages":[{"type":"sticker","packageId":"1","stickerId":"1"}]}` + "\n"),
   595  				Response:    &BasicResponse{},
   596  			},
   597  		},
   598  		{
   599  			Label:        "Bad request",
   600  			Messages:     []SendingMessage{NewTextMessage(""), NewTextMessage("")},
   601  			ResponseCode: 400,
   602  			Response:     []byte(`{"message":"Request body has 2 error(s).","details":[{"message":"may not be empty","property":"messages[0].text"},{"message":"may not be empty","property":"messages[1].text"}]}`),
   603  			Want: want{
   604  				RequestBody: []byte(`{"messages":[{"type":"text","text":""},{"type":"text","text":""}]}` + "\n"),
   605  				Error: &APIError{
   606  					Code: 400,
   607  					Response: &ErrorResponse{
   608  						Message: "Request body has 2 error(s).",
   609  						Details: []errorResponseDetail{
   610  							{
   611  								Message:  "may not be empty",
   612  								Property: "messages[0].text",
   613  							},
   614  							{
   615  								Message:  "may not be empty",
   616  								Property: "messages[1].text",
   617  							},
   618  						},
   619  					},
   620  				},
   621  			},
   622  		},
   623  	}
   624  
   625  	var currentTestIdx int
   626  	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   627  		defer r.Body.Close()
   628  		if r.Method != http.MethodPost {
   629  			t.Errorf("Method %s; want %s", r.Method, http.MethodPost)
   630  		}
   631  		if r.URL.Path != APIEndpointValidateReplyMessage {
   632  			t.Errorf("URLPath %s; want %s", r.URL.Path, APIEndpointValidateReplyMessage)
   633  		}
   634  		body, err := io.ReadAll(r.Body)
   635  		if err != nil {
   636  			t.Fatal(err)
   637  		}
   638  		tc := testCases[currentTestIdx]
   639  		if !reflect.DeepEqual(body, tc.Want.RequestBody) {
   640  			t.Errorf("RequestBody %s; want %s", body, tc.Want.RequestBody)
   641  		}
   642  		w.WriteHeader(tc.ResponseCode)
   643  		w.Write(tc.Response)
   644  	}))
   645  	defer server.Close()
   646  
   647  	dataServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   648  		defer r.Body.Close()
   649  		t.Error("Unexpected data API call")
   650  		w.WriteHeader(404)
   651  		w.Write([]byte(`{"message":"Not found"}`))
   652  	}))
   653  	defer dataServer.Close()
   654  
   655  	client, err := mockClient(server, dataServer)
   656  	if err != nil {
   657  		t.Fatal(err)
   658  	}
   659  	for i, tc := range testCases {
   660  		currentTestIdx = i
   661  		t.Run(strconv.Itoa(i)+"/"+tc.Label, func(t *testing.T) {
   662  			res, err := client.ValidateReplyMessage(tc.Messages...).Do()
   663  			if tc.Want.Error != nil {
   664  				if !reflect.DeepEqual(err, tc.Want.Error) {
   665  					t.Errorf("Error %v; want %v", err, tc.Want.Error)
   666  				}
   667  			} else {
   668  				if err != nil {
   669  					t.Error(err)
   670  				}
   671  			}
   672  			if tc.Want.Response != nil {
   673  				if !reflect.DeepEqual(res, tc.Want.Response) {
   674  					t.Errorf("Response %v; want %v", res, tc.Want.Response)
   675  				}
   676  			}
   677  		})
   678  	}
   679  }
   680  
   681  func TestValidateMulticastMessages(t *testing.T) {
   682  	type want struct {
   683  		RequestBody []byte
   684  		Response    *BasicResponse
   685  		Error       error
   686  	}
   687  	testCases := []struct {
   688  		Label        string
   689  		Messages     []SendingMessage
   690  		Response     []byte
   691  		ResponseCode int
   692  		Want         want
   693  	}{
   694  		{
   695  			Label:        "A text message",
   696  			Messages:     []SendingMessage{NewTextMessage("Hello, world")},
   697  			ResponseCode: 200,
   698  			Response:     []byte(`{}`),
   699  			Want: want{
   700  				RequestBody: []byte(`{"messages":[{"type":"text","text":"Hello, world"}]}` + "\n"),
   701  				Response:    &BasicResponse{},
   702  			},
   703  		},
   704  		{
   705  			Label:        "A location message",
   706  			Messages:     []SendingMessage{NewLocationMessage("title", "address", 35.65910807942215, 139.70372892916203)},
   707  			ResponseCode: 200,
   708  			Response:     []byte(`{}`),
   709  			Want: want{
   710  				RequestBody: []byte(`{"messages":[{"type":"location","title":"title","address":"address","latitude":35.65910807942215,"longitude":139.70372892916203}]}` + "\n"),
   711  				Response:    &BasicResponse{},
   712  			},
   713  		},
   714  		{
   715  			Label:        "A image message",
   716  			Messages:     []SendingMessage{NewImageMessage("https://example.com/original.jpg", "https://example.com/preview.jpg")},
   717  			ResponseCode: 200,
   718  			Response:     []byte(`{}`),
   719  			Want: want{
   720  				RequestBody: []byte(`{"messages":[{"type":"image","originalContentUrl":"https://example.com/original.jpg","previewImageUrl":"https://example.com/preview.jpg"}]}` + "\n"),
   721  				Response:    &BasicResponse{},
   722  			},
   723  		},
   724  		{
   725  			Label:        "A sticker message",
   726  			Messages:     []SendingMessage{NewStickerMessage("1", "1")},
   727  			ResponseCode: 200,
   728  			Response:     []byte(`{}`),
   729  			Want: want{
   730  				RequestBody: []byte(`{"messages":[{"type":"sticker","packageId":"1","stickerId":"1"}]}` + "\n"),
   731  				Response:    &BasicResponse{},
   732  			},
   733  		},
   734  		{
   735  			Label:        "Bad request",
   736  			Messages:     []SendingMessage{NewTextMessage(""), NewTextMessage("")},
   737  			ResponseCode: 400,
   738  			Response:     []byte(`{"message":"Request body has 2 error(s).","details":[{"message":"may not be empty","property":"messages[0].text"},{"message":"may not be empty","property":"messages[1].text"}]}`),
   739  			Want: want{
   740  				RequestBody: []byte(`{"messages":[{"type":"text","text":""},{"type":"text","text":""}]}` + "\n"),
   741  				Error: &APIError{
   742  					Code: 400,
   743  					Response: &ErrorResponse{
   744  						Message: "Request body has 2 error(s).",
   745  						Details: []errorResponseDetail{
   746  							{
   747  								Message:  "may not be empty",
   748  								Property: "messages[0].text",
   749  							},
   750  							{
   751  								Message:  "may not be empty",
   752  								Property: "messages[1].text",
   753  							},
   754  						},
   755  					},
   756  				},
   757  			},
   758  		},
   759  	}
   760  
   761  	var currentTestIdx int
   762  	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   763  		defer r.Body.Close()
   764  		if r.Method != http.MethodPost {
   765  			t.Errorf("Method %s; want %s", r.Method, http.MethodPost)
   766  		}
   767  		if r.URL.Path != APIEndpointValidateMulticastMessage {
   768  			t.Errorf("URLPath %s; want %s", r.URL.Path, APIEndpointValidateMulticastMessage)
   769  		}
   770  		body, err := io.ReadAll(r.Body)
   771  		if err != nil {
   772  			t.Fatal(err)
   773  		}
   774  		tc := testCases[currentTestIdx]
   775  		if !reflect.DeepEqual(body, tc.Want.RequestBody) {
   776  			t.Errorf("RequestBody %s; want %s", body, tc.Want.RequestBody)
   777  		}
   778  		w.WriteHeader(tc.ResponseCode)
   779  		w.Write(tc.Response)
   780  	}))
   781  	defer server.Close()
   782  
   783  	dataServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   784  		defer r.Body.Close()
   785  		t.Error("Unexpected data API call")
   786  		w.WriteHeader(404)
   787  		w.Write([]byte(`{"message":"Not found"}`))
   788  	}))
   789  	defer dataServer.Close()
   790  
   791  	client, err := mockClient(server, dataServer)
   792  	if err != nil {
   793  		t.Fatal(err)
   794  	}
   795  	for i, tc := range testCases {
   796  		currentTestIdx = i
   797  		t.Run(strconv.Itoa(i)+"/"+tc.Label, func(t *testing.T) {
   798  			res, err := client.ValidateMulticastMessage(tc.Messages...).Do()
   799  			if tc.Want.Error != nil {
   800  				if !reflect.DeepEqual(err, tc.Want.Error) {
   801  					t.Errorf("Error %v; want %v", err, tc.Want.Error)
   802  				}
   803  			} else {
   804  				if err != nil {
   805  					t.Error(err)
   806  				}
   807  			}
   808  			if tc.Want.Response != nil {
   809  				if !reflect.DeepEqual(res, tc.Want.Response) {
   810  					t.Errorf("Response %v; want %v", res, tc.Want.Response)
   811  				}
   812  			}
   813  		})
   814  	}
   815  }
   816  
   817  func TestValidateBroadcastMessages(t *testing.T) {
   818  	type want struct {
   819  		RequestBody []byte
   820  		Response    *BasicResponse
   821  		Error       error
   822  	}
   823  	testCases := []struct {
   824  		Messages     []SendingMessage
   825  		Response     []byte
   826  		ResponseCode int
   827  		Want         want
   828  	}{
   829  		{
   830  			// A text message
   831  			Messages:     []SendingMessage{NewTextMessage("Hello, world")},
   832  			ResponseCode: 200,
   833  			Response:     []byte(`{}`),
   834  			Want: want{
   835  				RequestBody: []byte(`{"messages":[{"type":"text","text":"Hello, world"}]}` + "\n"),
   836  				Response:    &BasicResponse{},
   837  			},
   838  		},
   839  		{
   840  			// A image message
   841  			Messages:     []SendingMessage{NewImageMessage("https://example.com/original.jpg", "https://example.com/preview.jpg")},
   842  			ResponseCode: 200,
   843  			Response:     []byte(`{}`),
   844  			Want: want{
   845  				RequestBody: []byte(`{"messages":[{"type":"image","originalContentUrl":"https://example.com/original.jpg","previewImageUrl":"https://example.com/preview.jpg"}]}` + "\n"),
   846  				Response:    &BasicResponse{},
   847  			},
   848  		},
   849  		{
   850  			// A video message
   851  			Messages:     []SendingMessage{NewVideoMessage("https://example.com/original.mp4", "https://example.com/preview.jpg")},
   852  			ResponseCode: 200,
   853  			Response:     []byte(`{}`),
   854  			Want: want{
   855  				RequestBody: []byte(`{"messages":[{"type":"video","originalContentUrl":"https://example.com/original.mp4","previewImageUrl":"https://example.com/preview.jpg"}]}` + "\n"),
   856  				Response:    &BasicResponse{},
   857  			},
   858  		},
   859  		{
   860  			// A audio message
   861  			Messages:     []SendingMessage{NewAudioMessage("https://example.com/original.m4a", 1000)},
   862  			ResponseCode: 200,
   863  			Response:     []byte(`{}`),
   864  			Want: want{
   865  				RequestBody: []byte(`{"messages":[{"type":"audio","originalContentUrl":"https://example.com/original.m4a","duration":1000}]}` + "\n"),
   866  				Response:    &BasicResponse{},
   867  			},
   868  		},
   869  		{
   870  			// A location message
   871  			Messages:     []SendingMessage{NewLocationMessage("title", "address", 35.65910807942215, 139.70372892916203)},
   872  			ResponseCode: 200,
   873  			Response:     []byte(`{}`),
   874  			Want: want{
   875  				RequestBody: []byte(`{"messages":[{"type":"location","title":"title","address":"address","latitude":35.65910807942215,"longitude":139.70372892916203}]}` + "\n"),
   876  				Response:    &BasicResponse{},
   877  			},
   878  		},
   879  		{
   880  			// A sticker message
   881  			Messages:     []SendingMessage{NewStickerMessage("1", "1")},
   882  			ResponseCode: 200,
   883  			Response:     []byte(`{}`),
   884  			Want: want{
   885  				RequestBody: []byte(`{"messages":[{"type":"sticker","packageId":"1","stickerId":"1"}]}` + "\n"),
   886  				Response:    &BasicResponse{},
   887  			},
   888  		},
   889  		{
   890  			// A buttons template message
   891  			Messages: []SendingMessage{
   892  				NewTemplateMessage(
   893  					"this is a buttons template",
   894  					NewButtonsTemplate(
   895  						"https://example.com/bot/images/image.jpg",
   896  						"Menu",
   897  						"Please select",
   898  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "displayText", "", ""),
   899  						NewPostbackAction("Buy", "action=buy&itemid=123", "text", "", "", ""),
   900  						NewURIAction("View detail", "https://example.com/page/123"),
   901  					),
   902  				),
   903  			},
   904  			ResponseCode: 200,
   905  			Response:     []byte(`{}`),
   906  			Want: want{
   907  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a buttons template","template":{"type":"buttons","thumbnailImageUrl":"https://example.com/bot/images/image.jpg","title":"Menu","text":"Please select","actions":[{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","displayText":"displayText"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","text":"text"},{"type":"uri","label":"View detail","uri":"https://example.com/page/123"}]}}]}` + "\n"),
   908  				Response:    &BasicResponse{},
   909  			},
   910  		},
   911  		{
   912  			// A buttons template message with datetimepicker action
   913  			Messages: []SendingMessage{
   914  				NewTemplateMessage(
   915  					"this is a buttons template",
   916  					NewButtonsTemplate(
   917  						"https://example.com/bot/images/image.jpg",
   918  						"Menu",
   919  						"Please select a date, time or datetime",
   920  						NewDatetimePickerAction("Date", "action=sel&only=date", "date", "2017-09-01", "2017-09-03", ""),
   921  						NewDatetimePickerAction("Time", "action=sel&only=time", "time", "", "23:59", "00:00"),
   922  						NewDatetimePickerAction("DateTime", "action=sel", "datetime", "2017-09-01T12:00", "", ""),
   923  					),
   924  				),
   925  			},
   926  			ResponseCode: 200,
   927  			Response:     []byte(`{}`),
   928  			Want: want{
   929  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a buttons template","template":{"type":"buttons","thumbnailImageUrl":"https://example.com/bot/images/image.jpg","title":"Menu","text":"Please select a date, time or datetime","actions":[{"type":"datetimepicker","label":"Date","data":"action=sel\u0026only=date","mode":"date","initial":"2017-09-01","max":"2017-09-03"},{"type":"datetimepicker","label":"Time","data":"action=sel\u0026only=time","mode":"time","max":"23:59","min":"00:00"},{"type":"datetimepicker","label":"DateTime","data":"action=sel","mode":"datetime","initial":"2017-09-01T12:00"}]}}]}` + "\n"),
   930  				Response:    &BasicResponse{},
   931  			},
   932  		},
   933  		{
   934  			// A buttons template message without thumbnailImageURL
   935  			Messages: []SendingMessage{
   936  				NewTemplateMessage(
   937  					"this is a buttons template",
   938  					NewButtonsTemplate(
   939  						"",
   940  						"Menu",
   941  						"Please select",
   942  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "displayText", "", ""),
   943  						NewPostbackAction("Buy", "action=buy&itemid=123", "text", "", "", ""),
   944  						NewURIAction("View detail", "https://example.com/page/123"),
   945  					),
   946  				),
   947  			},
   948  			ResponseCode: 200,
   949  			Response:     []byte(`{}`),
   950  			Want: want{
   951  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a buttons template","template":{"type":"buttons","title":"Menu","text":"Please select","actions":[{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","displayText":"displayText"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","text":"text"},{"type":"uri","label":"View detail","uri":"https://example.com/page/123"}]}}]}` + "\n"),
   952  				Response:    &BasicResponse{},
   953  			},
   954  		},
   955  		{
   956  			// A buttons template message without title
   957  			Messages: []SendingMessage{
   958  				NewTemplateMessage(
   959  					"this is a buttons template",
   960  					NewButtonsTemplate(
   961  						"https://example.com/bot/images/image.jpg",
   962  						"",
   963  						"Please select",
   964  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "displayText", "", ""),
   965  						NewPostbackAction("Buy", "action=buy&itemid=123", "text", "", "", ""),
   966  						NewURIAction("View detail", "https://example.com/page/123"),
   967  					),
   968  				),
   969  			},
   970  			ResponseCode: 200,
   971  			Response:     []byte(`{}`),
   972  			Want: want{
   973  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a buttons template","template":{"type":"buttons","thumbnailImageUrl":"https://example.com/bot/images/image.jpg","text":"Please select","actions":[{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","displayText":"displayText"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","text":"text"},{"type":"uri","label":"View detail","uri":"https://example.com/page/123"}]}}]}` + "\n"),
   974  				Response:    &BasicResponse{},
   975  			},
   976  		},
   977  		{
   978  			// A buttons template message without title, with image options and default action
   979  			Messages: []SendingMessage{
   980  				NewTemplateMessage(
   981  					"this is a buttons template",
   982  					NewButtonsTemplate(
   983  						"https://example.com/bot/images/image.jpg",
   984  						"",
   985  						"Please select",
   986  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "displayText", "", ""),
   987  						NewPostbackAction("Buy", "action=buy&itemid=123", "text", "", "", ""),
   988  						NewURIAction("View detail", "https://example.com/page/123"),
   989  					).WithImageOptions(
   990  						"rectangle",
   991  						"cover",
   992  						"#FFFFFF",
   993  					).WithDefaultAction(
   994  						NewURIAction("View detail", "https://example.com/page/123"),
   995  					),
   996  				),
   997  			},
   998  			ResponseCode: 200,
   999  			Response:     []byte(`{}`),
  1000  			Want: want{
  1001  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a buttons template","template":{"type":"buttons","thumbnailImageUrl":"https://example.com/bot/images/image.jpg","imageAspectRatio":"rectangle","imageSize":"cover","imageBackgroundColor":"#FFFFFF","text":"Please select","actions":[{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","displayText":"displayText"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","text":"text"},{"type":"uri","label":"View detail","uri":"https://example.com/page/123"}],"defaultAction":{"type":"uri","label":"View detail","uri":"https://example.com/page/123"}}}]}` + "\n"),
  1002  				Response:    &BasicResponse{},
  1003  			},
  1004  		},
  1005  		{
  1006  			// A buttons template message without thumbnailImageURL and title
  1007  			Messages: []SendingMessage{
  1008  				NewTemplateMessage(
  1009  					"this is a buttons template",
  1010  					NewButtonsTemplate(
  1011  						"",
  1012  						"",
  1013  						"Please select",
  1014  						NewPostbackAction("Buy", "action=buy&itemid=123", "", "displayText", "", ""),
  1015  						NewPostbackAction("Buy", "action=buy&itemid=123", "text", "", "", ""),
  1016  						NewURIAction("View detail", "https://example.com/page/123"),
  1017  					),
  1018  				),
  1019  			},
  1020  			ResponseCode: 200,
  1021  			Response:     []byte(`{}`),
  1022  			Want: want{
  1023  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a buttons template","template":{"type":"buttons","text":"Please select","actions":[{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","displayText":"displayText"},{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=123","text":"text"},{"type":"uri","label":"View detail","uri":"https://example.com/page/123"}]}}]}` + "\n"),
  1024  				Response:    &BasicResponse{},
  1025  			},
  1026  		},
  1027  		{
  1028  			// A confirm template message
  1029  			Messages: []SendingMessage{
  1030  				NewTemplateMessage(
  1031  					"this is a confirm template",
  1032  					NewConfirmTemplate(
  1033  						"Are you sure?",
  1034  						NewMessageAction("Yes", "yes"),
  1035  						NewMessageAction("No", "no"),
  1036  					),
  1037  				),
  1038  			},
  1039  			ResponseCode: 200,
  1040  			Response:     []byte(`{}`),
  1041  			Want: want{
  1042  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a confirm template","template":{"type":"confirm","text":"Are you sure?","actions":[{"type":"message","label":"Yes","text":"yes"},{"type":"message","label":"No","text":"no"}]}}]}` + "\n"),
  1043  				Response:    &BasicResponse{},
  1044  			},
  1045  		},
  1046  		{
  1047  			// A carousel template message
  1048  			Messages: []SendingMessage{
  1049  				NewTemplateMessage(
  1050  					"this is a carousel template",
  1051  					NewCarouselTemplate(
  1052  						NewCarouselColumn(
  1053  							"https://example.com/bot/images/item1.jpg",
  1054  							"this is menu",
  1055  							"description",
  1056  							NewPostbackAction("Buy", "action=buy&itemid=111", "", "", "", ""),
  1057  							NewPostbackAction("Add to cart", "action=add&itemid=111", "", "", "", ""),
  1058  							NewURIAction("View detail", "https://example.com/page/111"),
  1059  						),
  1060  					),
  1061  				),
  1062  			},
  1063  			ResponseCode: 200,
  1064  			Response:     []byte(`{}`),
  1065  			Want: want{
  1066  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a carousel template","template":{"type":"carousel","columns":[{"thumbnailImageUrl":"https://example.com/bot/images/item1.jpg","title":"this is menu","text":"description","actions":[{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=111"},{"type":"postback","label":"Add to cart","data":"action=add\u0026itemid=111"},{"type":"uri","label":"View detail","uri":"https://example.com/page/111"}]}]}}]}` + "\n"),
  1067  				Response:    &BasicResponse{},
  1068  			},
  1069  		},
  1070  		{
  1071  			// A carousel template message, with new image options and default action
  1072  			Messages: []SendingMessage{
  1073  				NewTemplateMessage(
  1074  					"this is a carousel template with imageAspectRatio, imageSize and imageBackgroundColor",
  1075  					NewCarouselTemplate(
  1076  						NewCarouselColumn(
  1077  							"https://example.com/bot/images/item1.jpg",
  1078  							"this is menu",
  1079  							"description",
  1080  							NewPostbackAction("Buy", "action=buy&itemid=111", "", "", "", ""),
  1081  							NewPostbackAction("Add to cart", "action=add&itemid=111", "", "", "", ""),
  1082  							NewURIAction("View detail", "https://example.com/page/111"),
  1083  						).WithImageOptions(
  1084  							"#FFFFFF",
  1085  						).WithDefaultAction(
  1086  							NewURIAction("View detail", "http://example.com/page/123"),
  1087  						),
  1088  					).WithImageOptions("rectangle", "cover"),
  1089  				),
  1090  			},
  1091  			ResponseCode: 200,
  1092  			Response:     []byte(`{}`),
  1093  			Want: want{
  1094  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a carousel template with imageAspectRatio, imageSize and imageBackgroundColor","template":{"type":"carousel","columns":[{"thumbnailImageUrl":"https://example.com/bot/images/item1.jpg","imageBackgroundColor":"#FFFFFF","title":"this is menu","text":"description","actions":[{"type":"postback","label":"Buy","data":"action=buy\u0026itemid=111"},{"type":"postback","label":"Add to cart","data":"action=add\u0026itemid=111"},{"type":"uri","label":"View detail","uri":"https://example.com/page/111"}],"defaultAction":{"type":"uri","label":"View detail","uri":"http://example.com/page/123"}}],"imageAspectRatio":"rectangle","imageSize":"cover"}}]}` + "\n"),
  1095  				Response:    &BasicResponse{},
  1096  			},
  1097  		},
  1098  		{
  1099  			// A imagecarousel template message
  1100  			Messages: []SendingMessage{
  1101  				NewTemplateMessage(
  1102  					"this is a image carousel template",
  1103  					NewImageCarouselTemplate(
  1104  						NewImageCarouselColumn(
  1105  							"https://example.com/bot/images/item1.jpg",
  1106  							NewURIAction("View detail", "https://example.com/page/111"),
  1107  						),
  1108  					),
  1109  				),
  1110  			},
  1111  			ResponseCode: 200,
  1112  			Response:     []byte(`{}`),
  1113  			Want: want{
  1114  				RequestBody: []byte(`{"messages":[{"type":"template","altText":"this is a image carousel template","template":{"type":"image_carousel","columns":[{"imageUrl":"https://example.com/bot/images/item1.jpg","action":{"type":"uri","label":"View detail","uri":"https://example.com/page/111"}}]}}]}` + "\n"),
  1115  				Response:    &BasicResponse{},
  1116  			},
  1117  		},
  1118  		{
  1119  			// A imagemap message
  1120  			Messages: []SendingMessage{
  1121  				NewImagemapMessage(
  1122  					"https://example.com/bot/images/rm001",
  1123  					"this is an imagemap",
  1124  					ImagemapBaseSize{1040, 1040},
  1125  					NewURIImagemapAction("example", "https://example.com/", ImagemapArea{520, 0, 520, 1040}),
  1126  					NewMessageImagemapAction("hello", "hello", ImagemapArea{520, 0, 520, 1040}),
  1127  				),
  1128  			},
  1129  			ResponseCode: 200,
  1130  			Response:     []byte(`{}`),
  1131  			Want: want{
  1132  				RequestBody: []byte(`{"messages":[{"type":"imagemap","baseUrl":"https://example.com/bot/images/rm001","altText":"this is an imagemap","baseSize":{"width":1040,"height":1040},"actions":[{"type":"uri","label":"example","linkUri":"https://example.com/","area":{"x":520,"y":0,"width":520,"height":1040}},{"type":"message","label":"hello","text":"hello","area":{"x":520,"y":0,"width":520,"height":1040}}]}]}` + "\n"),
  1133  				Response:    &BasicResponse{},
  1134  			},
  1135  		},
  1136  		{
  1137  			// A imagemap messages with video 1
  1138  			Messages: []SendingMessage{
  1139  				NewImagemapMessage(
  1140  					"https://example.com/bot/images/rm001",
  1141  					"this is an imagemap with video",
  1142  					ImagemapBaseSize{1040, 1040},
  1143  					NewURIImagemapAction("example", "https://example.com/", ImagemapArea{520, 0, 520, 1040}),
  1144  					NewMessageImagemapAction("hello", "hello", ImagemapArea{520, 0, 520, 1040}),
  1145  				).WithVideo(&ImagemapVideo{
  1146  					OriginalContentURL: "https://example.com/original.mp4",
  1147  					PreviewImageURL:    "https://example.com/preview.jpg",
  1148  					Area:               ImagemapArea{10, 10, 100, 200},
  1149  				}),
  1150  			},
  1151  			ResponseCode: 200,
  1152  			Response:     []byte(`{}`),
  1153  			Want: want{
  1154  				RequestBody: []byte(`{"messages":[{"type":"imagemap","baseUrl":"https://example.com/bot/images/rm001","altText":"this is an imagemap with video","baseSize":{"width":1040,"height":1040},"actions":[{"type":"uri","label":"example","linkUri":"https://example.com/","area":{"x":520,"y":0,"width":520,"height":1040}},{"type":"message","label":"hello","text":"hello","area":{"x":520,"y":0,"width":520,"height":1040}}],"video":{"originalContentUrl":"https://example.com/original.mp4","previewImageUrl":"https://example.com/preview.jpg","area":{"x":10,"y":10,"width":100,"height":200}}}]}` + "\n"),
  1155  				Response:    &BasicResponse{},
  1156  			},
  1157  		},
  1158  		{
  1159  			// A imagemap messages with video 2
  1160  			Messages: []SendingMessage{
  1161  				NewImagemapMessage(
  1162  					"https://example.com/bot/images/rm001",
  1163  					"this is an imagemap with video and external link",
  1164  					ImagemapBaseSize{1040, 1040},
  1165  					NewURIImagemapAction("example", "https://example.com/", ImagemapArea{520, 0, 520, 1040}),
  1166  					NewMessageImagemapAction("hello", "hello", ImagemapArea{520, 0, 520, 1040}),
  1167  				).WithVideo(&ImagemapVideo{
  1168  					OriginalContentURL: "https://example.com/original.mp4",
  1169  					PreviewImageURL:    "https://example.com/preview.jpg",
  1170  					Area:               ImagemapArea{10, 10, 100, 200},
  1171  					ExternalLink: &ImagemapVideoExternalLink{
  1172  						LinkURI: "https://example.com/",
  1173  						Label:   "external link",
  1174  					},
  1175  				}),
  1176  			},
  1177  			ResponseCode: 200,
  1178  			Response:     []byte(`{}`),
  1179  			Want: want{
  1180  				RequestBody: []byte(`{"messages":[{"type":"imagemap","baseUrl":"https://example.com/bot/images/rm001","altText":"this is an imagemap with video and external link","baseSize":{"width":1040,"height":1040},"actions":[{"type":"uri","label":"example","linkUri":"https://example.com/","area":{"x":520,"y":0,"width":520,"height":1040}},{"type":"message","label":"hello","text":"hello","area":{"x":520,"y":0,"width":520,"height":1040}}],"video":{"originalContentUrl":"https://example.com/original.mp4","previewImageUrl":"https://example.com/preview.jpg","area":{"x":10,"y":10,"width":100,"height":200},"externalLink":{"linkUri":"https://example.com/","label":"external link"}}}]}` + "\n"),
  1181  				Response:    &BasicResponse{},
  1182  			},
  1183  		},
  1184  		{
  1185  			// A flex message
  1186  			Messages: []SendingMessage{
  1187  				NewFlexMessage(
  1188  					"this is a flex message",
  1189  					&BubbleContainer{
  1190  						Type: FlexContainerTypeBubble,
  1191  						Body: &BoxComponent{
  1192  							Type:   FlexComponentTypeBox,
  1193  							Layout: FlexBoxLayoutTypeVertical,
  1194  							Contents: []FlexComponent{
  1195  								&TextComponent{
  1196  									Type: FlexComponentTypeText,
  1197  									Text: "hello",
  1198  								},
  1199  								&TextComponent{
  1200  									Type: FlexComponentTypeText,
  1201  									Text: "world",
  1202  									Flex: IntPtr(0),
  1203  								},
  1204  								&SpacerComponent{
  1205  									Type: FlexComponentTypeSpacer,
  1206  								},
  1207  							},
  1208  						},
  1209  					},
  1210  				),
  1211  			},
  1212  			ResponseCode: 200,
  1213  			Response:     []byte(`{}`),
  1214  			Want: want{
  1215  				RequestBody: []byte(`{"messages":[{"type":"flex","altText":"this is a flex message","contents":{"type":"bubble","body":{"type":"box","layout":"vertical","contents":[{"type":"text","text":"hello"},{"type":"text","text":"world","flex":0},{"type":"spacer"}]}}}]}` + "\n"),
  1216  				Response:    &BasicResponse{},
  1217  			},
  1218  		},
  1219  		{
  1220  			// A text message with quick replies
  1221  			Messages: []SendingMessage{
  1222  				NewTextMessage(
  1223  					"Select your favorite food category or send me your location!",
  1224  				).WithQuickReplies(
  1225  					NewQuickReplyItems(
  1226  						NewQuickReplyButton("https://example.com/sushi.png", NewMessageAction("Sushi", "Sushi")),
  1227  						NewQuickReplyButton("https://example.com/tempura.png", NewMessageAction("Tempura", "Tempura")),
  1228  						NewQuickReplyButton("", NewLocationAction("Send location")),
  1229  					),
  1230  				),
  1231  			},
  1232  			ResponseCode: 200,
  1233  			Response:     []byte(`{}`),
  1234  			Want: want{
  1235  				RequestBody: []byte(`{"messages":[{"type":"text","text":"Select your favorite food category or send me your location!","quickReply":{"items":[{"type":"action","imageUrl":"https://example.com/sushi.png","action":{"type":"message","label":"Sushi","text":"Sushi"}},{"type":"action","imageUrl":"https://example.com/tempura.png","action":{"type":"message","label":"Tempura","text":"Tempura"}},{"type":"action","action":{"type":"location","label":"Send location"}}]}}]}` + "\n"),
  1236  				Response:    &BasicResponse{},
  1237  			},
  1238  		},
  1239  		{
  1240  			// Multiple messages
  1241  			Messages:     []SendingMessage{NewTextMessage("Hello, world1"), NewTextMessage("Hello, world2")},
  1242  			ResponseCode: 200,
  1243  			Response:     []byte(`{}`),
  1244  			Want: want{
  1245  				RequestBody: []byte(`{"messages":[{"type":"text","text":"Hello, world1"},{"type":"text","text":"Hello, world2"}]}` + "\n"),
  1246  				Response:    &BasicResponse{},
  1247  			},
  1248  		},
  1249  		// Single message with sender
  1250  		{
  1251  			Messages: []SendingMessage{
  1252  				NewAudioMessage("https://example.com/original.m4a", 1000).WithSender(
  1253  					NewSender("Cony", "https://line.me/conyprof"),
  1254  				),
  1255  			},
  1256  			ResponseCode: 200,
  1257  			Response:     []byte(`{}`),
  1258  			Want: want{
  1259  				RequestBody: []byte(`{"messages":[{"type":"audio","originalContentUrl":"https://example.com/original.m4a","duration":1000,"sender":{"name":"Cony","iconUrl":"https://line.me/conyprof"}}]}` + "\n"),
  1260  				Response:    &BasicResponse{},
  1261  			},
  1262  		},
  1263  		// Single location message with sender
  1264  		{
  1265  			Messages: []SendingMessage{
  1266  				NewLocationMessage("title", "address", 35.65910807942215, 139.70372892916203).WithSender(
  1267  					NewSender("Cony", "https://line.me/conyprof"),
  1268  				),
  1269  			},
  1270  			ResponseCode: 200,
  1271  			Response:     []byte(`{}`),
  1272  			Want: want{
  1273  				RequestBody: []byte(`{"messages":[{"type":"location","title":"title","address":"address","latitude":35.65910807942215,"longitude":139.70372892916203,"sender":{"name":"Cony","iconUrl":"https://line.me/conyprof"}}]}` + "\n"),
  1274  				Response:    &BasicResponse{},
  1275  			},
  1276  		},
  1277  		{
  1278  			// A imagemap message with sender
  1279  			Messages: []SendingMessage{
  1280  				NewImagemapMessage(
  1281  					"https://example.com/bot/images/rm001",
  1282  					"this is an imagemap",
  1283  					ImagemapBaseSize{1040, 1040},
  1284  					NewURIImagemapAction("example", "https://example.com/", ImagemapArea{520, 0, 520, 1040}),
  1285  					NewMessageImagemapAction("hello", "hello", ImagemapArea{520, 0, 520, 1040}),
  1286  				).WithSender(
  1287  					NewSender("Cony", "https://line.me/conyprof"),
  1288  				),
  1289  			},
  1290  			ResponseCode: 200,
  1291  			Response:     []byte(`{}`),
  1292  			Want: want{
  1293  				RequestBody: []byte(`{"messages":[{"type":"imagemap","baseUrl":"https://example.com/bot/images/rm001","altText":"this is an imagemap","baseSize":{"width":1040,"height":1040},"actions":[{"type":"uri","label":"example","linkUri":"https://example.com/","area":{"x":520,"y":0,"width":520,"height":1040}},{"type":"message","label":"hello","text":"hello","area":{"x":520,"y":0,"width":520,"height":1040}}],"sender":{"name":"Cony","iconUrl":"https://line.me/conyprof"}}]}` + "\n"),
  1294  				Response:    &BasicResponse{},
  1295  			},
  1296  		},
  1297  		{
  1298  			Messages: []SendingMessage{
  1299  				NewTextMessage("Hello, I am Cony!!").WithSender(
  1300  					NewSender("Cony", "https://line.me/conyprof"),
  1301  				),
  1302  				NewStickerMessage("1", "1").WithSender(
  1303  					NewSender("Cony", "https://line.me/conyprof"),
  1304  				),
  1305  				NewVideoMessage("https://example.com/original.mp4", "https://example.com/preview.jpg").WithSender(
  1306  					NewSender("Cony", "https://line.me/conyprof"),
  1307  				),
  1308  			},
  1309  			ResponseCode: 200,
  1310  			Response:     []byte(`{}`),
  1311  			Want: want{
  1312  				RequestBody: []byte(`{"messages":[{"type":"text","text":"Hello, I am Cony!!","sender":{"name":"Cony","iconUrl":"https://line.me/conyprof"}},{"type":"sticker","packageId":"1","stickerId":"1","sender":{"name":"Cony","iconUrl":"https://line.me/conyprof"}},{"type":"video","originalContentUrl":"https://example.com/original.mp4","previewImageUrl":"https://example.com/preview.jpg","sender":{"name":"Cony","iconUrl":"https://line.me/conyprof"}}]}` + "\n"),
  1313  				Response:    &BasicResponse{},
  1314  			},
  1315  		},
  1316  		{
  1317  			// Multiple messages with sender
  1318  			Messages: []SendingMessage{
  1319  				NewTextMessage("Hello, I am Cony!!").WithSender(
  1320  					NewSender("Cony", "https://line.me/conyprof"),
  1321  				),
  1322  				NewStickerMessage("1", "1").WithSender(
  1323  					NewSender("Cony", "https://line.me/conyprof"),
  1324  				),
  1325  				NewVideoMessage("https://example.com/original.mp4", "https://example.com/preview.jpg").WithSender(
  1326  					NewSender("Cony", "https://line.me/conyprof"),
  1327  				),
  1328  			},
  1329  			ResponseCode: 200,
  1330  			Response:     []byte(`{}`),
  1331  			Want: want{
  1332  				RequestBody: []byte(`{"messages":[{"type":"text","text":"Hello, I am Cony!!","sender":{"name":"Cony","iconUrl":"https://line.me/conyprof"}},{"type":"sticker","packageId":"1","stickerId":"1","sender":{"name":"Cony","iconUrl":"https://line.me/conyprof"}},{"type":"video","originalContentUrl":"https://example.com/original.mp4","previewImageUrl":"https://example.com/preview.jpg","sender":{"name":"Cony","iconUrl":"https://line.me/conyprof"}}]}` + "\n"),
  1333  				Response:    &BasicResponse{},
  1334  			},
  1335  		},
  1336  		{
  1337  			// A text message with emojis
  1338  			Messages: []SendingMessage{
  1339  				NewTextMessage("$ Look at this: $ It's a LINE emoji!").AddEmoji(
  1340  					NewEmoji(0, "5ac1bfd5040ab15980c9b435", "086")).AddEmoji(
  1341  					NewEmoji(14, "5ac1bfd5040ab15980c9b435", "001"),
  1342  				),
  1343  			},
  1344  			ResponseCode: 200,
  1345  			Response:     []byte(`{}`),
  1346  			Want: want{
  1347  				RequestBody: []byte(`{"messages":[{"type":"text","text":"$ Look at this: $ It's a LINE emoji!","emojis":[{"index":0,"productId":"5ac1bfd5040ab15980c9b435","emojiId":"086"},{"index":14,"productId":"5ac1bfd5040ab15980c9b435","emojiId":"001"}]}]}` + "\n"),
  1348  				Response:    &BasicResponse{},
  1349  			},
  1350  		},
  1351  		{
  1352  			// Bad request
  1353  			Messages:     []SendingMessage{NewTextMessage(""), NewTextMessage("")},
  1354  			ResponseCode: 400,
  1355  			Response:     []byte(`{"message":"Request body has 2 error(s).","details":[{"message":"may not be empty","property":"messages[0].text"},{"message":"may not be empty","property":"messages[1].text"}]}`),
  1356  			Want: want{
  1357  				RequestBody: []byte(`{"messages":[{"type":"text","text":""},{"type":"text","text":""}]}` + "\n"),
  1358  				Error: &APIError{
  1359  					Code: 400,
  1360  					Response: &ErrorResponse{
  1361  						Message: "Request body has 2 error(s).",
  1362  						Details: []errorResponseDetail{
  1363  							{
  1364  								Message:  "may not be empty",
  1365  								Property: "messages[0].text",
  1366  							},
  1367  							{
  1368  								Message:  "may not be empty",
  1369  								Property: "messages[1].text",
  1370  							},
  1371  						},
  1372  					},
  1373  				},
  1374  			},
  1375  		},
  1376  	}
  1377  
  1378  	var currentTestIdx int
  1379  	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1380  		defer r.Body.Close()
  1381  		tc := testCases[currentTestIdx]
  1382  		if r.Method != http.MethodPost {
  1383  			t.Errorf("Method %s; want %s", r.Method, http.MethodPost)
  1384  		}
  1385  		if r.URL.Path != APIEndpointValidateBroadcastMessage {
  1386  			t.Errorf("URLPath %s; want %s", r.URL.Path, APIEndpointValidateBroadcastMessage)
  1387  		}
  1388  		body, err := io.ReadAll(r.Body)
  1389  		if err != nil {
  1390  			t.Fatal(err)
  1391  		}
  1392  		if !reflect.DeepEqual(body, tc.Want.RequestBody) {
  1393  			t.Errorf("RequestBody %s; want %s", body, tc.Want.RequestBody)
  1394  		}
  1395  		w.WriteHeader(tc.ResponseCode)
  1396  		w.Write(tc.Response)
  1397  	}))
  1398  	defer server.Close()
  1399  
  1400  	dataServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1401  		defer r.Body.Close()
  1402  		t.Error("Unexpected data API call")
  1403  		w.WriteHeader(404)
  1404  		w.Write([]byte(`{"message":"Not found"}`))
  1405  	}))
  1406  	defer dataServer.Close()
  1407  
  1408  	client, err := mockClient(server, dataServer)
  1409  	if err != nil {
  1410  		t.Fatal(err)
  1411  	}
  1412  	for i, tc := range testCases {
  1413  		currentTestIdx = i
  1414  		res, err := client.ValidateBroadcastMessage(tc.Messages...).Do()
  1415  		if tc.Want.Error != nil {
  1416  			if !reflect.DeepEqual(err, tc.Want.Error) {
  1417  				t.Errorf("Error %d %v; want %v", i, err, tc.Want.Error)
  1418  			}
  1419  		} else {
  1420  			if err != nil {
  1421  				t.Error(err)
  1422  			}
  1423  		}
  1424  		if tc.Want.Response != nil {
  1425  			if !reflect.DeepEqual(res, tc.Want.Response) {
  1426  				t.Errorf("Response %d %v; want %v", i, res, tc.Want.Response)
  1427  			}
  1428  		}
  1429  	}
  1430  }
  1431  
  1432  func TestValidateNarrowcastMessages(t *testing.T) {
  1433  	type want struct {
  1434  		RequestBody []byte
  1435  		Response    *BasicResponse
  1436  		Error       error
  1437  	}
  1438  	testCases := []struct {
  1439  		Label        string
  1440  		Messages     []SendingMessage
  1441  		RequestID    string
  1442  		Response     []byte
  1443  		ResponseCode int
  1444  		Want         want
  1445  	}{
  1446  		{
  1447  			Label:        "A text message for Narrowcast Message",
  1448  			Messages:     []SendingMessage{NewTextMessage("Hello, world")},
  1449  			RequestID:    "12222",
  1450  			Response:     []byte(`{}`),
  1451  			ResponseCode: 202,
  1452  			Want: want{
  1453  				RequestBody: []byte(`{"messages":[{"type":"text","text":"Hello, world"}]}` + "\n"),
  1454  				Response:    &BasicResponse{RequestID: "12222"},
  1455  			},
  1456  		},
  1457  	}
  1458  
  1459  	var currentTestIdx int
  1460  	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1461  		defer r.Body.Close()
  1462  		if r.Method != http.MethodPost {
  1463  			t.Errorf("Method %s; want %s", r.Method, http.MethodPost)
  1464  		}
  1465  		if r.URL.Path != APIEndpointValidateNarrowcastMessage {
  1466  			t.Errorf("URLPath %s; want %s", r.URL.Path, APIEndpointValidateNarrowcastMessage)
  1467  		}
  1468  		body, err := io.ReadAll(r.Body)
  1469  		if err != nil {
  1470  			t.Fatal(err)
  1471  		}
  1472  		tc := testCases[currentTestIdx]
  1473  		if !reflect.DeepEqual(body, tc.Want.RequestBody) {
  1474  			t.Errorf("RequestBody \n%s; want \n%s", body, tc.Want.RequestBody)
  1475  		}
  1476  		w.Header().Set("X-Line-Request-Id", tc.RequestID)
  1477  		w.WriteHeader(tc.ResponseCode)
  1478  		w.Write(tc.Response)
  1479  	}))
  1480  	defer server.Close()
  1481  
  1482  	dataServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1483  		defer r.Body.Close()
  1484  		t.Error("Unexpected data API call")
  1485  		w.WriteHeader(404)
  1486  		w.Write([]byte(`{"message":"Not found"}`))
  1487  	}))
  1488  	defer dataServer.Close()
  1489  
  1490  	client, err := mockClient(server, dataServer)
  1491  	if err != nil {
  1492  		t.Fatal(err)
  1493  	}
  1494  	for i, tc := range testCases {
  1495  		currentTestIdx = i
  1496  		t.Run(strconv.Itoa(i)+"/"+tc.Label, func(t *testing.T) {
  1497  			narrowCast := client.ValidateNarrowcastMessage(tc.Messages...)
  1498  			res, err := narrowCast.Do()
  1499  			if tc.Want.Error != nil {
  1500  				if !reflect.DeepEqual(err, tc.Want.Error) {
  1501  					t.Errorf("Error %d %v; want %v", i, err, tc.Want.Error)
  1502  				}
  1503  			} else {
  1504  				if err != nil {
  1505  					t.Error(err)
  1506  				}
  1507  			}
  1508  			if tc.Want.Response != nil {
  1509  				if !reflect.DeepEqual(res, tc.Want.Response) {
  1510  					t.Errorf("Response %d %v; want %v", i, res, tc.Want.Response)
  1511  				}
  1512  			}
  1513  		})
  1514  	}
  1515  }