github.com/actions-on-google/gactions@v3.2.0+incompatible/api/sdk_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 sdk
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"io"
    21  	"io/ioutil"
    22  	"net/http"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"runtime"
    27  	"sort"
    28  	"strings"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/actions-on-google/gactions/api/request"
    33  	"github.com/actions-on-google/gactions/api/testutils"
    34  	"github.com/actions-on-google/gactions/api/yamlutils"
    35  	"github.com/actions-on-google/gactions/project"
    36  	"github.com/actions-on-google/gactions/project/studio"
    37  	"github.com/google/go-cmp/cmp"
    38  	"github.com/google/go-cmp/cmp/cmpopts"
    39  )
    40  
    41  func buildPathToProjectFiles() string {
    42  	return filepath.Join("api", "examples", "account_linking_gsi")
    43  }
    44  
    45  type MockStudio struct {
    46  	files        map[string][]byte
    47  	clientSecret []byte
    48  	root         string
    49  	projectID    string
    50  }
    51  
    52  func NewMock(files map[string][]byte) MockStudio {
    53  	m := MockStudio{}
    54  	m.files = files
    55  	m.projectID = "placeholder_project"
    56  	return m
    57  }
    58  
    59  func (m MockStudio) ProjectID() string {
    60  	return m.projectID
    61  }
    62  
    63  func (MockStudio) Download(sample project.SampleProject, dest string) error {
    64  	return nil
    65  }
    66  
    67  func (MockStudio) AlreadySetup(pathToWorkDir string) bool {
    68  	return false
    69  }
    70  
    71  func (p MockStudio) Files() (map[string][]byte, error) {
    72  	return p.files, nil
    73  }
    74  
    75  func (MockStudio) ClientSecretJSON() ([]byte, error) {
    76  	return []byte{}, nil
    77  }
    78  
    79  func (p MockStudio) ProjectRoot() string {
    80  	return p.root
    81  }
    82  
    83  type myReader struct {
    84  	r   io.Reader
    85  	lat time.Duration
    86  }
    87  
    88  func (mr myReader) Read(p []byte) (n int, err error) {
    89  	time.Sleep(mr.lat)
    90  	return mr.r.Read(p)
    91  }
    92  
    93  func TestReadBodyWithTimeout(t *testing.T) {
    94  	var got, want []byte
    95  	var err error
    96  	var r myReader
    97  
    98  	r = myReader{r: strings.NewReader("hello"), lat: time.Duration(200) * time.Millisecond}
    99  	// Timeout for 5 seconds to reduce flakiness.
   100  	got, err = readBodyWithTimeout(r, time.Duration(5)*time.Second)
   101  	want = []byte("hello")
   102  	if err != nil {
   103  		t.Errorf("readBodyWithTimeout returned %v, want %v", err, nil)
   104  	}
   105  	if string(got) != string(want) {
   106  		t.Errorf("readBodyWithTimeout got %v, want %v", string(got), string(want))
   107  	}
   108  
   109  	// slow case
   110  	r = myReader{r: strings.NewReader("hello"), lat: time.Duration(3) * time.Second}
   111  	got, err = readBodyWithTimeout(r, time.Duration(1)*time.Second)
   112  	want = []byte("")
   113  	if err != nil {
   114  		t.Errorf("readBodyWithTimeout returned %v, want %v", err, nil)
   115  	}
   116  	if string(got) != string(want) {
   117  		t.Errorf("readBodyWithTimeout got %v, want %v", string(got), string(want))
   118  	}
   119  }
   120  
   121  func TestPostprocessJSONResponse(t *testing.T) {
   122  	tests := []struct {
   123  		in        *http.Response
   124  		shouldErr bool
   125  	}{
   126  		{
   127  			in: &http.Response{
   128  				StatusCode: 200,
   129  				Body: ioutil.NopCloser(bytes.NewReader([]byte(
   130  					`{
   131  						  "validationResults": {
   132  							  "results":[
   133  								  {
   134  									  "validationMesssage": "Your app doesn't have the correct size for the logo."
   135  								  }
   136  								]
   137  							}
   138  						}`,
   139  				))),
   140  			},
   141  			shouldErr: false,
   142  		},
   143  		{
   144  			in: &http.Response{
   145  				StatusCode: 500,
   146  				Body: ioutil.NopCloser(bytes.NewReader([]byte(
   147  					`{
   148  						  "error": {
   149  						  "code": 500,
   150  						  "message": "Internal error encountered",
   151  						  "status": "INTERNAL",
   152  						  "details": [
   153  							 	{
   154  									"@type": "type.googleapis.com/google.rpc.DebugInfo",
   155  									"detail": "Should not be shown to user."
   156  								}
   157  							 ]
   158  						 }
   159  					 }`,
   160  				))),
   161  			},
   162  			shouldErr: true,
   163  		},
   164  		{
   165  			in: &http.Response{
   166  				StatusCode: 400,
   167  				Body: ioutil.NopCloser(bytes.NewReader([]byte(
   168  					`{}`,
   169  				))),
   170  			},
   171  			shouldErr: true,
   172  		},
   173  	}
   174  	for _, tc := range tests {
   175  		errCh := make(chan error)
   176  		go postprocessJSONResponse(tc.in, errCh, func(body []byte) error {
   177  			// TODO: Ideally would like to check that this function gets called.
   178  			// Need a way to cleanly implement it.
   179  			return nil
   180  		})
   181  		got := <-errCh
   182  		if tc.shouldErr && got == nil {
   183  			t.Errorf("postprocessJSONResponse returned incorrect result: got %v, want an error", got)
   184  		}
   185  	}
   186  }
   187  
   188  func unmarshal(t *testing.T, p string) map[string]interface{} {
   189  	t.Helper()
   190  	b := testutils.ReadFileOrDie(p)
   191  	m, err := yamlutils.UnmarshalYAMLToMap(b)
   192  	if err != nil {
   193  		t.Fatalf("unmarshal: can not parse settins yaml into proto: %v", err)
   194  	}
   195  	return m
   196  }
   197  
   198  func TestSendFilesToServerJSON(t *testing.T) {
   199  	if runtime.GOOS == "windows" {
   200  		// This test does not work on Windows, as the "actions/actions.yaml"
   201  		// and other files cannot be found.
   202  		// The error specifically is:
   203  		// Cannot open file C:\...\_bazel_kbuilder\jzegmkbf\execroot\__main__\bazel-out\x64_windows-fastbuild\bin\api\sdk_test_\sdk_test.exe.runfiles\__main__92api\examples\account_linking_gsi\actions\actions.yaml
   204  		// Exit early.
   205  		return
   206  	}
   207  	tests := []struct {
   208  		projFiles                 map[string][]byte
   209  		wantRequests              []map[string]interface{}
   210  		wantErrorMessageToContain string
   211  	}{
   212  		{
   213  			projFiles: map[string][]byte{
   214  				"actions/actions.yaml":               testutils.ReadFileOrDie(filepath.Join(buildPathToProjectFiles(), "actions", "actions.yaml")),
   215  				"manifest.yaml":                      testutils.ReadFileOrDie(filepath.Join(buildPathToProjectFiles(), "manifest.yaml")),
   216  				"settings/settings.yaml":             testutils.ReadFileOrDie(filepath.Join(buildPathToProjectFiles(), "settings", "settings.yaml")),
   217  				"resources/audio/confirmation_01.mp3": testutils.ReadFileOrDie(filepath.Join(buildPathToProjectFiles(), "resources", "audio", "confirmation_01.mp3")),
   218  				"resources/images/smallLogo.jpg":     testutils.ReadFileOrDie(filepath.Join(buildPathToProjectFiles(), "resources", "images", "smallLogo.jpg")),
   219  				"settings/zh-TW/settings.yaml":       testutils.ReadFileOrDie(filepath.Join(buildPathToProjectFiles(), "settings", "zh-TW", "settings.yaml")),
   220  				"resources/images/zh-TW/smallLogo.jpg": testutils.ReadFileOrDie(filepath.Join(buildPathToProjectFiles(), "resources", "images", "zh-TW", "smallLogo.jpg")),
   221  				"webhooks/webhook.yaml":              testutils.ReadFileOrDie(filepath.Join(buildPathToProjectFiles(), "webhooks", "webhook.yaml")),
   222  				"settings/accountLinkingSecret.yaml": []byte(strings.Join([]string{"encryptedClientSecret: bar", "encryptionKeyVersion: 1"}, "\n")),
   223  			},
   224  			wantRequests: []map[string]interface{}{
   225  				map[string]interface{}{
   226  					"parent": "projects/placeholder_project",
   227  					"files": map[string]interface{}{
   228  						"configFiles": map[string]interface{}{
   229  							"configFiles": []map[string]interface{}{
   230  								map[string]interface{}{
   231  									"filePath": "actions/actions.yaml",
   232  									"actions": unmarshal(t, filepath.Join(buildPathToProjectFiles(), "actions", "actions.yaml")),
   233  								},
   234  								map[string]interface{}{
   235  									"filePath": "manifest.yaml",
   236  									"manifest": unmarshal(t, path.Join(buildPathToProjectFiles(), "manifest.yaml")),
   237  								},
   238  								map[string]interface{}{
   239  									"filePath": "settings/settings.yaml",
   240  									"settings": unmarshal(t, path.Join(buildPathToProjectFiles(), "settings", "settings.yaml")),
   241  								},
   242  								map[string]interface{}{
   243  									"filePath": "settings/zh-TW/settings.yaml",
   244  									"settings": unmarshal(t, path.Join(buildPathToProjectFiles(), "settings", "zh-TW", "settings.yaml")),
   245  								},
   246  								map[string]interface{}{
   247  									"filePath": "webhooks/webhook.yaml",
   248  									"webhook":  unmarshal(t, path.Join(buildPathToProjectFiles(), "webhooks", "webhook.yaml")),
   249  								},
   250  								map[string]interface{}{
   251  									"filePath": "settings/accountLinkingSecret.yaml",
   252  									"accountLinkingSecret": map[string]interface{}{
   253  										"encryptedClientSecret": "bar",
   254  										"encryptionKeyVersion":  1,
   255  									},
   256  								},
   257  							},
   258  						},
   259  					},
   260  				},
   261  				map[string]interface{}{
   262  					"parent": "projects/placeholder_project",
   263  					"files": map[string]interface{}{
   264  						"dataFiles": map[string]interface{}{
   265  							"dataFiles": []map[string]interface{}{
   266  								map[string]interface{}{
   267  									"filePath":    "resources/images/smallLogo.jpg",
   268  									"contentType": "image/jpeg",
   269  									"payload":     testutils.ReadFileOrDie(path.Join(buildPathToProjectFiles(), "resources", "images", "smallLogo.jpg")),
   270  								},
   271  								map[string]interface{}{
   272  									"filePath":    "resources/audio/confirmation_01.mp3",
   273  									"contentType": "audio/mpeg",
   274  									"payload":     testutils.ReadFileOrDie(path.Join(buildPathToProjectFiles(), "resources", "audio", "confirmation_01.mp3")),
   275  								},
   276  								map[string]interface{}{
   277  									"filePath":    "resources/images/zh-TW/smallLogo.jpg",
   278  									"contentType": "image/jpeg",
   279  									"payload":     testutils.ReadFileOrDie(path.Join(buildPathToProjectFiles(), "resources", "images", "zh-TW", "smallLogo.jpg")),
   280  								},
   281  							},
   282  						},
   283  					},
   284  				},
   285  			},
   286  			wantErrorMessageToContain: "",
   287  		},
   288  		{
   289  			projFiles:                 map[string][]byte{},
   290  			wantRequests:              nil,
   291  			wantErrorMessageToContain: "configuration files for your Action were not found",
   292  		},
   293  	}
   294  	for _, tc := range tests {
   295  		p := NewMock(tc.projFiles)
   296  		r, w := io.Pipe()
   297  		ch := make(chan []byte)
   298  		errCh := make(chan error)
   299  		go func() {
   300  			b, err := ioutil.ReadAll(r)
   301  			ch <- b
   302  			errCh <- err
   303  		}()
   304  		err := sendFilesToServerJSON(p, w, func() map[string]interface{} {
   305  			// TODO: Parametrize this to enable testing of various requests.
   306  			// This will remove need for request tests in request_test.
   307  			return request.WriteDraft("placeholder_project")
   308  		})
   309  		gotBytes := <-ch
   310  		if err := <-errCh; err != nil {
   311  			t.Errorf("Unable to read from pipe: got %v, input %v", err, tc.projFiles)
   312  		}
   313  		if tc.wantRequests != nil {
   314  			wantBytes, err := json.Marshal(tc.wantRequests)
   315  			if err != nil {
   316  				t.Errorf("Could not marshall into JSON: got %v", err)
   317  			}
   318  			var got []map[string]interface{}
   319  			if err := json.Unmarshal(gotBytes, &got); err != nil {
   320  				t.Errorf("Could not unmarshall to JSON: got %v", err)
   321  			}
   322  			// Checks request were sent in alphabetical order of filenames.
   323  			var fps []string
   324  			for _, v := range got {
   325  				if fp, ok := v["filePath"]; ok {
   326  					fps = append(fps, fp.(string))
   327  				}
   328  			}
   329  			if ok := sort.StringsAreSorted(fps); !ok {
   330  				t.Errorf("Expected requests to be in alphabetical order, but got %v\n", fps)
   331  			}
   332  			var want []map[string]interface{}
   333  			if err := json.Unmarshal(wantBytes, &want); err != nil {
   334  				t.Errorf("Could not unmarshall to JSON: got %v", err)
   335  			}
   336  			if diff := cmp.Diff(want, got, cmpopts.SortSlices(func(l, r interface{}) bool {
   337  				lb, err := json.Marshal(l)
   338  				if err != nil {
   339  					t.Errorf("can not marshal %v to JSON: %v", lb, err)
   340  				}
   341  				rb, err := json.Marshal(r)
   342  				if err != nil {
   343  					t.Errorf("can not marshal %v to JSON: %v", rb, err)
   344  				}
   345  				return string(lb) < string(rb)
   346  			})); diff != "" {
   347  				t.Errorf("sendFilesToServerJSON didn't send correct files: diff (-want, +got)\n%s", diff)
   348  			}
   349  		} else {
   350  			if !strings.Contains(err.Error(), tc.wantErrorMessageToContain) {
   351  				t.Errorf("sendFilesToServerJSON got %v, but want the error to have %v\n", err, tc.wantErrorMessageToContain)
   352  			}
   353  		}
   354  	}
   355  }
   356  
   357  func TestProcWritePreviewResponse(t *testing.T) {
   358  	tests := []struct {
   359  		in      []byte
   360  		wantURL string
   361  	}{
   362  		{
   363  			in: []byte(
   364  				`
   365  {
   366   "simulatorUrl": "https://google.com"
   367  }`,
   368  			),
   369  			wantURL: "https://google.com",
   370  		},
   371  		{
   372  			in: []byte(
   373  				`
   374  {
   375  	"simulatorUrl": "https://google.com",
   376  	"validationResults": {
   377  		"results": [
   378  			{
   379  				"validationMessage": "Your app must have a 32x32 logo"
   380  			}
   381  		]
   382  	}
   383  }`,
   384  			),
   385  			wantURL: "https://google.com",
   386  		},
   387  		{
   388  			in:      []byte("{}"),
   389  			wantURL: "",
   390  		},
   391  		{
   392  			in: []byte(
   393  				`
   394  {
   395  	"simulatorUrl": "https://google.com",
   396  	"validationResults": {
   397  		"results": [
   398  			{}
   399  		]
   400  	}
   401  }`,
   402  			),
   403  			wantURL: "https://google.com",
   404  		},
   405  	}
   406  	for _, tc := range tests {
   407  		gotURL, err := procWritePreviewResponse(tc.in)
   408  		if err != nil {
   409  			t.Errorf("procWritePreviewResponse returned %v, but want %v, input %v", err, nil, tc.in)
   410  		}
   411  		if tc.wantURL != gotURL {
   412  			t.Errorf("procWritePreviewResponse didn't set the right value of the simulator URL: got %v, want %v, input %v", gotURL, tc.wantURL, tc.in)
   413  		}
   414  	}
   415  }
   416  
   417  func TestProcWriteDraftResponse(t *testing.T) {
   418  	tests := []struct {
   419  		body string
   420  	}{
   421  		{
   422  			body: `
   423  {
   424  	"name": "foo/bar",
   425  	"validationResults": {
   426  		"results": [
   427  			{
   428  				"validationMessage": "Your app must have a 32x32 logo"
   429  			}
   430  		]
   431  	}
   432  }
   433  `,
   434  		},
   435  		{
   436  			body: `
   437  {
   438  	"name": "foo/bar",
   439  	"validationResults": {
   440  		"results": [
   441  			{}
   442  		]
   443  	}
   444  }
   445  `,
   446  		},
   447  	}
   448  	for _, tc := range tests {
   449  		if err := procWriteDraftResponse([]byte(tc.body)); err != nil {
   450  			t.Errorf("procWriteDraftResponse returned %v, but want %v", err, nil)
   451  		}
   452  	}
   453  }
   454  
   455  func TestErrorMessage(t *testing.T) {
   456  	tests := []struct {
   457  		code    int
   458  		message string
   459  		details []map[string]interface{}
   460  		want    string
   461  	}{
   462  		{
   463  			code:    500,
   464  			message: "Internal error occurred",
   465  			details: []map[string]interface{}{
   466  				map[string]interface{}{
   467  					"@type":  "type.googleapis.com/google.rpc.DebugInfo",
   468  					"detail": "[ORIGINAL ERROR]",
   469  				},
   470  			},
   471  			want: strings.Join([]string{
   472  				"{",
   473  				"  \"error\": {",
   474  				"    \"code\": 500,",
   475  				"    \"message\": \"Internal error occurred\"",
   476  				"  }",
   477  				"}",
   478  			}, "\n"),
   479  		},
   480  		{
   481  			code:    400,
   482  			message: "Invalid Argument",
   483  			details: []map[string]interface{}{
   484  				map[string]interface{}{
   485  					"@type":  "type.googleapis.com/google.rpc.InvalidArgument",
   486  					"detail": "[ORIGINAL ERROR]",
   487  				},
   488  			},
   489  			want: strings.Join([]string{
   490  				`{`,
   491  				`  "error": {`,
   492  				`    "code": 400,`,
   493  				`    "message": "Invalid Argument",`,
   494  				`    "details": [`,
   495  				`      {`,
   496  				`        "@type": "type.googleapis.com/google.rpc.InvalidArgument",`,
   497  				`        "detail": "[ORIGINAL ERROR]"`,
   498  				`      }`,
   499  				`    ]`,
   500  				`  }`,
   501  				`}`,
   502  			}, "\n"),
   503  		},
   504  		{
   505  			code:    400,
   506  			message: "Failed precondition",
   507  			details: []map[string]interface{}{
   508  				map[string]interface{}{
   509  					"@type":  "type.googleapis.com/google.rpc.FailedPrecondition",
   510  					"detail": "[ORIGINAL ERROR]",
   511  				},
   512  			},
   513  			want: strings.Join([]string{
   514  				`{`,
   515  				`  "error": {`,
   516  				`    "code": 400,`,
   517  				`    "message": "Failed precondition",`,
   518  				`    "details": [`,
   519  				`      {`,
   520  				`        "@type": "type.googleapis.com/google.rpc.FailedPrecondition",`,
   521  				`        "detail": "[ORIGINAL ERROR]"`,
   522  				`      }`,
   523  				`    ]`,
   524  				`  }`,
   525  				`}`,
   526  			}, "\n"),
   527  		},
   528  	}
   529  	for _, tc := range tests {
   530  		in := &PublicError{}
   531  		in.Error.Code = tc.code
   532  		in.Error.Message = tc.message
   533  		in.Error.Details = tc.details
   534  		got := errorMessage(in)
   535  		if got != tc.want {
   536  			t.Errorf("errorMessages got %v, want %v", got, tc.want)
   537  		}
   538  	}
   539  }
   540  
   541  func TestReceiveStream(t *testing.T) {
   542  	tests := []struct {
   543  		body      string
   544  		wantFiles []string
   545  		name      string
   546  	}{
   547  		{
   548  			name: "only settings",
   549  			body: strings.Join([]string{
   550  				`[`,
   551  				`  {`,
   552  				`    "files": {`,
   553  				`      "configFiles": {`,
   554  				`        "configFiles":`,
   555  				`          [`,
   556  				`            {`,
   557  				`	             "filePath": "settings/settings.yaml",`,
   558  				`	             "settings": {`,
   559  				`		             "actionsForFamilyUpdated": true,`,
   560  				`		             "category": "GAMES_AND_TRIVIA",`,
   561  				`		             "defaultLocale": "en",`,
   562  				`		             "localizedSettings": {`,
   563  				`					         "developerEmail": "dschrute@gmail.com",`,
   564  				`				           "developerName": "Dwight Schrute",`,
   565  				` 	       			   "displayName": "Mike Simple Question",`,
   566  				`       				   "fullDescription": "Test Full Description",`,
   567  				`				           "sampleInvocations": [`,
   568  				`					           "Talk to Mike Simple Question"`,
   569  				`				           ],`,
   570  				`				           "smallLogoImage": "$resources.images.square"`,
   571  				`	                },`,
   572  				`	               "projectId": "placeholder_project"`,
   573  				`              }`,
   574  				`            }`,
   575  				`          ]`,
   576  				`        }`,
   577  				`     }`,
   578  				`  }`,
   579  				`]`}, "\n"),
   580  			wantFiles: []string{"settings/settings.yaml"},
   581  		},
   582  		{
   583  			name: "configFiles and dataFiles",
   584  			body: strings.Join([]string{
   585  				`[`,
   586  				`  {`,
   587  				`    "files": {`,
   588  				`      "configFiles": {`,
   589  				`        "configFiles": [`,
   590  				`          {`,
   591  				`            "filePath": "settings/settings.yaml",`,
   592  				`	           "settings": {`,
   593  				`     		     "category": "GAMES_AND_TRIVIA"`,
   594  				`            }`,
   595  				`          },`,
   596  				`          {`,
   597  				`	           "filePath": "custom/global/actions.intent.MAIN.yaml",`,
   598  				`	           "globalIntentEvent": {`,
   599  				`		           "handler": {`,
   600  				`  		           "staticPrompt": {`,
   601  				`			           "candidates": [`,
   602  				`   			         {`,
   603  				`				             "promptResponse": {`,
   604  				`					             "firstSimple": {`,
   605  				`    					             "variants": [`,
   606  				` 						                 {`,
   607  				`					                     "speech": "$resources.strings.WELCOME",`,
   608  				`						                   "text": "$resources.strings.WELCOME"`,
   609  				`        						           }`,
   610  				`					                  ]`,
   611  				`					                }`,
   612  				`				                }`,
   613  				`    				         }`,
   614  				`			             ]`,
   615  				` 			         }`,
   616  				`		            },`,
   617  				`	              "transitionToScene": "questionpage"`,
   618  				`	           }`,
   619  				`          },`,
   620  				`          {`,
   621  				`	           "filePath": "settings/es/settings.yaml",`,
   622  				`	           "settings": {`,
   623  				`		           "localizedSettings": {`,
   624  				`			           "displayName": "Mike Pregunta simple",`,
   625  				`			           "fullDescription": "Descripción completa de la muestra"`,
   626  				`		           }`,
   627  				`	           }`,
   628  				`          }`,
   629  				`        ]`,
   630  				`      }`,
   631  				`    }`,
   632  				`  },`,
   633  				`  {`,
   634  				`    "files": {`,
   635  				`      "dataFiles": {`,
   636  				`        "dataFiles": [`,
   637  				`          {`,
   638  				`	           "filePath": "resources/images/foo.png",`,
   639  				`            "contentType": "images/png",`,
   640  				`            "payload": ""`,
   641  				`          }`,
   642  				`        ]`,
   643  				`      }`,
   644  				`    }`,
   645  				`  }`,
   646  				`]`}, "\n"),
   647  			wantFiles: []string{"resources/images/foo.png", "settings/es/settings.yaml", "custom/global/actions.intent.MAIN.yaml"},
   648  		},
   649  	}
   650  	for _, tc := range tests {
   651  		t.Run(tc.name, func(t *testing.T) {
   652  			// Setup directory where receiveStream will write files to.
   653  			dirName, err := ioutil.TempDir(testutils.TestTmpDir, "actions-sdk-cli-project-folder")
   654  			if err != nil {
   655  				t.Fatalf("Can't create temporary directory under %q: %v", testutils.TestTmpDir, err)
   656  			}
   657  			defer func() {
   658  				if err := os.RemoveAll(dirName); err != nil {
   659  					t.Fatalf("Can't remove temp directory: %v", err)
   660  				}
   661  			}()
   662  			proj := studio.New([]byte("secret"), dirName)
   663  			seen := map[string]bool{}
   664  			if err := receiveStream(proj, strings.NewReader(tc.body), false, seen); err != nil {
   665  				t.Errorf("receiveStream returned %v, but expected to return %v", err, nil)
   666  			}
   667  			for _, v := range tc.wantFiles {
   668  				osPath := filepath.FromSlash(v)
   669  				// TODO: Verify the content of the written file
   670  				_, err := ioutil.ReadFile(filepath.Join(proj.ProjectRoot(), osPath))
   671  				if err != nil {
   672  					t.Errorf("receiveStream expected to write file to disk, but got %v", err)
   673  				}
   674  				if !seen[v] {
   675  					t.Errorf("receiveStream expected to mark file as seen, but did not")
   676  				}
   677  			}
   678  		})
   679  	}
   680  }
   681  
   682  func TestFindExtra(t *testing.T) {
   683  	tests := []struct {
   684  		a    map[string][]byte
   685  		b    map[string]bool
   686  		want []string
   687  	}{
   688  		{
   689  			a: map[string][]byte{
   690  				"settings/settings.yaml":           []byte("abc"),
   691  				"manifest.yaml":                    []byte("abc"),
   692  				"resources/strings/en/bundle.yaml": []byte("abc"),
   693  			},
   694  			b: map[string]bool{
   695  				"settings/settings.yaml":           true,
   696  				"manifest.yaml":                    true,
   697  				"resources/strings/en/bundle.yaml": true,
   698  			},
   699  			want: nil,
   700  		},
   701  		{
   702  			a: map[string][]byte{
   703  				"settings/settings.yaml": []byte("abc"),
   704  				"manifest.yaml":          []byte("abc"),
   705  			},
   706  			b: map[string]bool{
   707  				"settings/settings.yaml":           true,
   708  				"manifest.yaml":                    true,
   709  				"resources/strings/en/bundle.yaml": true,
   710  			},
   711  			want: nil,
   712  		},
   713  		{
   714  			a: map[string][]byte{
   715  				"settings/settings.yaml":           []byte("abc"),
   716  				"manifest.yaml":                    []byte("abc"),
   717  				"resources/strings/en/bundle.yaml": []byte("abc"),
   718  			},
   719  			b: map[string]bool{
   720  				"settings/settings.yaml": true,
   721  				"manifest.yaml":          true,
   722  			},
   723  			want: []string{"resources/strings/en/bundle.yaml"},
   724  		},
   725  	}
   726  	for _, tc := range tests {
   727  		got := findExtra(tc.a, tc.b)
   728  		sort.Strings(got)
   729  		sort.Strings(tc.want)
   730  		if diff := cmp.Diff(tc.want, got); diff != "" {
   731  			t.Errorf("findExtra didn't return correct result: diff (-want, +got)\n%s", diff)
   732  		}
   733  	}
   734  }