github.com/actions-on-google/gactions@v3.2.0+incompatible/project/studio_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 studio
    16  
    17  import (
    18  	"archive/zip"
    19  	"bytes"
    20  	"errors"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"runtime"
    27  	"strings"
    28  	"testing"
    29  
    30  	"github.com/actions-on-google/gactions/api/testutils"
    31  	"github.com/actions-on-google/gactions/project"
    32  	"github.com/google/go-cmp/cmp"
    33  )
    34  
    35  type MockStudio struct {
    36  	root         string
    37  	files        map[string][]byte
    38  	clientSecret []byte
    39  	projectID    string
    40  }
    41  
    42  func (p MockStudio) ProjectID() string {
    43  	return p.projectID
    44  }
    45  
    46  func NewMock(root string) MockStudio {
    47  	m := MockStudio{}
    48  	m.root = root
    49  	m.files = map[string][]byte{}
    50  	for k, v := range configFiles {
    51  		m.files[k] = v
    52  	}
    53  	for k, v := range dataFiles {
    54  		m.files[k] = v
    55  	}
    56  	// Add extra files that should be ignored by the CLI
    57  	m.files[".git"] = []byte("...")
    58  	folders := []string{
    59  		"verticals",
    60  		"actions",
    61  		"custom",
    62  		"custom/global",
    63  		"custom/intents",
    64  		"custom/prompts",
    65  		"custom/scenes",
    66  		"custom/types",
    67  		"webhooks/",
    68  	}
    69  	for _, v := range folders {
    70  		m.files[path.Join(v, ".DS_Store")] = []byte("...")
    71  	}
    72  	m.files["webhooks/webhook1/.git"] = []byte("...")
    73  	return m
    74  }
    75  
    76  func (MockStudio) Download(sample project.SampleProject, dest string) error {
    77  	return nil
    78  }
    79  
    80  func (MockStudio) AlreadySetup(pathToWorkDir string) bool {
    81  	return false
    82  }
    83  
    84  func (p MockStudio) Files() (map[string][]byte, error) {
    85  	return p.files, nil
    86  }
    87  
    88  func (MockStudio) ClientSecretJSON() ([]byte, error) {
    89  	return []byte{}, nil
    90  }
    91  
    92  func (p MockStudio) ProjectRoot() string {
    93  	return p.root
    94  }
    95  
    96  func obtainProjectDirectory(t *testing.T, got string, dirName string) string {
    97  	t.Helper()
    98  	prefix := ""
    99  	if runtime.GOOS == "darwin" {
   100  	
   101  	  if strings.HasPrefix(got, "/Volumes/BuildData/tmpfs") {
   102  		prefix = "/Volumes/BuildData/tmpfs"
   103  	  } else {
   104  		prefix = "/private"
   105  	  }
   106  	  return filepath.Join(prefix, dirName)
   107  	}
   108  	// Windows case
   109  	return filepath.Join(testutils.TestTmpRoot(), dirName)
   110  }
   111  
   112  var configFiles = map[string][]byte{
   113  	"verticals/character_alarm.yaml":           []byte("name: foo"),
   114  	"actions/actions.yaml":                     []byte("intent: bar"),
   115  	"manifest.yaml":                            []byte("version: 1"),
   116  	"custom/global/actions.intent.CANCEL.yaml": []byte("transitionToScene: actions.scene.END_CONVERSATION"),
   117  	"custom/intents/help.yaml":                 []byte("phrase: hello"),
   118  	"custom/intents/ru/help.yaml":              []byte("phrase: hello"),
   119  	"custom/prompts/foo.yaml":                  []byte("prompt: yes"),
   120  	"custom/prompts/ru/foo.yaml":               []byte("prompt: yes"),
   121  	"custom/scenes/a.yaml":                     []byte("name: a"),
   122  	"custom/types/b.yaml":                      []byte("type: b"),
   123  	"custom/types/ru/b.yaml":                   []byte("type: b"),
   124  	"webhooks/webhook1.yaml": []byte(
   125  		`
   126  inlineCloudFunction:
   127    execute_function: hello
   128  `),
   129  	"webhooks/webhook2.yaml": []byte(
   130  		`
   131  external_endpoint:
   132    base_url: https://google.com
   133    http_headers:
   134      content-type: application/json
   135    endpoint_api_version: 1
   136  `),
   137  	"resources/strings/bundle.yaml": []byte(
   138  		`
   139  x: "777"
   140  y: "777"
   141  greeting: "hello world"
   142  `),
   143  }
   144  
   145  var dataFiles = map[string][]byte{
   146  	"resources/images/a.png":         []byte("abc123"),
   147  	"resources/audio/b.mp3":          []byte("cde456"),
   148  	"resources/audio/c.wav":          []byte("mno234"),
   149  	"webhooks/webhook1/index.js":     []byte("exports.hello = functions.https.onRequest(app);"),
   150  	"webhooks/webhook1/package.json": []byte("{}"),
   151  	"resources/animations/d.flr":     []byte("fgh789"),
   152  }
   153  
   154  func TestAlreadySetup(t *testing.T) {
   155  	proj := New([]byte{}, ".")
   156  	tests := []struct {
   157  		dirExists   bool
   158  		dirEmpty    bool
   159  		wantIsSetup bool
   160  	}{
   161  		{
   162  			dirExists:   true,
   163  			dirEmpty:    false,
   164  			wantIsSetup: true,
   165  		},
   166  		{
   167  			dirExists:   true,
   168  			dirEmpty:    true,
   169  			wantIsSetup: false,
   170  		},
   171  		{
   172  			dirExists:   false,
   173  			dirEmpty:    true,
   174  			wantIsSetup: false,
   175  		},
   176  		{
   177  			dirExists:   false,
   178  			dirEmpty:    false,
   179  			wantIsSetup: false,
   180  		},
   181  	}
   182  	for _, tc := range tests {
   183  		var dirName string
   184  		if tc.dirExists {
   185  			var err error
   186  			dirName, err = ioutil.TempDir(testutils.TestTmpDir, "actions-sdk-cli-project-folder")
   187  			if err != nil {
   188  				t.Errorf("Can't create temporary directory under %q: %v", testutils.TestTmpDir, err)
   189  			}
   190  			defer os.RemoveAll(dirName)
   191  			if !tc.dirEmpty {
   192  				tempFile, err := ioutil.TempFile(dirName, "actions-sdk-*.yaml")
   193  				fmt.Printf("tempFile = %v\n", tempFile.Name())
   194  				if err != nil {
   195  					t.Fatalf("can not create tempfile. got %v", err)
   196  				}
   197  				defer tempFile.Close()
   198  			}
   199  		}
   200  		if isSetup := proj.AlreadySetup(dirName); isSetup != tc.wantIsSetup {
   201  			t.Errorf("AlreadySetup returned %v, expected %v, when project directory exists (%v) and is empty (%v)", isSetup, tc.wantIsSetup, tc.dirExists, tc.dirEmpty)
   202  		}
   203  	}
   204  }
   205  
   206  func TestFilesWhenDirectoryManifestPresent(t *testing.T) {
   207  	dirName, err := ioutil.TempDir(testutils.TestTmpDir, "actions-sdk-cli-project-folder")
   208  	if err != nil {
   209  		t.Fatalf("Can't create temporary directory under %q: %v", testutils.TestTmpDir, err)
   210  	}
   211  	proj := New([]byte("secret"), dirName)
   212  	defer os.RemoveAll(dirName)
   213  	// first file
   214  	err = ioutil.WriteFile(filepath.Join(dirName, "manifest.yaml"), []byte("hello"), 0666)
   215  	if err != nil {
   216  		t.Fatalf("Can't write a file under %q: %v", dirName, err)
   217  	}
   218  	// second file
   219  	err = ioutil.WriteFile(filepath.Join(dirName, "second-file.yaml"), []byte("world"), 0666)
   220  	if err != nil {
   221  		t.Fatalf("Can't create a file under %q: %v", dirName, err)
   222  	}
   223  	got, err := proj.Files()
   224  	if err != nil {
   225  		t.Errorf("Files got %v, want %v\n", err, nil)
   226  	}
   227  	gotNorm := make(map[string][]byte)
   228  	// strip parent paths to eliminate undeterminism
   229  	for k, v := range got {
   230  		gotNorm[filepath.Base(k)] = v
   231  	}
   232  	want := map[string][]byte{
   233  		"manifest.yaml":    []byte("hello"),
   234  		"second-file.yaml": []byte("world"),
   235  	}
   236  	if !cmp.Equal(gotNorm, want) {
   237  		t.Errorf("Files returned incorrect files, got %v, want %v", got, want)
   238  	}
   239  }
   240  
   241  func TestClientSecretJSON(t *testing.T) {
   242  	dirName, err := ioutil.TempDir(testutils.TestTmpDir, "actions-sdk-cli-project-folder")
   243  	if err != nil {
   244  		t.Fatalf("Can't create temporary directory under %q: %v", testutils.TestTmpDir, err)
   245  	}
   246  	defer os.RemoveAll(dirName)
   247  	want := "{client_id: 123456789}"
   248  	err = ioutil.WriteFile(filepath.Join(dirName, "test-client-secret.json"), []byte(want), 0666)
   249  	if err != nil {
   250  		t.Fatalf("Can't create a file under %q: %v", dirName, err)
   251  	}
   252  	proj := Studio{clientSecretJSON: []byte(want)}
   253  	got, err := proj.ClientSecretJSON()
   254  	if err != nil {
   255  		t.Errorf("ClientSecretJSON got %v, want %v", err, nil)
   256  	}
   257  	if string(got) != want {
   258  		t.Errorf("ClientSecretJSON returned incorrect result, got %v, want %v", string(got), want)
   259  	}
   260  }
   261  
   262  func TestConfigFiles(t *testing.T) {
   263  	p := NewMock(".")
   264  	want := configFiles
   265  	files, _ := p.Files()
   266  	got := ConfigFiles(files)
   267  	if diff := cmp.Diff(got, want); diff != "" {
   268  		t.Errorf("ConfigFiles returned %v, want %v, diff %v", got, want, diff)
   269  	}
   270  }
   271  
   272  func TestDataFiles(t *testing.T) {
   273  	p := NewMock(".")
   274  	want := map[string][]byte{}
   275  	// Server expects Cloud Functions to have the filePath stripped
   276  	// (i.e. webhooks/myfunction/index.js -> ./index.js)
   277  	for k, v := range dataFiles {
   278  		if !strings.Contains(k, "resources/") {
   279  			want[path.Base(k)] = v
   280  		} else {
   281  			want[k] = v
   282  		}
   283  	}
   284  	p.files["webhooks/myfunction/node_modules/foo/foo.js"] = []byte("console.log('hello world');")
   285  	got, err := DataFiles(p)
   286  	if err != nil {
   287  		t.Errorf("DataFiles got %v, want %v", err, nil)
   288  	}
   289  	if zipped, ok := got["webhooks/webhook1.zip"]; !ok {
   290  		t.Errorf("DataFiles didn't include webhook1.zip into a map of data files: data files = %v", got)
   291  	} else {
   292  		r, err := zip.NewReader(bytes.NewReader(zipped), int64(len(zipped)))
   293  		if err != nil {
   294  			t.Fatalf("can not create a zip.NewReader: got %v", err)
   295  		}
   296  		for _, f := range r.File {
   297  			rc, err := f.Open()
   298  			if err != nil {
   299  				t.Fatalf("can not open %v: got %v", f.Name, err)
   300  			}
   301  			b, err := ioutil.ReadAll(rc)
   302  			if err != nil {
   303  				t.Fatalf("can not read from %v: got %v", f.Name, err)
   304  			}
   305  			rc.Close()
   306  			got[f.Name] = b
   307  		}
   308  		delete(got, "webhooks/webhook1.zip")
   309  	}
   310  	if diff := cmp.Diff(got, want); diff != "" {
   311  		t.Errorf("DataFiles returned %v, want %v, diff %v", got, want, diff)
   312  	}
   313  }
   314  
   315  func TestAddInlineWebhooksReturnsErrorWithInvalidWebhookYaml(t *testing.T) {
   316  	p := NewMock(".")
   317  	p.files["webhooks/malformed_webhook.yaml"] = []byte(
   318  		`
   319  external_endpoint:
   320     base_url: https://google.com
   321    endpoint_api_version: 1
   322  `)
   323  
   324  	err := addInlineWebhooks(map[string][]byte{}, p.files, "")
   325  	if err == nil || !strings.Contains(err.Error(), "malformed_webhook.yaml has incorrect syntax") {
   326  		t.Errorf("Expected error not thrown")
   327  	}
   328  }
   329  
   330  func TestProjectIDFound(t *testing.T) {
   331  	want := "my_project123"
   332  	files := map[string][]byte{
   333  		"settings/settings.yaml": []byte(fmt.Sprintf("projectId: %v", want)),
   334  	}
   335  	proj := MockStudio{files: files}
   336  	got, err := ProjectID(proj)
   337  	if err != nil {
   338  		t.Errorf("ProjectID returned %v, want %v", err, nil)
   339  	}
   340  	if got != want {
   341  		t.Errorf("ProjectID returned %v, want %v", got, want)
   342  	}
   343  }
   344  
   345  func TestProjectIDSNotFound(t *testing.T) {
   346  	files := map[string][]byte{
   347  		"manifest.yaml": []byte("version: 1"),
   348  	}
   349  	proj := MockStudio{files: files}
   350  	_, err := ProjectID(proj)
   351  	if err == nil {
   352  		t.Errorf("When settings.yaml is absent, ProjectID returned %v, want %v", err, errors.New("can't find a projectId: settings.yaml not found"))
   353  	}
   354  	files = map[string][]byte{
   355  		"settings.yaml": []byte("display_name: foo"),
   356  	}
   357  	proj = MockStudio{files: files}
   358  	_, err = ProjectID(proj)
   359  	if err == nil {
   360  		t.Errorf("When settings.yaml doesn't contain projectId field, ProjectID returned %v, want %v", err, errors.New("projectId is not present in the settings file"))
   361  	}
   362  }
   363  
   364  func TestUnixPath(t *testing.T) {
   365  	tests := []struct {
   366  		in   string
   367  		want string
   368  	}{
   369  		{
   370  			in:   "/google/assistant/aog/sdk/",
   371  			want: "/google/assistant/aog/sdk/",
   372  		},
   373  		{
   374  			in:   "\\google\\assistant\\aog\\sdk",
   375  			want: "/google/assistant/aog/sdk",
   376  		},
   377  		{
   378  			in:   "foo/",
   379  			want: "foo/",
   380  		},
   381  		{
   382  			in:   "foo\\",
   383  			want: "foo/",
   384  		},
   385  		{
   386  			in:   "dir\\to\\foo bar",
   387  			want: "dir/to/foo bar",
   388  		},
   389  	}
   390  	for _, tc := range tests {
   391  		if got := winToUnix(tc.in); got != tc.want {
   392  			t.Errorf("unixPath returned %v, want %v", got, tc.want)
   393  		}
   394  	}
   395  }
   396  
   397  func TestSetProjectID(t *testing.T) {
   398  	tests := []struct {
   399  		settings []byte
   400  		flag     string
   401  		want     string
   402  	}{
   403  		{ // Case 1.
   404  			settings: nil,
   405  			flag:     "",
   406  			want:     "",
   407  		},
   408  		{ // Case 2.
   409  			settings: []byte("projectId: placeholder_project"),
   410  			flag:     "",
   411  			want:     "placeholder_project",
   412  		},
   413  		{ // Case 3.
   414  			settings: []byte("projectId: hello-world"),
   415  			flag:     "",
   416  			want:     "hello-world",
   417  		},
   418  		{ // Case 4.
   419  			settings: nil,
   420  			flag:     "foobar",
   421  			want:     "foobar",
   422  		},
   423  		{ // Case 5.
   424  			settings: []byte("projectId: placeholder_project"),
   425  			flag:     "hello-world",
   426  			want:     "hello-world",
   427  		},
   428  		{
   429  			settings: []byte("projectId: hello-world"),
   430  			flag:     "foobar",
   431  			want:     "foobar",
   432  		},
   433  	}
   434  	for _, tc := range tests {
   435  		t.Run("foo", func(t *testing.T) {
   436  			dirName, err := ioutil.TempDir(testutils.TestTmpDir, "actions-sdk-cli-project-folder")
   437  			if err != nil {
   438  				t.Fatalf("Can't create temporary directory under %q: %v", testutils.TestTmpDir, err)
   439  			}
   440  			defer func() {
   441  				if err := os.RemoveAll(dirName); err != nil {
   442  					t.Fatalf("Can't remove temp directory: %v", err)
   443  				}
   444  			}()
   445  			if tc.settings != nil {
   446  				fp := filepath.Join(dirName, "settings", "settings.yaml")
   447  				if err := os.MkdirAll(filepath.Dir(fp), 0750); err != nil {
   448  					t.Fatalf("Can't create settings directory: %v", err)
   449  				}
   450  				if err := ioutil.WriteFile(fp, tc.settings, 0640); err != nil {
   451  					t.Fatalf("Can't create settings file: %v", err)
   452  				}
   453  			}
   454  			studio := New([]byte{}, dirName)
   455  			if err := (&studio).SetProjectID(tc.flag); err != nil && tc.settings != nil {
   456  				t.Errorf("SetProjectID returned %v, want %v", err, nil)
   457  			}
   458  			if studio.projectID != tc.want {
   459  				t.Errorf("Project ID is %v after calling SetProjectID, but want %v", studio.projectID, tc.want)
   460  			}
   461  		})
   462  	}
   463  }
   464  
   465  func cloudFuncZip(t *testing.T) []byte {
   466  	t.Helper()
   467  	files := map[string][]byte{}
   468  	for k, v := range dataFiles {
   469  		if strings.Contains(k, ".js") {
   470  			files[k] = v
   471  		}
   472  	}
   473  	b, err := zipFiles(files)
   474  	if err != nil {
   475  		t.Fatalf("Can not zip %v: %v", files, err)
   476  	}
   477  	return b
   478  }
   479  
   480  func TestWriteToDiskToNonEmptyDir(t *testing.T) {
   481  	tests := []struct {
   482  		user  string
   483  		force bool
   484  		name  string
   485  	}{
   486  		{
   487  			user:  "yes",
   488  			force: false,
   489  			name:  "User says yes",
   490  		},
   491  		{
   492  			user:  "no",
   493  			name:  "User says no",
   494  			force: false,
   495  		},
   496  		{
   497  			user:  "",
   498  			force: true,
   499  			name:  "Force is true",
   500  		},
   501  	}
   502  	for _, tc := range tests {
   503  		t.Run(tc.name, func(t *testing.T) {
   504  			dirName, err := ioutil.TempDir(testutils.TestTmpDir, "actions-sdk-cli-project-folder")
   505  			if err != nil {
   506  				t.Fatalf("Can't create temporary directory under %q: %v", testutils.TestTmpDir, err)
   507  			}
   508  			defer os.RemoveAll(dirName)
   509  			proj := NewMock(dirName)
   510  			og := askYesNo
   511  			askYesNo = func(msg string) (string, error) {
   512  				return tc.user, nil
   513  			}
   514  			defer func() {
   515  				askYesNo = og
   516  			}()
   517  			if err := ioutil.WriteFile(filepath.Join(dirName, "manifest.yaml"), []byte("version:2.0"), 0640); err != nil {
   518  				t.Fatalf("Can't write %v: %v", filepath.Join(dirName, "manifest.yaml"), err)
   519  			}
   520  			if err := WriteToDisk(proj, "manifest.yaml", "", []byte("version:1.0"), tc.force); err != nil {
   521  				t.Errorf("WriteToDisk returned %v, want %v", err, nil)
   522  			}
   523  			if tc.user == "yes" || tc.force {
   524  				if !exists(filepath.Join(dirName, "manifest.yaml")) {
   525  					t.Errorf("WriteToDisk didn't write %v to disk", filepath.Join(dirName, "manifest.yaml"))
   526  				}
   527  				b, err := ioutil.ReadFile(filepath.Join(dirName, "manifest.yaml"))
   528  				if err != nil {
   529  					t.Errorf("Failed to read %v: %v", filepath.Join(dirName, "manifest.yaml"), err)
   530  				}
   531  				if len(b) == 0 {
   532  					t.Errorf("WriteToDisk wrote empty file %v", filepath.Join(dirName, "manifest.yaml"))
   533  				}
   534  			}
   535  		})
   536  	}
   537  }
   538  
   539  func TestWriteToDiskToEmptyDir(t *testing.T) {
   540  	tests := []struct {
   541  		path        string
   542  		contentType string
   543  		payload     []byte
   544  		wantFiles   []string
   545  		name        string
   546  	}{
   547  		{
   548  			path:        "webhooks/webhook1.zip",
   549  			contentType: "application/zip;zip_type=cloud_function",
   550  			payload:     cloudFuncZip(t),
   551  			wantFiles:   []string{"webhooks/webhook1/index.js", "webhooks/webhook1/package.json"},
   552  			name:        "Webhook.zip",
   553  		},
   554  		{
   555  			path:        "settings/en/settings.yaml",
   556  			contentType: "",
   557  			payload:     []byte("projectId: hello-world"),
   558  			wantFiles:   []string{"settings/en/settings.yaml"},
   559  			name:        "Settings",
   560  		},
   561  	}
   562  	for _, tc := range tests {
   563  		t.Run(tc.name, func(t *testing.T) {
   564  			dirName, err := ioutil.TempDir(testutils.TestTmpDir, "actions-sdk-cli-project-folder")
   565  			if err != nil {
   566  				t.Fatalf("Can't create temporary directory under %q: %v", testutils.TestTmpDir, err)
   567  			}
   568  			defer os.RemoveAll(dirName)
   569  			proj := NewMock(dirName)
   570  			if err := WriteToDisk(proj, tc.path, tc.contentType, tc.payload, false); err != nil {
   571  				t.Errorf("WriteToDisk got %v, want %v", err, nil)
   572  			}
   573  			for _, f := range tc.wantFiles {
   574  				fp := path.Join(dirName, f)
   575  				fp = filepath.FromSlash(fp)
   576  				if !exists(fp) {
   577  					t.Errorf("WriteToDisk didn't write %v to disk", fp)
   578  				}
   579  				b, err := ioutil.ReadFile(fp)
   580  				if err != nil {
   581  					t.Errorf("Failed to read %v: %v", fp, err)
   582  				}
   583  				if len(b) == 0 {
   584  					t.Errorf("WriteToDisk created an empty file %v, want not empty", fp)
   585  				}
   586  			}
   587  		})
   588  	}
   589  }
   590  
   591  func TestFindProjectRootWithConfig(t *testing.T) {
   592  	tests := []struct {
   593  		names   []string
   594  		err     error
   595  		cwd     string
   596  		name    string
   597  		sdkPath string
   598  	}{
   599  		{
   600  			names: []string{
   601  				"manifest.yaml",
   602  				project.ConfigName,
   603  				filepath.Join("settings", "settings.yaml"),
   604  				filepath.Join("mywebhook", "index.js"),
   605  			},
   606  			err:     nil,
   607  			cwd:     "",
   608  			sdkPath: ".",
   609  			name:    "CLI config is found, sdkPath is . and cwd is .",
   610  		},
   611  		{
   612  			names: []string{
   613  				filepath.Join("sdk", "manifest.yaml"),
   614  				project.ConfigName,
   615  				filepath.Join("sdk", "settings", "settings.yaml"),
   616  				filepath.Join("sdk", "mywebhook", "index.js"),
   617  			},
   618  			err:     nil,
   619  			cwd:     "",
   620  			sdkPath: "sdk/",
   621  			name:    "CLI config is found, sdkPath is sdk/ and cwd is .",
   622  		},
   623  		{
   624  			names: []string{
   625  				filepath.Join("settings", "settings.yaml"),
   626  				filepath.Join("verticals", "foo.yaml"),
   627  			},
   628  			err:  fmt.Errorf("%s not found, and manifest is not present", project.ConfigName),
   629  			cwd:  "",
   630  			name: "CLI config is not found and manifest is not present",
   631  		},
   632  		{
   633  			names: []string{
   634  				filepath.Join("settings", "settings.yaml"),
   635  				filepath.Join("verticals", "foo.yaml"),
   636  				project.ConfigName,
   637  			},
   638  			cwd:     "settings",
   639  			err:     nil,
   640  			sdkPath: ".",
   641  			name:    "CLI config is found and cwd is settings",
   642  		},
   643  		{
   644  			names: []string{
   645  				filepath.Join("settings", "settings.yaml"),
   646  				filepath.Join("verticals", "foo.yaml"),
   647  				project.ConfigName,
   648  			},
   649  			cwd:  "settings",
   650  			err:  errors.New("sdkPath can not be empty"),
   651  			name: "CLI config is found and cwd is settings",
   652  		},
   653  	}
   654  	for _, tc := range tests {
   655  		t.Run(tc.name, func(t *testing.T) {
   656  			dirName, err := ioutil.TempDir(testutils.TestTmpDir, "actions-sdk-cli-project-folder")
   657  			if err != nil {
   658  				t.Fatalf("Can't create temporary directory under %q: %v", testutils.TestTmpDir, err)
   659  			}
   660  			defer os.RemoveAll(dirName)
   661  			for _, f := range tc.names {
   662  				if err := os.MkdirAll(filepath.Join(dirName, filepath.Dir(f)), 0777); err != nil {
   663  					t.Errorf("Can't create a directory %v, got %v", filepath.Join(dirName, filepath.Dir(f)), err)
   664  				}
   665  				content := []byte("hello")
   666  				if strings.Contains(f, project.ConfigName) {
   667  					content = []byte(fmt.Sprintf("sdkPath: %s", tc.sdkPath))
   668  				}
   669  				if err := ioutil.WriteFile(filepath.Join(dirName, f), content, 0666); err != nil {
   670  					t.Fatalf("Can't write a file under %q: %v", dirName, err)
   671  				}
   672  			}
   673  
   674  			// wkdir is where CLI config file will be located.
   675  			wkdir := dirName
   676  			if tc.cwd != "" {
   677  				wkdir = filepath.Join(wkdir, tc.cwd)
   678  			}
   679  			if err := os.Chdir(wkdir); err != nil {
   680  				t.Errorf("Could not cd into %v: %v", wkdir, err)
   681  			}
   682  			got, err := FindProjectRoot()
   683  			directory := obtainProjectDirectory(t, got, dirName)
   684  			if tc.err == nil {
   685  				if got != filepath.Join(directory, tc.sdkPath) {
   686  					t.Errorf("findProjectRoot found %v as root, but should get %v", got, filepath.Join(directory, tc.sdkPath))
   687  				}
   688  			} else {
   689  				if err == nil {
   690  					t.Errorf("findProjectRoot got %v, want %v", err, tc.err)
   691  				}
   692  			}
   693  		})
   694  	}
   695  }
   696  
   697  func TestFindProjectRootWithoutConfig(t *testing.T) {
   698  	tests := []struct {
   699  		names []string
   700  		err   error
   701  		cwd   string
   702  		name  string
   703  	}{
   704  		{
   705  			names: []string{
   706  				"manifest.yaml",
   707  				filepath.Join("settings", "settings.yaml"),
   708  				filepath.Join("mywebhook", "index.js"),
   709  			},
   710  			err:  nil,
   711  			cwd:  "",
   712  			name: "manifest found and cwd is .",
   713  		},
   714  		{
   715  			names: []string{
   716  				filepath.Join("settings", "settings.yaml"),
   717  				filepath.Join("verticals", "foo.yaml"),
   718  			},
   719  			err:  errors.New("manifest.yaml not found"),
   720  			cwd:  "",
   721  			name: "manifest not found",
   722  		},
   723  		{
   724  			names: []string{
   725  				filepath.Join("settings", "settings.yaml"),
   726  				filepath.Join("verticals", "foo.yaml"),
   727  				"manifest.yaml",
   728  			},
   729  			cwd:  "settings",
   730  			err:  nil,
   731  			name: "manifest found and cwd is settings",
   732  		},
   733  	}
   734  	for _, tc := range tests {
   735  		t.Run(tc.name, func(t *testing.T) {
   736  			dirName, err := ioutil.TempDir(testutils.TestTmpDir, "actions-sdk-cli-project-folder")
   737  			if err != nil {
   738  				t.Fatalf("Can't create temporary directory under %q: %v", testutils.TestTmpDir, err)
   739  			}
   740  			defer os.RemoveAll(dirName)
   741  			for _, f := range tc.names {
   742  				if err := os.MkdirAll(filepath.Join(dirName, filepath.Dir(f)), 0777); err != nil {
   743  					t.Errorf("Can't create a directory %v, got %v", filepath.Join(dirName, filepath.Dir(f)), err)
   744  				}
   745  				if err := ioutil.WriteFile(filepath.Join(dirName, f), []byte("hello"), 0666); err != nil {
   746  					t.Fatalf("Can't write a file under %q: %v", dirName, err)
   747  				}
   748  			}
   749  			wkdir := dirName
   750  			if tc.cwd != "" {
   751  				wkdir = filepath.Join(wkdir, tc.cwd)
   752  			}
   753  			if err := os.Chdir(wkdir); err != nil {
   754  				t.Errorf("Could not cd into %v: %v", wkdir, err)
   755  			}
   756  			got, err := FindProjectRoot()
   757  			directory := obtainProjectDirectory(t, got, dirName)
   758  			if tc.err == nil {
   759  				if got != directory {
   760  					t.Errorf("findProjectRoot found %v as root, but should get %v", directory, got)
   761  				}
   762  			} else {
   763  				if err == nil {
   764  					t.Errorf("findProjectRoot got %v, want %v", err, tc.err)
   765  				}
   766  			}
   767  		})
   768  	}
   769  }