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  }