github.com/actions-on-google/gactions@v3.2.0+incompatible/api/request_test.go (about) 1 // Copyright 2020 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://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, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package request 16 17 import ( 18 "encoding/json" 19 "errors" 20 "fmt" 21 "mime" 22 "path/filepath" 23 "testing" 24 25 "github.com/google/go-cmp/cmp" 26 "github.com/google/go-cmp/cmp/cmpopts" 27 "github.com/protolambda/messagediff" 28 "gopkg.in/yaml.v2" 29 ) 30 31 func TestWritePreview(t *testing.T) { 32 projectID := "project-123" 33 sandbox := true 34 want := map[string]interface{}{ 35 "parent": fmt.Sprintf("projects/%v", projectID), 36 "previewSettings": map[string]interface{}{ 37 "sandbox": sandbox, 38 }, 39 } 40 got := WritePreview(projectID, sandbox) 41 diff, equal := messagediff.DeepDiff(want, got) 42 if !equal { 43 t.Errorf("WritePreview returned an incorrect value; diff (want -> got)\n%s", diff) 44 } 45 } 46 47 func TestWriteDraft(t *testing.T) { 48 projectID := "project-123" 49 want := map[string]interface{}{ 50 "parent": fmt.Sprintf("projects/%v", projectID), 51 } 52 got := WriteDraft(projectID) 53 diff, equal := messagediff.DeepDiff(want, got) 54 if !equal { 55 t.Errorf("WritePreview returned an incorrect value; diff (want -> got)\n%s", diff) 56 } 57 } 58 59 func TestCreateVersion(t *testing.T) { 60 projectID := "project-123" 61 releaseChannel := "prod" 62 want := map[string]interface{}{ 63 "parent": fmt.Sprintf("projects/%v", projectID), 64 "release_channel": releaseChannel, 65 } 66 got := CreateVersion(projectID, releaseChannel) 67 if diff := cmp.Diff(want, got); diff != "" { 68 t.Errorf("WriteVersion incorrectly populated the request: diff (-want, +got)\n%s", diff) 69 } 70 } 71 72 func TestReadVersion(t *testing.T) { 73 projectID := "project-123" 74 versionID := "2" 75 want := map[string]interface{}{ 76 "name": fmt.Sprintf("projects/%v/versions/%v", projectID, versionID), 77 } 78 got := ReadVersion(projectID, versionID) 79 if diff := cmp.Diff(want, got); diff != "" { 80 t.Errorf("ReadVersion returned an incorrect value: diff (-want, +got)\n%s", diff) 81 } 82 } 83 84 func TestListReleaseChannels(t *testing.T) { 85 projectID := "project-123" 86 want := map[string]interface{}{ 87 "parent": fmt.Sprintf("projects/%v", projectID), 88 } 89 got := ListReleaseChannels(projectID) 90 if diff := cmp.Diff(want, got); diff != "" { 91 t.Errorf("ListReleaseChannels returned an incorrect value: diff (-want, +got)\n%s", diff) 92 } 93 } 94 95 func TestListVersions(t *testing.T) { 96 projectID := "project-123" 97 want := map[string]interface{}{ 98 "parent": fmt.Sprintf("projects/%v", projectID), 99 } 100 got := ListVersions(projectID) 101 if diff := cmp.Diff(want, got); diff != "" { 102 t.Errorf("ListVersions returned an incorrect value: diff (-want, +got)\n%s", diff) 103 } 104 } 105 106 func TestAddConfigFiles(t *testing.T) { 107 tests := []struct { 108 files map[string][]byte 109 want map[string]interface{} 110 err error 111 }{ 112 { 113 files: map[string][]byte{ 114 "verticals/CharacterAlarms.yaml": []byte("foo: bar"), 115 "actions/actions.yaml": []byte("intent_name: alarm"), 116 "manifest.yaml": []byte("version: 1.0"), 117 "settings/settings.yaml": []byte("display_name: alarm"), 118 "settings/zh-TW/settings.yaml": []byte("developer_email: foo@foo.com"), 119 "custom/global/actions.intent.CANCEL.yaml": []byte("transitionToScene: actions.scene.END_CONVERSATION"), 120 "custom/intents/help.yaml": []byte("phrase: hello"), 121 "custom/intents/ru/help.yaml": []byte("phrase: hello"), 122 "custom/prompts/foo.yaml": []byte("prompt: \"yes\""), 123 "custom/prompts/ru/foo.yaml": []byte("prompt: \"yes\""), 124 "custom/scenes/a.yaml": []byte("name: a"), 125 "custom/types/b.yaml": []byte("type: b"), 126 "custom/types/ru/b.yaml": []byte("type: b"), 127 "webhooks/webhook1.yaml": []byte( 128 ` 129 inlineCloudFunction: 130 execute_function: hello 131 `), 132 "webhooks/webhook2.yaml": []byte( 133 ` 134 external_endpoint: 135 base_url: https://google.com 136 http_headers: 137 content-type: application/json 138 endpoint_api_version: 1 139 `), 140 "resources/strings/bundle.yaml": []byte( 141 ` 142 x: "777" 143 y: "777" 144 greeting: "hello world" 145 `), 146 }, 147 want: map[string]interface{}{ 148 "configFiles": map[string][]interface{}{ 149 "configFiles": { 150 map[string]interface{}{ 151 "filePath": "verticals/CharacterAlarms.yaml", 152 "verticalSettings": map[string]interface{}{"foo": "bar"}, 153 }, 154 map[string]interface{}{ 155 "filePath": "actions/actions.yaml", 156 "actions": map[string]interface{}{"intent_name": "alarm"}, 157 }, 158 map[string]interface{}{ 159 "filePath": "manifest.yaml", 160 "manifest": map[string]interface{}{"version": 1.0}, 161 }, 162 map[string]interface{}{ 163 "filePath": "settings/settings.yaml", 164 "settings": map[string]interface{}{"display_name": "alarm"}, 165 }, 166 map[string]interface{}{ 167 "filePath": "settings/zh-TW/settings.yaml", 168 "settings": map[string]interface{}{"developer_email": "foo@foo.com"}, 169 }, 170 map[string]interface{}{ 171 "filePath": "custom/global/actions.intent.CANCEL.yaml", 172 "globalIntentEvent": map[string]interface{}{"transitionToScene": "actions.scene.END_CONVERSATION"}, 173 }, 174 map[string]interface{}{ 175 "filePath": "custom/intents/help.yaml", 176 "intent": map[string]interface{}{"phrase": "hello"}, 177 }, 178 map[string]interface{}{ 179 "filePath": "custom/intents/ru/help.yaml", 180 "intent": map[string]interface{}{"phrase": "hello"}, 181 }, 182 map[string]interface{}{ 183 "filePath": "custom/prompts/foo.yaml", 184 "staticPrompt": map[string]interface{}{"prompt": "yes"}, 185 }, 186 map[string]interface{}{ 187 "filePath": "custom/prompts/ru/foo.yaml", 188 "staticPrompt": map[string]interface{}{"prompt": "yes"}, 189 }, 190 map[string]interface{}{ 191 "filePath": "custom/scenes/a.yaml", 192 "scene": map[string]interface{}{"name": "a"}, 193 }, 194 map[string]interface{}{ 195 "filePath": "custom/types/b.yaml", 196 "type": map[string]interface{}{"type": "b"}, 197 }, 198 map[string]interface{}{ 199 "filePath": "custom/types/ru/b.yaml", 200 "type": map[string]interface{}{"type": "b"}, 201 }, 202 map[string]interface{}{ 203 "filePath": "webhooks/webhook1.yaml", 204 "webhook": map[string]interface{}{ 205 "inlineCloudFunction": map[string]interface{}{ 206 "execute_function": "hello", 207 }, 208 }, 209 }, 210 map[string]interface{}{ 211 "filePath": "webhooks/webhook2.yaml", 212 "webhook": map[string]interface{}{ 213 "external_endpoint": map[string]interface{}{ 214 "base_url": "https://google.com", 215 "http_headers": map[string]interface{}{ 216 "content-type": "application/json", 217 }, 218 "endpoint_api_version": 1, 219 }, 220 }, 221 }, 222 map[string]interface{}{ 223 "filePath": "resources/strings/bundle.yaml", 224 "resourceBundle": map[string]interface{}{ 225 "x": "777", 226 "y": "777", 227 "greeting": "hello world", 228 }, 229 }, 230 }, 231 }, 232 }, 233 err: nil, 234 }, 235 { 236 files: map[string][]byte{}, 237 want: map[string]interface{}{ 238 "configFiles": map[string][]interface{}{}, 239 }, 240 err: nil, 241 }, 242 { 243 files: map[string][]byte{ 244 "manifest.yaml": []byte("version: 1.0"), 245 "extrafile": []byte("key: should raise an error"), 246 }, 247 want: map[string]interface{}{}, 248 err: errors.New("failed to add extrafile to a request"), 249 }, 250 } 251 for _, tc := range tests { 252 req := map[string]interface{}{} 253 err := addConfigFiles(req, tc.files, ".") 254 if err != nil { 255 if tc.err == nil { 256 t.Errorf("AddConfigFiles returned %v, want %v, input %v", err, tc.err, tc.files) 257 } 258 } 259 if tc.err == nil { 260 wantCfgs, ok := tc.want["configFiles"].(map[string][]interface{}) 261 if !ok { 262 t.Errorf("Failed to convert to type: tc.want[\"configFiles\"] is incorrect type") 263 } 264 fs, ok := req["files"].(map[string]interface{}) 265 if !ok { 266 t.Errorf("Failed type conversion: expected files inside of the request to be of type map[string]interface{}") 267 } 268 reqCfgs, ok := fs["configFiles"].(map[string][]interface{}) 269 if !ok { 270 t.Errorf("Failed type conversion: expected configFiles inside of the request to be of type map[string][]interface{}") 271 } 272 if diff := cmp.Diff(wantCfgs["configFiles"], reqCfgs["configFiles"], cmpopts.SortSlices(func(l, r interface{}) bool { 273 lmp, ok := l.(map[string]interface{}) 274 if !ok { 275 t.Errorf("can not convert %v to map[string]interface{}", l) 276 } 277 rmp, ok := r.(map[string]interface{}) 278 if !ok { 279 t.Errorf("can not convert %v to map[string]interface{}", r) 280 } 281 return lmp["filePath"].(string) < rmp["filePath"].(string) 282 })); diff != "" { 283 t.Errorf("AddConfigFiles didn't add the config files to a request correctly: diff (-want, +got)\n%s", diff) 284 } 285 } 286 } 287 } 288 289 func TestAddDataFiles(t *testing.T) { 290 tests := []struct { 291 files map[string][]byte 292 want map[string]interface{} 293 err error 294 }{ 295 { 296 files: map[string][]byte{ 297 "audio1.mp3": []byte("abc123"), 298 "image1.jpg": []byte("abc123"), 299 "audio2.wav": []byte("abc123"), 300 "animation1.flr": []byte("xyz789"), 301 }, 302 want: map[string]interface{}{ 303 "files": map[string]interface{}{ 304 "dataFiles": map[string][]interface{}{ 305 "dataFiles": { 306 map[string]interface{}{ 307 "filePath": "audio1.mp3", 308 "payload": []byte("abc123"), 309 "contentType": mime.TypeByExtension(filepath.Ext("audio1.mp3")), 310 }, 311 map[string]interface{}{ 312 "filePath": "image1.jpg", 313 "payload": []byte("abc123"), 314 "contentType": mime.TypeByExtension(filepath.Ext("image1.jpg")), 315 }, 316 map[string]interface{}{ 317 "filePath": "audio2.wav", 318 "payload": []byte("abc123"), 319 "contentType": mime.TypeByExtension(filepath.Ext("audio2.wav")), 320 }, 321 map[string]interface{}{ 322 "filePath": "animation1.flr", 323 "payload": []byte("xyz789"), 324 "contentType": "x-world/x-vrml", 325 }, 326 }, 327 }, 328 }, 329 }, 330 err: nil, 331 }, 332 { 333 files: map[string][]byte{ 334 "audio1.xyz": []byte("abc123"), 335 }, 336 want: map[string]interface{}{}, 337 err: nil, 338 }, 339 { 340 files: map[string][]byte{ 341 "webhooks/webhook1.zip": []byte("===abc==="), 342 }, 343 want: map[string]interface{}{ 344 "files": map[string]interface{}{ 345 "dataFiles": map[string][]interface{}{ 346 "dataFiles": { 347 map[string]interface{}{ 348 "filePath": "webhooks/webhook1.zip", 349 "payload": []byte("===abc==="), 350 "contentType": "application/zip;zip_type=cloud_function", 351 }, 352 }, 353 }, 354 }, 355 }, 356 err: nil, 357 }, 358 } 359 for _, tc := range tests { 360 req := map[string]interface{}{} 361 if err := addDataFiles(req, tc.files, "."); err != nil { 362 if tc.err == nil { 363 t.Errorf("addDataFiles returned %v, want %v, input (files: %v)", err, tc.err, tc.files) 364 } 365 } 366 type dataFile struct { 367 Filepath string `json:"filePath"` 368 Payload []byte `json:"payload"` 369 ContentType string `json:"contentType"` 370 } 371 type reqFmt struct { 372 Files struct { 373 DataFiles struct { 374 DataFiles []dataFile `json:"dataFiles"` 375 } `json:"dataFiles"` 376 } `json:"files"` 377 } 378 b, err := json.Marshal(tc.want) 379 if err != nil { 380 t.Errorf("Failed to marshal %v into JSON: %v", tc.want, err) 381 } 382 r := &reqFmt{} 383 if err := json.Unmarshal(b, r); err != nil { 384 t.Errorf("Failed to unmarshal into a struct: %v", err) 385 } 386 387 b, err = json.Marshal(req) 388 if err != nil { 389 t.Errorf("Failed to marshal %v into JSON: %v", req, err) 390 } 391 r2 := &reqFmt{} 392 if err := json.Unmarshal(b, r2); err != nil { 393 t.Errorf("Failed to unmarshal into a struct: %v", err) 394 } 395 if diff := cmp.Diff(r.Files.DataFiles.DataFiles, r2.Files.DataFiles.DataFiles, cmpopts.SortSlices(func(l, r dataFile) bool { 396 return l.Filepath < r.Filepath 397 })); diff != "" { 398 t.Errorf("addDataFiles incorrectly populated the request: diff (-want, +got)\n%s", diff) 399 } 400 } 401 } 402 403 func TestNewStreamer(t *testing.T) { 404 cfgs := map[string][]byte{ 405 "actions/actions.yaml": []byte("42"), 406 "settings/en/settings.yaml": []byte("displayName: foo"), 407 "custom/intents/intent1.yaml": []byte("name: intent123"), 408 "settings/settings.yaml": []byte("projectID: 123"), 409 "manifest.yaml": []byte("version: 1.0"), 410 "resources/strings/bundle.yaml": []byte("a: foo"), 411 "resources/strings/en/bundle.yaml": []byte("a: foo b: bar"), 412 } 413 dfs := map[string][]byte{ 414 "resources/images/image1.png": []byte("abc"), 415 "resources/images/image3.png": []byte("abcdefghi"), 416 "resources/images/image2.png": []byte("abcdef"), 417 } 418 makeRequest := func() map[string]interface{} { 419 return nil 420 } 421 root := "." 422 chunkSize := 1024 423 s := NewStreamer(cfgs, dfs, makeRequest, root, chunkSize) 424 425 // This is in correct sorted order 426 wantCfgnames := []string{"settings/settings.yaml", "manifest.yaml", "settings/en/settings.yaml", 427 "actions/actions.yaml", "resources/strings/bundle.yaml", "resources/strings/en/bundle.yaml", "custom/intents/intent1.yaml"} 428 // Check that first three elements are settings, manifest files and the rest are sorted according to their size. 429 if diff := cmp.Diff(wantCfgnames[:3], s.configFilenames[:3], cmpopts.SortSlices(strLess)); diff != "" { 430 t.Errorf("NewStreamer didn't have settings and manifest in the beginning of configFilenames: diff (-want, +got)\n%s", diff) 431 } 432 if diff := cmp.Diff(wantCfgnames[3:], s.configFilenames[3:]); diff != "" { 433 t.Errorf("NewStreamer didn't have rest of config files sorted correctly: diff (-want, +got)\n%s", diff) 434 } 435 436 wantDfnames := []string{"resources/images/image1.png", "resources/images/image2.png", "resources/images/image3.png"} 437 if diff := cmp.Diff(wantDfnames, s.dataFilenames); diff != "" { 438 t.Errorf("NewStreamer didn't have rest of config files sorted correctly: diff (-want, +got)\n%s", diff) 439 } 440 } 441 442 func TestMoveToFront(t *testing.T) { 443 tests := []struct { 444 a []string 445 ps []int 446 want []string 447 }{ 448 { 449 a: []string{"settings/settings.yaml", "settings/en/settings.yaml", "manifest.yaml"}, 450 ps: []int{0, 1, 2}, 451 want: []string{"settings/settings.yaml", "settings/en/settings.yaml", "manifest.yaml"}, 452 }, 453 { 454 a: []string{"settings/settings.yaml", "custom/intents/intent.yaml", "settings/en/settings.yaml", "manifest.yaml", "actions/actions.yaml"}, 455 ps: []int{0, 2, 3}, 456 want: []string{"settings/settings.yaml", "settings/en/settings.yaml", "manifest.yaml"}, 457 }, 458 } 459 for _, tc := range tests { 460 moveToFront(tc.a, tc.ps) 461 if diff := cmp.Diff(tc.a[:len(tc.ps)], tc.want, cmpopts.SortSlices(strLess)); diff != "" { 462 t.Errorf("moveToFront didn't produce correct result: diff (-want, +got)\n%s", diff) 463 } 464 } 465 } 466 467 var strLess = func(s1, s2 string) bool { return s1 < s2 } 468 469 func parseReq(t *testing.T, req map[string]interface{}) []string { 470 t.Helper() 471 type configFileReq struct { 472 Files struct { 473 ConfigFiles struct { 474 ConfigFiles []struct { 475 FilePath string `json:"filePath"` 476 } `json:"configFiles"` 477 } `json:"configFiles"` 478 } `json:"files"` 479 } 480 b, err := json.Marshal(req) 481 if err != nil { 482 t.Errorf("Failed to marshal request into JSON: %v", err) 483 } 484 r := configFileReq{} 485 if err = json.Unmarshal(b, &r); err != nil { 486 t.Errorf("Failed to unmarshal JSON into a map: %v", err) 487 } 488 res := []string{} 489 for _, v := range r.Files.ConfigFiles.ConfigFiles { 490 res = append(res, v.FilePath) 491 } 492 return res 493 } 494 495 func TestNextWithTwoFiles(t *testing.T) { 496 cfgs := map[string][]byte{ 497 "settings/settings.yaml": []byte(`projectId: hello-world`), 498 "manifest.yaml": []byte(`version: 1.0`), 499 } 500 // Add a file that is equal to the "sum" of the rest of the confg files, 501 // so that it will be easier to split files. 502 yml := map[string]interface{}{ 503 "version": "1.0", 504 "projectId": "hello-world", 505 } 506 out, err := yaml.Marshal(yml) 507 if err != nil { 508 t.Fatalf("Failed to marshall %v into YAML: %v", yml, err) 509 } 510 cfgs["custom/intents/intent1.yaml"] = out 511 dfs := map[string][]byte{} 512 mkreq := func() map[string]interface{} { 513 return map[string]interface{}{} 514 } 515 // Sets chunkSize to the sum of the first two request. Thus, 516 // streamer is guaranteed to return two requests. 517 s := NewStreamer(cfgs, dfs, mkreq, ".", len(out)) 518 req1, err := s.Next() 519 if err != nil { 520 t.Errorf("SDKStreamer.Next failed to return the 1st request: %v", err) 521 } 522 want1 := []string{"settings/settings.yaml", "manifest.yaml"} 523 got1 := parseReq(t, req1) 524 if diff := cmp.Diff(want1, got1, cmpopts.SortSlices(strLess)); diff != "" { 525 t.Errorf("SDKStreamer.Next returned an incorrect request: diff (-want, +got)\n%s", diff) 526 } 527 if hasNext := s.HasNext(); !hasNext { 528 t.Errorf("HasNext returned %v, but want %v", hasNext, true) 529 } 530 req2, err := s.Next() 531 if err != nil { 532 t.Errorf("SDKStreamer.Next failed to return the 1st request: %v", err) 533 } 534 want2 := []string{"custom/intents/intent1.yaml"} 535 got2 := parseReq(t, req2) 536 if diff := cmp.Diff(want2, got2, cmpopts.SortSlices(strLess)); diff != "" { 537 t.Errorf("SDKStreamer.Next returned an incorrect request: diff (-want, +got)\n%s", diff) 538 } 539 if hasNext := s.HasNext(); hasNext { 540 t.Errorf("HasNext returned %v, but want %v", hasNext, false) 541 } 542 } 543 544 func TestNextWhenChunkSizeTooSmall(t *testing.T) { 545 cfgs := map[string][]byte{ 546 "settings/settings.yaml": []byte(`projectId: hello-world`), 547 "manifest.yaml": []byte(`version: 1.0`), 548 } 549 yml := map[string]interface{}{ 550 "version": "1.0", 551 "projectId": "hello-world", 552 } 553 out, err := yaml.Marshal(yml) 554 if err != nil { 555 t.Fatalf("Failed to marshall %v into YAML: %v", yml, err) 556 } 557 cfgs["custom/intents/intent1.yaml"] = out 558 dfs := map[string][]byte{} 559 mkreq := func() map[string]interface{} { 560 return map[string]interface{}{} 561 } 562 s := NewStreamer(cfgs, dfs, mkreq, ".", 1) 563 req1, err := s.Next() 564 if err == nil { 565 t.Errorf("SDKStreamer.Next returned %v, but needs an error: %v", req1, err) 566 } 567 }