github.com/haalcala/mattermost-server-change-repo/v5@v5.33.2/model/manifest_test.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package model
     5  
     6  import (
     7  	"encoding/json"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  	"gopkg.in/yaml.v2"
    17  )
    18  
    19  func TestIsValid(t *testing.T) {
    20  	testCases := []struct {
    21  		Title       string
    22  		manifest    *Manifest
    23  		ExpectError bool
    24  	}{
    25  		{"Invalid Id", &Manifest{Id: "some id", Name: "some name"}, true},
    26  		{"Invalid Name", &Manifest{Id: "com.company.test", Name: "  "}, true},
    27  		{"Invalid homePageURL", &Manifest{Id: "com.company.test", Name: "some name", HomepageURL: "some url"}, true},
    28  		{"Invalid supportURL", &Manifest{Id: "com.company.test", Name: "some name", SupportURL: "some url"}, true},
    29  		{"Invalid ReleaseNotesURL", &Manifest{Id: "com.company.test", Name: "some name", ReleaseNotesURL: "some url"}, true},
    30  		{"Invalid version", &Manifest{Id: "com.company.test", Name: "some name", HomepageURL: "http://someurl.com", SupportURL: "http://someotherurl.com", Version: "version"}, true},
    31  		{"Invalid min version", &Manifest{Id: "com.company.test", Name: "some name", HomepageURL: "http://someurl.com", SupportURL: "http://someotherurl.com", Version: "5.10.0", MinServerVersion: "version"}, true},
    32  		{"SettingSchema error", &Manifest{Id: "com.company.test", Name: "some name", HomepageURL: "http://someurl.com", SupportURL: "http://someotherurl.com", Version: "5.10.0", MinServerVersion: "5.10.8", SettingsSchema: &PluginSettingsSchema{
    33  			Settings: []*PluginSetting{{Type: "Invalid"}},
    34  		}}, true},
    35  		{"Minimal valid manifest", &Manifest{Id: "com.company.test", Name: "some name"}, false},
    36  		{"Happy case", &Manifest{
    37  			Id:               "com.company.test",
    38  			Name:             "thename",
    39  			Description:      "thedescription",
    40  			HomepageURL:      "http://someurl.com",
    41  			SupportURL:       "http://someotherurl.com",
    42  			ReleaseNotesURL:  "http://someotherurl.com/releases/v0.0.1",
    43  			Version:          "0.0.1",
    44  			MinServerVersion: "5.6.0",
    45  			Server: &ManifestServer{
    46  				Executable: "theexecutable",
    47  			},
    48  			Webapp: &ManifestWebapp{
    49  				BundlePath: "thebundlepath",
    50  			},
    51  			SettingsSchema: &PluginSettingsSchema{
    52  				Header: "theheadertext",
    53  				Footer: "thefootertext",
    54  				Settings: []*PluginSetting{
    55  					{
    56  						Key:         "thesetting",
    57  						DisplayName: "thedisplayname",
    58  						Type:        "dropdown",
    59  						HelpText:    "thehelptext",
    60  						Options: []*PluginOption{
    61  							{
    62  								DisplayName: "theoptiondisplayname",
    63  								Value:       "thevalue",
    64  							},
    65  						},
    66  						Default: "thedefault",
    67  					},
    68  				},
    69  			},
    70  		}, false},
    71  	}
    72  
    73  	for _, tc := range testCases {
    74  		t.Run(tc.Title, func(t *testing.T) {
    75  			err := tc.manifest.IsValid()
    76  			if tc.ExpectError {
    77  				assert.Error(t, err)
    78  			} else {
    79  				assert.NoError(t, err)
    80  			}
    81  		})
    82  	}
    83  }
    84  
    85  func TestIsValidSettingsSchema(t *testing.T) {
    86  	testCases := []struct {
    87  		Title          string
    88  		settingsSchema *PluginSettingsSchema
    89  		ExpectError    bool
    90  	}{
    91  		{"Invalid Setting", &PluginSettingsSchema{Settings: []*PluginSetting{{Type: "invalid"}}}, true},
    92  		{"Happy case", &PluginSettingsSchema{Settings: []*PluginSetting{{Type: "text"}}}, false},
    93  	}
    94  
    95  	for _, tc := range testCases {
    96  		t.Run(tc.Title, func(t *testing.T) {
    97  			err := tc.settingsSchema.isValid()
    98  			if tc.ExpectError {
    99  				assert.Error(t, err)
   100  			} else {
   101  				assert.NoError(t, err)
   102  			}
   103  		})
   104  	}
   105  }
   106  
   107  func TestSettingIsValid(t *testing.T) {
   108  	for name, test := range map[string]struct {
   109  		Setting     PluginSetting
   110  		ExpectError bool
   111  	}{
   112  		"Invalid setting type": {
   113  			PluginSetting{Type: "invalid"},
   114  			true,
   115  		},
   116  		"RegenerateHelpText error": {
   117  			PluginSetting{Type: "text", RegenerateHelpText: "some text"},
   118  			true,
   119  		},
   120  		"Placeholder error": {
   121  			PluginSetting{Type: "bool", Placeholder: "some text"},
   122  			true,
   123  		},
   124  		"Nil Options": {
   125  			PluginSetting{Type: "bool"},
   126  			false,
   127  		},
   128  		"Options error": {
   129  			PluginSetting{Type: "generated", Options: []*PluginOption{}},
   130  			true,
   131  		},
   132  		"Options displayName error": {
   133  			PluginSetting{
   134  				Type: "radio",
   135  				Options: []*PluginOption{{
   136  					Value: "some value",
   137  				}},
   138  			},
   139  			true,
   140  		},
   141  		"Options value error": {
   142  			PluginSetting{
   143  				Type: "radio",
   144  				Options: []*PluginOption{{
   145  					DisplayName: "some name",
   146  				}},
   147  			},
   148  			true,
   149  		},
   150  		"Happy case": {
   151  			PluginSetting{
   152  				Type: "radio",
   153  				Options: []*PluginOption{{
   154  					DisplayName: "Name",
   155  					Value:       "value",
   156  				}},
   157  			},
   158  			false,
   159  		},
   160  		"Valid number setting": {
   161  			PluginSetting{
   162  				Type:    "number",
   163  				Default: 10,
   164  			},
   165  			false,
   166  		},
   167  		"Placeholder is disallowed for bool settings": {
   168  			PluginSetting{
   169  				Type:        "bool",
   170  				Placeholder: "some Text",
   171  			},
   172  			true,
   173  		},
   174  		"Placeholder is allowed for text settings": {
   175  			PluginSetting{
   176  				Type:        "text",
   177  				Placeholder: "some Text",
   178  			},
   179  			false,
   180  		},
   181  		"Placeholder is allowed for long text settings": {
   182  			PluginSetting{
   183  				Type:        "longtext",
   184  				Placeholder: "some Text",
   185  			},
   186  			false,
   187  		},
   188  	} {
   189  		t.Run(name, func(t *testing.T) {
   190  			err := test.Setting.isValid()
   191  			if test.ExpectError {
   192  				assert.Error(t, err)
   193  			} else {
   194  				assert.NoError(t, err)
   195  			}
   196  		})
   197  	}
   198  }
   199  
   200  func TestConvertTypeToPluginSettingType(t *testing.T) {
   201  	testCases := []struct {
   202  		Title               string
   203  		Type                string
   204  		ExpectedSettingType PluginSettingType
   205  		ExpectError         bool
   206  	}{
   207  		{"bool", "bool", Bool, false},
   208  		{"dropdown", "dropdown", Dropdown, false},
   209  		{"generated", "generated", Generated, false},
   210  		{"radio", "radio", Radio, false},
   211  		{"text", "text", Text, false},
   212  		{"longtext", "longtext", LongText, false},
   213  		{"username", "username", Username, false},
   214  		{"custom", "custom", Custom, false},
   215  		{"invalid", "invalid", Bool, true},
   216  	}
   217  
   218  	for _, tc := range testCases {
   219  		t.Run(tc.Title, func(t *testing.T) {
   220  			settingType, err := convertTypeToPluginSettingType(tc.Type)
   221  			if !tc.ExpectError {
   222  				assert.Equal(t, settingType, tc.ExpectedSettingType)
   223  			} else {
   224  				assert.Error(t, err)
   225  			}
   226  		})
   227  	}
   228  }
   229  
   230  func TestFindManifest(t *testing.T) {
   231  	for _, tc := range []struct {
   232  		Filename       string
   233  		Contents       string
   234  		ExpectError    bool
   235  		ExpectNotExist bool
   236  	}{
   237  		{"foo", "bar", true, true},
   238  		{"plugin.json", "bar", true, false},
   239  		{"plugin.json", `{"id": "foo"}`, false, false},
   240  		{"plugin.json", `{"id": "FOO"}`, false, false},
   241  		{"plugin.yaml", `id: foo`, false, false},
   242  		{"plugin.yaml", "bar", true, false},
   243  		{"plugin.yml", `id: foo`, false, false},
   244  		{"plugin.yml", `id: FOO`, false, false},
   245  		{"plugin.yml", "bar", true, false},
   246  	} {
   247  		dir, err := ioutil.TempDir("", "mm-plugin-test")
   248  		require.NoError(t, err)
   249  		defer os.RemoveAll(dir)
   250  
   251  		path := filepath.Join(dir, tc.Filename)
   252  		f, err := os.Create(path)
   253  		require.NoError(t, err)
   254  		_, err = f.WriteString(tc.Contents)
   255  		f.Close()
   256  		require.NoError(t, err)
   257  
   258  		m, mpath, err := FindManifest(dir)
   259  		assert.True(t, (err != nil) == tc.ExpectError, tc.Filename)
   260  		assert.True(t, (err != nil && os.IsNotExist(err)) == tc.ExpectNotExist, tc.Filename)
   261  		if !tc.ExpectNotExist {
   262  			assert.Equal(t, path, mpath, tc.Filename)
   263  		} else {
   264  			assert.Empty(t, mpath, tc.Filename)
   265  		}
   266  		if !tc.ExpectError {
   267  			require.NotNil(t, m, tc.Filename)
   268  			assert.NotEmpty(t, m.Id, tc.Filename)
   269  			assert.Equal(t, strings.ToLower(m.Id), m.Id)
   270  		}
   271  	}
   272  }
   273  
   274  func TestManifestUnmarshal(t *testing.T) {
   275  	expected := Manifest{
   276  		Id:               "theid",
   277  		HomepageURL:      "https://example.com",
   278  		SupportURL:       "https://example.com/support",
   279  		IconPath:         "assets/icon.svg",
   280  		MinServerVersion: "5.6.0",
   281  		Server: &ManifestServer{
   282  			Executable: "theexecutable",
   283  			Executables: &ManifestExecutables{
   284  				LinuxAmd64:   "theexecutable-linux-amd64",
   285  				DarwinAmd64:  "theexecutable-darwin-amd64",
   286  				WindowsAmd64: "theexecutable-windows-amd64",
   287  			},
   288  			AllExecutables: map[string]string{
   289  				"linux-amd64":   "theexecutable-linux-amd64",
   290  				"darwin-amd64":  "theexecutable-darwin-amd64",
   291  				"windows-amd64": "theexecutable-windows-amd64",
   292  				"linux-arm64":   "theexecutable-linux-arm64",
   293  			},
   294  		},
   295  		Webapp: &ManifestWebapp{
   296  			BundlePath: "thebundlepath",
   297  		},
   298  		SettingsSchema: &PluginSettingsSchema{
   299  			Header: "theheadertext",
   300  			Footer: "thefootertext",
   301  			Settings: []*PluginSetting{
   302  				{
   303  					Key:                "thesetting",
   304  					DisplayName:        "thedisplayname",
   305  					Type:               "dropdown",
   306  					HelpText:           "thehelptext",
   307  					RegenerateHelpText: "theregeneratehelptext",
   308  					Placeholder:        "theplaceholder",
   309  					Options: []*PluginOption{
   310  						{
   311  							DisplayName: "theoptiondisplayname",
   312  							Value:       "thevalue",
   313  						},
   314  					},
   315  					Default: "thedefault",
   316  				},
   317  			},
   318  		},
   319  	}
   320  
   321  	t.Run("yaml", func(t *testing.T) {
   322  		var yamlResult Manifest
   323  		require.NoError(t, yaml.Unmarshal([]byte(`
   324  id: theid
   325  homepage_url: https://example.com
   326  support_url: https://example.com/support
   327  icon_path: assets/icon.svg
   328  min_server_version: 5.6.0
   329  server:
   330      executable: theexecutable
   331      executables:
   332            linux-amd64: theexecutable-linux-amd64
   333            darwin-amd64: theexecutable-darwin-amd64
   334            windows-amd64: theexecutable-windows-amd64
   335            linux-arm64: theexecutable-linux-arm64
   336  webapp:
   337      bundle_path: thebundlepath
   338  settings_schema:
   339      header: theheadertext
   340      footer: thefootertext
   341      settings:
   342          - key: thesetting
   343            display_name: thedisplayname
   344            type: dropdown
   345            help_text: thehelptext
   346            regenerate_help_text: theregeneratehelptext
   347            placeholder: theplaceholder
   348            options:
   349                - display_name: theoptiondisplayname
   350                  value: thevalue
   351            default: thedefault
   352  `), &yamlResult))
   353  		assert.Equal(t, expected, yamlResult)
   354  	})
   355  
   356  	t.Run("json", func(t *testing.T) {
   357  		var jsonResult Manifest
   358  		require.NoError(t, json.Unmarshal([]byte(`{
   359  	"id": "theid",
   360  	"homepage_url": "https://example.com",
   361  	"support_url": "https://example.com/support",
   362  	"icon_path": "assets/icon.svg",
   363  	"min_server_version": "5.6.0",
   364  	"server": {
   365  		"executable": "theexecutable",
   366  		"executables": {
   367  			"linux-amd64": "theexecutable-linux-amd64",
   368  			"darwin-amd64": "theexecutable-darwin-amd64",
   369  			"windows-amd64": "theexecutable-windows-amd64",
   370  			"linux-arm64": "theexecutable-linux-arm64"
   371  		}
   372  	},
   373  	"webapp": {
   374  		"bundle_path": "thebundlepath"
   375  	},
   376      "settings_schema": {
   377          "header": "theheadertext",
   378          "footer": "thefootertext",
   379          "settings": [
   380  			{
   381  				"key": "thesetting",
   382  				"display_name": "thedisplayname",
   383  				"type": "dropdown",
   384  				"help_text": "thehelptext",
   385  				"regenerate_help_text": "theregeneratehelptext",
   386  				"placeholder": "theplaceholder",
   387  				"options": [
   388  					{
   389  						"display_name": "theoptiondisplayname",
   390  						"value": "thevalue"
   391  					}
   392  				],
   393  				"default": "thedefault"
   394  			}
   395  		]
   396      }
   397  	}`), &jsonResult))
   398  		assert.Equal(t, expected, jsonResult)
   399  	})
   400  }
   401  
   402  func TestFindManifest_FileErrors(t *testing.T) {
   403  	for _, tc := range []string{"plugin.yaml", "plugin.json"} {
   404  		dir, err := ioutil.TempDir("", "mm-plugin-test")
   405  		require.NoError(t, err)
   406  		defer os.RemoveAll(dir)
   407  
   408  		path := filepath.Join(dir, tc)
   409  		require.NoError(t, os.Mkdir(path, 0700))
   410  
   411  		m, mpath, err := FindManifest(dir)
   412  		assert.Nil(t, m)
   413  		assert.Equal(t, path, mpath)
   414  		assert.Error(t, err, tc)
   415  		assert.False(t, os.IsNotExist(err), tc)
   416  	}
   417  }
   418  
   419  func TestFindManifest_FolderPermission(t *testing.T) {
   420  	if os.Geteuid() == 0 {
   421  		t.Skip("skipping test while running as root: can't effectively remove permissions")
   422  	}
   423  
   424  	for _, tc := range []string{"plugin.yaml", "plugin.json"} {
   425  		dir, err := ioutil.TempDir("", "mm-plugin-test")
   426  		require.NoError(t, err)
   427  		defer os.RemoveAll(dir)
   428  
   429  		path := filepath.Join(dir, tc)
   430  		require.NoError(t, os.Mkdir(path, 0700))
   431  
   432  		// User does not have permission in the plugin folder
   433  		err = os.Chmod(dir, 0066)
   434  		require.NoError(t, err)
   435  
   436  		m, mpath, err := FindManifest(dir)
   437  		assert.Nil(t, m)
   438  		assert.Equal(t, "", mpath)
   439  		assert.Error(t, err, tc)
   440  		assert.False(t, os.IsNotExist(err), tc)
   441  	}
   442  }
   443  
   444  func TestManifestJson(t *testing.T) {
   445  	manifest := &Manifest{
   446  		Id: "theid",
   447  		Server: &ManifestServer{
   448  			Executable: "theexecutable",
   449  			Executables: &ManifestExecutables{
   450  				LinuxAmd64:   "linux-amd64/path/to/executable",
   451  				DarwinAmd64:  "darwin-amd64/path/to/executable",
   452  				WindowsAmd64: "windows-amd64/path/to/executable",
   453  			},
   454  			AllExecutables: map[string]string{
   455  				"linux-amd64":   "linux-amd64/path/to/executable",
   456  				"darwin-amd64":  "darwin-amd64/path/to/executable",
   457  				"windows-amd64": "windows-amd64/path/to/executable",
   458  				"linux-arm64":   "linux-arm64/path/to/executable",
   459  			},
   460  		},
   461  		Webapp: &ManifestWebapp{
   462  			BundlePath: "thebundlepath",
   463  		},
   464  		SettingsSchema: &PluginSettingsSchema{
   465  			Header: "theheadertext",
   466  			Footer: "thefootertext",
   467  			Settings: []*PluginSetting{
   468  				{
   469  					Key:                "thesetting",
   470  					DisplayName:        "thedisplayname",
   471  					Type:               "dropdown",
   472  					HelpText:           "thehelptext",
   473  					RegenerateHelpText: "theregeneratehelptext",
   474  					Placeholder:        "theplaceholder",
   475  					Options: []*PluginOption{
   476  						{
   477  							DisplayName: "theoptiondisplayname",
   478  							Value:       "thevalue",
   479  						},
   480  					},
   481  					Default: "thedefault",
   482  				},
   483  			},
   484  		},
   485  	}
   486  
   487  	json := manifest.ToJson()
   488  	newManifest := ManifestFromJson(strings.NewReader(json))
   489  	assert.Equal(t, newManifest, manifest)
   490  	assert.Equal(t, newManifest.ToJson(), json)
   491  	assert.Equal(t, ManifestFromJson(strings.NewReader("junk")), (*Manifest)(nil))
   492  
   493  	manifestList := []*Manifest{manifest}
   494  	json = ManifestListToJson(manifestList)
   495  	newManifestList := ManifestListFromJson(strings.NewReader(json))
   496  	assert.Equal(t, newManifestList, manifestList)
   497  	assert.Equal(t, ManifestListToJson(newManifestList), json)
   498  }
   499  
   500  func TestManifestHasClient(t *testing.T) {
   501  	manifest := &Manifest{
   502  		Id: "theid",
   503  		Server: &ManifestServer{
   504  			Executable: "theexecutable",
   505  		},
   506  		Webapp: &ManifestWebapp{
   507  			BundlePath: "thebundlepath",
   508  		},
   509  	}
   510  
   511  	assert.True(t, manifest.HasClient())
   512  
   513  	manifest.Webapp = nil
   514  	assert.False(t, manifest.HasClient())
   515  }
   516  
   517  func TestManifestClientManifest(t *testing.T) {
   518  	manifest := &Manifest{
   519  		Id:               "theid",
   520  		Name:             "thename",
   521  		Description:      "thedescription",
   522  		Version:          "0.0.1",
   523  		MinServerVersion: "5.6.0",
   524  		Server: &ManifestServer{
   525  			Executable: "theexecutable",
   526  		},
   527  		Webapp: &ManifestWebapp{
   528  			BundlePath: "thebundlepath",
   529  			BundleHash: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
   530  		},
   531  		SettingsSchema: &PluginSettingsSchema{
   532  			Header: "theheadertext",
   533  			Footer: "thefootertext",
   534  			Settings: []*PluginSetting{
   535  				{
   536  					Key:                "thesetting",
   537  					DisplayName:        "thedisplayname",
   538  					Type:               "dropdown",
   539  					HelpText:           "thehelptext",
   540  					RegenerateHelpText: "theregeneratehelptext",
   541  					Placeholder:        "theplaceholder",
   542  					Options: []*PluginOption{
   543  						{
   544  							DisplayName: "theoptiondisplayname",
   545  							Value:       "thevalue",
   546  						},
   547  					},
   548  					Default: "thedefault",
   549  				},
   550  			},
   551  		},
   552  	}
   553  
   554  	sanitized := manifest.ClientManifest()
   555  
   556  	assert.Equal(t, manifest.Id, sanitized.Id)
   557  	assert.Equal(t, manifest.Version, sanitized.Version)
   558  	assert.Equal(t, manifest.MinServerVersion, sanitized.MinServerVersion)
   559  	assert.Equal(t, "/static/theid/theid_000102030405060708090a0b0c0d0e0f_bundle.js", sanitized.Webapp.BundlePath)
   560  	assert.Equal(t, manifest.Webapp.BundleHash, sanitized.Webapp.BundleHash)
   561  	assert.Equal(t, manifest.SettingsSchema, sanitized.SettingsSchema)
   562  	assert.Empty(t, sanitized.Name)
   563  	assert.Empty(t, sanitized.Description)
   564  	assert.Empty(t, sanitized.Server)
   565  
   566  	assert.NotEmpty(t, manifest.Id)
   567  	assert.NotEmpty(t, manifest.Version)
   568  	assert.NotEmpty(t, manifest.MinServerVersion)
   569  	assert.NotEmpty(t, manifest.Webapp)
   570  	assert.NotEmpty(t, manifest.Name)
   571  	assert.NotEmpty(t, manifest.Description)
   572  	assert.NotEmpty(t, manifest.Server)
   573  	assert.NotEmpty(t, manifest.SettingsSchema)
   574  }
   575  
   576  func TestManifestGetExecutableForRuntime(t *testing.T) {
   577  	testCases := []struct {
   578  		Description        string
   579  		Manifest           *Manifest
   580  		GoOs               string
   581  		GoArch             string
   582  		ExpectedExecutable string
   583  	}{
   584  		{
   585  			"no server",
   586  			&Manifest{},
   587  			"linux",
   588  			"amd64",
   589  			"",
   590  		},
   591  		{
   592  			"no executable",
   593  			&Manifest{
   594  				Server: &ManifestServer{},
   595  			},
   596  			"linux",
   597  			"amd64",
   598  			"",
   599  		},
   600  		{
   601  			"single executable",
   602  			&Manifest{
   603  				Server: &ManifestServer{
   604  					Executable: "path/to/executable",
   605  				},
   606  			},
   607  			"linux",
   608  			"amd64",
   609  			"path/to/executable",
   610  		},
   611  		{
   612  			"single executable, different runtime",
   613  			&Manifest{
   614  				Server: &ManifestServer{
   615  					Executable: "path/to/executable",
   616  				},
   617  			},
   618  			"darwin",
   619  			"amd64",
   620  			"path/to/executable",
   621  		},
   622  		{
   623  			"multiple legacy executables ignored",
   624  			&Manifest{
   625  				Server: &ManifestServer{
   626  					Executables: &ManifestExecutables{
   627  						LinuxAmd64:   "linux-amd64/path/to/executable",
   628  						DarwinAmd64:  "darwin-amd64/path/to/executable",
   629  						WindowsAmd64: "windows-amd64/path/to/executable",
   630  					},
   631  				},
   632  			},
   633  			"linux",
   634  			"amd64",
   635  			"",
   636  		},
   637  		{
   638  			"multiple executables, no match",
   639  			&Manifest{
   640  				Server: &ManifestServer{
   641  					AllExecutables: map[string]string{
   642  						"linux-amd64":   "linux-amd64/path/to/executable",
   643  						"darwin-amd64":  "darwin-amd64/path/to/executable",
   644  						"windows-amd64": "windows-amd64/path/to/executable",
   645  						"linux-arm64":   "linux-arm64/path/to/executable",
   646  					},
   647  				},
   648  			},
   649  			"other",
   650  			"amd64",
   651  			"",
   652  		},
   653  		{
   654  			"multiple executables, linux-amd64 match",
   655  			&Manifest{
   656  				Server: &ManifestServer{
   657  					AllExecutables: map[string]string{
   658  						"linux-amd64":   "linux-amd64/path/to/executable",
   659  						"darwin-amd64":  "darwin-amd64/path/to/executable",
   660  						"windows-amd64": "windows-amd64/path/to/executable",
   661  						"linux-arm64":   "linux-arm64/path/to/executable",
   662  					},
   663  				},
   664  			},
   665  			"linux",
   666  			"amd64",
   667  			"linux-amd64/path/to/executable",
   668  		},
   669  		{
   670  			"multiple executables, linux-amd64 match, single executable ignored",
   671  			&Manifest{
   672  				Server: &ManifestServer{
   673  					AllExecutables: map[string]string{
   674  						"linux-amd64":   "linux-amd64/path/to/executable",
   675  						"darwin-amd64":  "darwin-amd64/path/to/executable",
   676  						"windows-amd64": "windows-amd64/path/to/executable",
   677  						"linux-arm64":   "linux-arm64/path/to/executable",
   678  					},
   679  					Executable: "path/to/executable",
   680  				},
   681  			},
   682  			"linux",
   683  			"amd64",
   684  			"linux-amd64/path/to/executable",
   685  		},
   686  		{
   687  			"multiple executables, darwin-amd64 match",
   688  			&Manifest{
   689  				Server: &ManifestServer{
   690  					AllExecutables: map[string]string{
   691  						"linux-amd64":   "linux-amd64/path/to/executable",
   692  						"darwin-amd64":  "darwin-amd64/path/to/executable",
   693  						"windows-amd64": "windows-amd64/path/to/executable",
   694  						"linux-arm64":   "linux-arm64/path/to/executable",
   695  					},
   696  				},
   697  			},
   698  			"darwin",
   699  			"amd64",
   700  			"darwin-amd64/path/to/executable",
   701  		},
   702  		{
   703  			"multiple executables, windows-amd64 match",
   704  			&Manifest{
   705  				Server: &ManifestServer{
   706  					AllExecutables: map[string]string{
   707  						"linux-amd64":   "linux-amd64/path/to/executable",
   708  						"darwin-amd64":  "darwin-amd64/path/to/executable",
   709  						"windows-amd64": "windows-amd64/path/to/executable",
   710  						"linux-arm64":   "linux-arm64/path/to/executable",
   711  					},
   712  				},
   713  			},
   714  			"windows",
   715  			"amd64",
   716  			"windows-amd64/path/to/executable",
   717  		},
   718  		{
   719  			"multiple executables, no match, single executable fallback",
   720  			&Manifest{
   721  				Server: &ManifestServer{
   722  					AllExecutables: map[string]string{
   723  						"linux-amd64":   "linux-amd64/path/to/executable",
   724  						"darwin-amd64":  "darwin-amd64/path/to/executable",
   725  						"windows-amd64": "windows-amd64/path/to/executable",
   726  						"linux-arm64":   "linux-arm64/path/to/executable",
   727  					},
   728  					Executable: "path/to/executable",
   729  				},
   730  			},
   731  			"other",
   732  			"amd64",
   733  			"path/to/executable",
   734  		},
   735  		{
   736  			"deprecated backend field, ignored since server present",
   737  			&Manifest{
   738  				Server: &ManifestServer{
   739  					AllExecutables: map[string]string{
   740  						"linux-amd64":   "linux-amd64/path/to/executable",
   741  						"darwin-amd64":  "darwin-amd64/path/to/executable",
   742  						"windows-amd64": "windows-amd64/path/to/executable",
   743  						"linux-arm64":   "linux-arm64/path/to/executable",
   744  					},
   745  				},
   746  				Backend: &ManifestServer{
   747  					AllExecutables: map[string]string{
   748  						"linux-amd64":   "linux-amd64/path/to/executable",
   749  						"darwin-amd64":  "darwin-amd64/path/to/executable",
   750  						"windows-amd64": "windows-amd64/path/to/executable",
   751  					},
   752  				},
   753  			},
   754  			"linux",
   755  			"amd64",
   756  			"linux-amd64/path/to/executable",
   757  		},
   758  		{
   759  			"deprecated backend field used, since no server present",
   760  			&Manifest{
   761  				Backend: &ManifestServer{
   762  					AllExecutables: map[string]string{
   763  						"linux-amd64":   "linux-amd64/path/to/executable",
   764  						"darwin-amd64":  "darwin-amd64/path/to/executable",
   765  						"windows-amd64": "windows-amd64/path/to/executable",
   766  					},
   767  				},
   768  			},
   769  			"linux",
   770  			"amd64",
   771  			"linux-amd64/path/to/executable",
   772  		},
   773  		{
   774  			"all executables, linux-arm64",
   775  			&Manifest{
   776  				Backend: &ManifestServer{
   777  					Executables: &ManifestExecutables{
   778  						LinuxAmd64:   "linux-amd64/path/to/executable",
   779  						DarwinAmd64:  "darwin-amd64/path/to/executable",
   780  						WindowsAmd64: "windows-amd64/path/to/executable",
   781  					},
   782  					AllExecutables: map[string]string{
   783  						"linux-amd64":   "linux-amd64/path/to/executable",
   784  						"darwin-amd64":  "darwin-amd64/path/to/executable",
   785  						"windows-amd64": "windows-amd64/path/to/executable",
   786  						"linux-arm64":   "linux-arm64/path/to/executable",
   787  					},
   788  				},
   789  			},
   790  			"linux",
   791  			"arm64",
   792  			"linux-arm64/path/to/executable",
   793  		},
   794  		{
   795  			"all executables, linux-amd64, legacy executables ignored",
   796  			&Manifest{
   797  				Backend: &ManifestServer{
   798  					Executables: &ManifestExecutables{
   799  						LinuxAmd64:   "linux-amd64/ignored/path/to/executable",
   800  						DarwinAmd64:  "darwin-amd64/ignored/path/to/executable",
   801  						WindowsAmd64: "windows-amd64/ignored/path/to/executable",
   802  					},
   803  					AllExecutables: map[string]string{
   804  						"linux-amd64":   "linux-amd64/path/to/executable",
   805  						"darwin-amd64":  "darwin-amd64/path/to/executable",
   806  						"windows-amd64": "windows-amd64/path/to/executable",
   807  						"linux-arm64":   "linux-arm64/path/to/executable",
   808  					},
   809  				},
   810  			},
   811  			"linux",
   812  			"amd64",
   813  			"linux-amd64/path/to/executable",
   814  		},
   815  	}
   816  
   817  	for _, testCase := range testCases {
   818  		t.Run(testCase.Description, func(t *testing.T) {
   819  			assert.Equal(
   820  				t,
   821  				testCase.ExpectedExecutable,
   822  				testCase.Manifest.GetExecutableForRuntime(testCase.GoOs, testCase.GoArch),
   823  			)
   824  		})
   825  	}
   826  }
   827  
   828  func TestManifestHasServer(t *testing.T) {
   829  	testCases := []struct {
   830  		Description string
   831  		Manifest    *Manifest
   832  		Expected    bool
   833  	}{
   834  		{
   835  			"no server",
   836  			&Manifest{},
   837  			false,
   838  		},
   839  		{
   840  			"no executable, but server still considered present",
   841  			&Manifest{
   842  				Server: &ManifestServer{},
   843  			},
   844  			true,
   845  		},
   846  		{
   847  			"single executable",
   848  			&Manifest{
   849  				Server: &ManifestServer{
   850  					Executable: "path/to/executable",
   851  				},
   852  			},
   853  			true,
   854  		},
   855  		{
   856  			"multiple executables",
   857  			&Manifest{
   858  				Server: &ManifestServer{
   859  					Executables: &ManifestExecutables{
   860  						LinuxAmd64:   "linux-amd64/path/to/executable",
   861  						DarwinAmd64:  "darwin-amd64/path/to/executable",
   862  						WindowsAmd64: "windows-amd64/path/to/executable",
   863  					},
   864  				},
   865  			},
   866  			true,
   867  		},
   868  		{
   869  			"single executable defined via deprecated backend",
   870  			&Manifest{
   871  				Backend: &ManifestServer{
   872  					Executable: "path/to/executable",
   873  				},
   874  			},
   875  			true,
   876  		},
   877  		{
   878  			"multiple executables defined via deprecated backend",
   879  			&Manifest{
   880  				Backend: &ManifestServer{
   881  					Executables: &ManifestExecutables{
   882  						LinuxAmd64:   "linux-amd64/path/to/executable",
   883  						DarwinAmd64:  "darwin-amd64/path/to/executable",
   884  						WindowsAmd64: "windows-amd64/path/to/executable",
   885  					},
   886  				},
   887  			},
   888  			true,
   889  		},
   890  	}
   891  
   892  	for _, testCase := range testCases {
   893  		t.Run(testCase.Description, func(t *testing.T) {
   894  			assert.Equal(t, testCase.Expected, testCase.Manifest.HasServer())
   895  		})
   896  	}
   897  }
   898  
   899  func TestManifestHasWebapp(t *testing.T) {
   900  	testCases := []struct {
   901  		Description string
   902  		Manifest    *Manifest
   903  		Expected    bool
   904  	}{
   905  		{
   906  			"no webapp",
   907  			&Manifest{},
   908  			false,
   909  		},
   910  		{
   911  			"no bundle path, but webapp still considered present",
   912  			&Manifest{
   913  				Webapp: &ManifestWebapp{},
   914  			},
   915  			true,
   916  		},
   917  		{
   918  			"bundle path defined",
   919  			&Manifest{
   920  				Webapp: &ManifestWebapp{
   921  					BundlePath: "path/to/bundle",
   922  				},
   923  			},
   924  			true,
   925  		},
   926  	}
   927  
   928  	for _, testCase := range testCases {
   929  		t.Run(testCase.Description, func(t *testing.T) {
   930  			assert.Equal(t, testCase.Expected, testCase.Manifest.HasWebapp())
   931  		})
   932  	}
   933  }
   934  
   935  func TestManifestMeetMinServerVersion(t *testing.T) {
   936  	for name, test := range map[string]struct {
   937  		MinServerVersion string
   938  		ServerVersion    string
   939  		ShouldError      bool
   940  		ShouldFulfill    bool
   941  	}{
   942  		"generously fulfilled": {
   943  			MinServerVersion: "5.5.0",
   944  			ServerVersion:    "5.6.0",
   945  			ShouldError:      false,
   946  			ShouldFulfill:    true,
   947  		},
   948  		"exactly fulfilled": {
   949  			MinServerVersion: "5.6.0",
   950  			ServerVersion:    "5.6.0",
   951  			ShouldError:      false,
   952  			ShouldFulfill:    true,
   953  		},
   954  		"not fulfilled": {
   955  			MinServerVersion: "5.6.0",
   956  			ServerVersion:    "5.5.0",
   957  			ShouldError:      false,
   958  			ShouldFulfill:    false,
   959  		},
   960  		"fail to parse MinServerVersion": {
   961  			MinServerVersion: "abc",
   962  			ServerVersion:    "5.5.0",
   963  			ShouldError:      true,
   964  		},
   965  	} {
   966  		t.Run(name, func(t *testing.T) {
   967  			assert := assert.New(t)
   968  
   969  			manifest := Manifest{
   970  				MinServerVersion: test.MinServerVersion,
   971  			}
   972  			fulfilled, err := manifest.MeetMinServerVersion(test.ServerVersion)
   973  
   974  			if test.ShouldError {
   975  				assert.NotNil(err)
   976  				assert.False(fulfilled)
   977  				return
   978  			}
   979  			assert.Nil(err)
   980  			assert.Equal(test.ShouldFulfill, fulfilled)
   981  		})
   982  	}
   983  }