...

Source file src/edge-infra.dev/pkg/edge/edgeadmin/commands/operatorintervention/loaddata/loaddata_test.go

Documentation: edge-infra.dev/pkg/edge/edgeadmin/commands/operatorintervention/loaddata

     1  package loaddata
     2  
     3  import (
     4  	"cmp"
     5  	"context"
     6  	"encoding/json"
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"path/filepath"
    12  	"slices"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/99designs/gqlgen/graphql"
    18  	graphqlTypes "github.com/shurcooL/graphql"
    19  	"github.com/stretchr/testify/require"
    20  
    21  	"edge-infra.dev/pkg/edge/api/graph/model"
    22  	"edge-infra.dev/pkg/edge/api/utils"
    23  	"edge-infra.dev/pkg/edge/edgecli"
    24  	"edge-infra.dev/pkg/edge/edgecli/flagutil"
    25  )
    26  
    27  var defaultConfig = `
    28  {
    29  	"ROLE_MAPPING": {
    30  		"EDGE_BANNER_ADMIN": [
    31  			"ea-read",
    32  			"ea-banner-admin"
    33  		]
    34  	},
    35  	"RULES": {
    36  		"ea-write": [
    37  			"ls",
    38  			"cat"
    39  		],
    40  		"ea-banner-admin": [
    41  			"kubectl"
    42  			]
    43  		}
    44  }
    45  `
    46  
    47  // testing helper type
    48  type helper interface {
    49  	Helper()
    50  }
    51  
    52  func ErrorEqualMsg(msg string) require.ErrorAssertionFunc {
    53  	return func(tt require.TestingT, err error, _ ...interface{}) {
    54  		if help, ok := tt.(helper); ok {
    55  			help.Helper()
    56  		}
    57  
    58  		require.EqualError(tt, err, msg)
    59  	}
    60  }
    61  
    62  // Tests that an error that to consists of several joined-together error messages
    63  // is what we expect, with no respect to ordering.
    64  func ErrorEqualMsgElements(msg string) require.ErrorAssertionFunc {
    65  	return func(tt require.TestingT, err error, _ ...interface{}) {
    66  		if help, ok := tt.(helper); ok {
    67  			help.Helper()
    68  		}
    69  
    70  		expected := strings.Split(msg, "\n")
    71  		actual := strings.Split(err.Error(), "\n")
    72  		for _, errMsg := range expected {
    73  			require.Contains(tt, actual, errMsg)
    74  		}
    75  	}
    76  }
    77  
    78  func TestValidate(t *testing.T) {
    79  	t.Parallel()
    80  
    81  	tests := map[string]struct {
    82  		cfg    config
    83  		expErr require.ErrorAssertionFunc
    84  	}{
    85  		"Valid Full Config": {
    86  			cfg: config{
    87  				RoleMappings: map[string][]string{
    88  					"role1": {
    89  						"priv1",
    90  						"priv2",
    91  					},
    92  					"role2": {
    93  						"priv3",
    94  					},
    95  				},
    96  				Rules: map[string][]string{
    97  					"priv1": {
    98  						"command1",
    99  						"command2",
   100  					},
   101  					"priv2": {
   102  						"command3",
   103  					},
   104  				},
   105  			},
   106  			expErr: require.NoError,
   107  		},
   108  		"Valid Half Config": {
   109  			cfg: config{
   110  				RoleMappings: map[string][]string{
   111  					"role1": {
   112  						"priv1",
   113  					},
   114  				},
   115  			},
   116  			expErr: require.NoError,
   117  		},
   118  		"Invalid Uninitialized Maps": {
   119  			cfg: config{
   120  				RoleMappings: map[string][]string{},
   121  				Rules:        map[string][]string{},
   122  			},
   123  			expErr: ErrorEqualMsg("empty config"),
   124  		},
   125  		"Invalid No Config": {
   126  			cfg:    config{},
   127  			expErr: ErrorEqualMsg("empty config"),
   128  		},
   129  		"Invalid Full Config": {
   130  			cfg: config{
   131  				RoleMappings: map[string][]string{
   132  					"role1": {
   133  						// Privileges must be non-nil
   134  						"",
   135  					},
   136  					// Every role must have at least one privilege
   137  					"role2": {},
   138  				},
   139  				Rules: map[string][]string{
   140  					"priv1": {
   141  						// Commands must be non-nil
   142  						"",
   143  					},
   144  					// Every privilege must have at least one command
   145  					"priv2": {},
   146  				},
   147  			},
   148  			expErr: ErrorEqualMsgElements("role \"role1\" has empty privilege\nrole \"role2\" has no privileges\nprivilege \"priv1\" has empty command\nprivilege \"priv2\" has no commands"),
   149  		},
   150  	}
   151  
   152  	for name, tc := range tests {
   153  		tc := tc
   154  		t.Run(name, func(t *testing.T) {
   155  			t.Parallel()
   156  
   157  			tc.expErr(t, tc.cfg.Validate())
   158  		})
   159  	}
   160  }
   161  
   162  func TestLoadConfig(t *testing.T) {
   163  	t.Parallel()
   164  	tests := map[string]struct {
   165  		testData     string
   166  		wantConfig   config
   167  		requireError require.ErrorAssertionFunc
   168  	}{
   169  		"ValidConfig": {
   170  			testData: `{
   171  				"ROLE_MAPPING": {
   172  					"EDGE_BANNER_ADMIN": [
   173  						"ea-read",
   174  						"ea-banner-admin"
   175  					]
   176  				},
   177  				"RULES": {
   178  					"ea-write": [
   179  						"ls",
   180  						"cat"
   181  					],
   182  					"ea-banner-admin": [
   183  						"kubectl"
   184  					]
   185  				}
   186  			}`,
   187  			wantConfig: config{
   188  				RoleMappings: map[string][]string{
   189  					"EDGE_BANNER_ADMIN": {
   190  						"ea-read",
   191  						"ea-banner-admin",
   192  					},
   193  				},
   194  				Rules: map[string][]string{
   195  					"ea-write": {
   196  						"ls",
   197  						"cat",
   198  					},
   199  					"ea-banner-admin": {
   200  						"kubectl",
   201  					},
   202  				},
   203  			},
   204  			requireError: require.NoError,
   205  		},
   206  		"Duplicate Keys": {
   207  			// Note if duplicate keys exist in the config.json, one set of keys
   208  			// is lost, it is not additive
   209  			testData: `{
   210  				"ROLE_MAPPING": {
   211  					"EDGE_BANNER_ADMIN": [
   212  						"ea-admin"
   213  					],
   214  					"EDGE_ORG_ADMIN": [
   215  						"ea-admin"
   216  					],
   217  					"EDGE_BANNER_ADMIN": [
   218  						"ea-write"
   219  					]
   220  				}
   221  			}`,
   222  			wantConfig: config{
   223  				RoleMappings: map[string][]string{
   224  					"EDGE_BANNER_ADMIN": {
   225  						"ea-write",
   226  					},
   227  					"EDGE_ORG_ADMIN": {
   228  						"ea-admin",
   229  					},
   230  				},
   231  			},
   232  			requireError: require.NoError,
   233  		},
   234  		"InvalidConfig": {
   235  			testData:     `invalid json`,
   236  			wantConfig:   config{},
   237  			requireError: ErrorEqualMsg("invalid character 'i' looking for beginning of value"),
   238  		},
   239  		"Invalid After Valid JSON": {
   240  			testData: `{
   241  				"ROLE_MAPPING": {
   242  					"EDGE_BANNER_ADMIN": [
   243  						"ea-banner-admin"
   244  					]
   245  				},
   246  				"RULES": {
   247  					"ea-banner-admin": [
   248  						"kubectl"
   249  					]
   250  				}
   251  			}INVALIDINPUTOVERHERE`,
   252  			wantConfig:   config{},
   253  			requireError: ErrorEqualMsg("error multiple objects in config file"),
   254  		},
   255  		"Invalid Multiple Valid JSON Objects": {
   256  			testData: `{
   257  				"ROLE_MAPPING": {
   258  					"EDGE_BANNER_ADMIN": [
   259  						"ea-banner-admin"
   260  					]
   261  				}
   262  			},
   263  			{
   264  				"RULES": {
   265  					"ea-banner-admin": [
   266  						"kubectl"
   267  					]
   268  				}
   269  			}`,
   270  			wantConfig:   config{},
   271  			requireError: ErrorEqualMsg("error multiple objects in config file"),
   272  		},
   273  		"Unknown Fields": {
   274  			testData: `{
   275  				"ROLE_MAPPING": {
   276  					"EDGE_BANNER_ADMIN": [
   277  						"ea-read"
   278  					]
   279  				},
   280  				"RULES": {
   281  					"ea-write": [
   282  						"ls"
   283  					]
   284  				},
   285  				"somethingelse": {
   286  					"somethingelse": [
   287  						"somethingelse"
   288  					]
   289  				}
   290  			}`,
   291  			requireError: ErrorEqualMsg("json: unknown field \"somethingelse\""),
   292  		},
   293  		"EmptyConfig": {
   294  			testData:     `{}`,
   295  			requireError: ErrorEqualMsg("error invalid config: empty config"),
   296  		},
   297  		"Empty Fields": {
   298  			testData: `{
   299  				"ROLE_MAPPING": {},
   300  				"RULES": {}
   301  			}`,
   302  			requireError: ErrorEqualMsg("error invalid config: empty config"),
   303  		},
   304  	}
   305  
   306  	for name, tt := range tests {
   307  		tt := tt
   308  		t.Run(name, func(t *testing.T) {
   309  			t.Parallel()
   310  			// Create a temporary directory for testing
   311  			tempDir := t.TempDir()
   312  
   313  			// Create the file path
   314  			filePath := filepath.Join(tempDir, "config.json")
   315  
   316  			// Write test data to the temporary file
   317  			err := os.WriteFile(filePath, []byte(tt.testData), 0644)
   318  			if err != nil {
   319  				t.Fatal(err)
   320  			}
   321  
   322  			// Call the loadConfig function
   323  			conf, err := loadConfig(filePath)
   324  
   325  			// Verify the error
   326  			tt.requireError(t, err)
   327  
   328  			// Verify the loaded configuration
   329  			require.EqualValues(t, tt.wantConfig, conf)
   330  		})
   331  	}
   332  }
   333  
   334  func TestVariables(t *testing.T) {
   335  	t.Parallel()
   336  
   337  	tests := map[string]struct {
   338  		conf         config
   339  		expVariables map[string]interface{}
   340  	}{
   341  		"No Config": {
   342  			conf: config{
   343  				Rules:        nil,
   344  				RoleMappings: nil,
   345  			},
   346  			expVariables: map[string]interface{}{
   347  				// Every value must be initialised (non-nil) even if they are
   348  				// not being applied, otherwise the graphql schema is invalid
   349  				"commands":     []model.OperatorInterventionCommandInput{},
   350  				"privileges":   []model.OperatorInterventionPrivilegeInput{},
   351  				"rules":        []model.UpdateOperatorInterventionRuleInput{},
   352  				"roleMappings": []*model.UpdateOperatorInterventionRoleMappingInput{},
   353  
   354  				// Every type should be skipped
   355  				"skipCommands":     graphqlTypes.Boolean(true),
   356  				"skipPrivileges":   graphqlTypes.Boolean(true),
   357  				"skipRules":        graphqlTypes.Boolean(true),
   358  				"skipRoleMappings": graphqlTypes.Boolean(true),
   359  			},
   360  		},
   361  		"Only Role Mappings": {
   362  			conf: config{
   363  				Rules: nil,
   364  				RoleMappings: map[string][]string{
   365  					"EDGE_ORG_ADMIN": {
   366  						"ea-read", "ea-write",
   367  					},
   368  				},
   369  			},
   370  			expVariables: map[string]interface{}{
   371  				// Every value must be initialised (non-nil) even if they are
   372  				// not being applied, otherwise the graphql schema is invalid
   373  				// Commands and rules should not be added
   374  				"commands": []model.OperatorInterventionCommandInput{},
   375  				"rules":    []model.UpdateOperatorInterventionRuleInput{},
   376  
   377  				// All privileges referenced in roles must be added
   378  				"privileges": []model.OperatorInterventionPrivilegeInput{
   379  					{Name: "ea-read"}, {Name: "ea-write"},
   380  				},
   381  
   382  				"roleMappings": []*model.UpdateOperatorInterventionRoleMappingInput{
   383  					{
   384  						Role: "EDGE_ORG_ADMIN",
   385  						Privileges: []*model.OperatorInterventionPrivilegeInput{
   386  							{Name: "ea-read"},
   387  							{Name: "ea-write"},
   388  						},
   389  					},
   390  				},
   391  
   392  				// commands and rules must be skipped as there is no data to add
   393  				"skipCommands": graphqlTypes.Boolean(true),
   394  				"skipRules":    graphqlTypes.Boolean(true),
   395  
   396  				"skipPrivileges":   graphqlTypes.Boolean(false),
   397  				"skipRoleMappings": graphqlTypes.Boolean(false),
   398  			},
   399  		},
   400  		"Only Rules": {
   401  			conf: config{
   402  				Rules: map[string][]string{
   403  					"ea-admin": {"ls", "systemctl"},
   404  					"ea-read":  {"ls", "cat"},
   405  				},
   406  				RoleMappings: nil,
   407  			},
   408  			expVariables: map[string]interface{}{
   409  				// Every value must be initialised (non-nil) even if they are
   410  				// not being applied, otherwise the graphql schema is invalid
   411  				// Only role mappings should not be added
   412  				"roleMappings": []*model.UpdateOperatorInterventionRoleMappingInput{},
   413  
   414  				// All privileges and commands referenced in rules must be added
   415  				"privileges": []model.OperatorInterventionPrivilegeInput{
   416  					{Name: "ea-admin"}, {Name: "ea-read"},
   417  				},
   418  				"commands": []model.OperatorInterventionCommandInput{
   419  					// Note deduplication occurs
   420  					{Name: "cat"}, {Name: "ls"}, {Name: "systemctl"},
   421  				},
   422  
   423  				"rules": []model.UpdateOperatorInterventionRuleInput{
   424  					{
   425  						Privilege: &model.OperatorInterventionPrivilegeInput{Name: "ea-admin"},
   426  						Commands:  []*model.OperatorInterventionCommandInput{{Name: "ls"}, {Name: "systemctl"}},
   427  					},
   428  					{
   429  						Privilege: &model.OperatorInterventionPrivilegeInput{Name: "ea-read"},
   430  						Commands:  []*model.OperatorInterventionCommandInput{{Name: "ls"}, {Name: "cat"}},
   431  					},
   432  				},
   433  
   434  				// Only role mappings are skipped
   435  				"skipRoleMappings": graphqlTypes.Boolean(true),
   436  
   437  				"skipCommands":   graphqlTypes.Boolean(false),
   438  				"skipPrivileges": graphqlTypes.Boolean(false),
   439  				"skipRules":      graphqlTypes.Boolean(false),
   440  			},
   441  		},
   442  		"Both Rules and Mappings": {
   443  			conf: config{
   444  				Rules: map[string][]string{
   445  					"ea-admin": {"ls", "systemctl"},
   446  					"ea-read":  {"ls", "cat"},
   447  				},
   448  				RoleMappings: map[string][]string{
   449  					"EDGE_ORG_ADMIN": {
   450  						"ea-read", "ea-write",
   451  					},
   452  				},
   453  			},
   454  			expVariables: map[string]interface{}{
   455  				"commands": []model.OperatorInterventionCommandInput{
   456  					// Note no deduplication occurs
   457  					{Name: "cat"}, {Name: "ls"}, {Name: "systemctl"},
   458  				},
   459  				// All privileges referenced in either roles or rules must be
   460  				// included
   461  				"privileges": []model.OperatorInterventionPrivilegeInput{
   462  					// Note deduplication occurs
   463  					{Name: "ea-admin"},
   464  					{Name: "ea-read"},
   465  					{Name: "ea-write"},
   466  				},
   467  
   468  				"rules": []model.UpdateOperatorInterventionRuleInput{
   469  					{
   470  						Privilege: &model.OperatorInterventionPrivilegeInput{Name: "ea-admin"},
   471  						Commands:  []*model.OperatorInterventionCommandInput{{Name: "ls"}, {Name: "systemctl"}},
   472  					},
   473  					{
   474  						Privilege: &model.OperatorInterventionPrivilegeInput{Name: "ea-read"},
   475  						Commands:  []*model.OperatorInterventionCommandInput{{Name: "ls"}, {Name: "cat"}},
   476  					},
   477  				},
   478  				"roleMappings": []*model.UpdateOperatorInterventionRoleMappingInput{
   479  					{
   480  						Role:       "EDGE_ORG_ADMIN",
   481  						Privileges: []*model.OperatorInterventionPrivilegeInput{{Name: "ea-read"}, {Name: "ea-write"}},
   482  					},
   483  				},
   484  
   485  				// Every type should be added
   486  				"skipCommands":     graphqlTypes.Boolean(false),
   487  				"skipPrivileges":   graphqlTypes.Boolean(false),
   488  				"skipRules":        graphqlTypes.Boolean(false),
   489  				"skipRoleMappings": graphqlTypes.Boolean(false),
   490  			},
   491  		},
   492  		"Duplicate Rules": {
   493  			// Notice when the commands within a single rule is duplicated, the
   494  			// commands mutation doesn't include duplicate entries, but the
   495  			// rules mutation does
   496  			conf: config{
   497  				Rules: map[string][]string{
   498  					"ea-admin": {"ls", "ls"},
   499  				},
   500  				RoleMappings: nil,
   501  			},
   502  			expVariables: map[string]interface{}{
   503  				// Every value must be initialised (non-nil) even if they are
   504  				// not being applied, otherwise the graphql schema is invalid
   505  				// Only role mappings should not be added
   506  				"roleMappings": []*model.UpdateOperatorInterventionRoleMappingInput{},
   507  
   508  				// All privileges and commands referenced in rules must be added
   509  				"privileges": []model.OperatorInterventionPrivilegeInput{
   510  					{Name: "ea-admin"},
   511  				},
   512  				"commands": []model.OperatorInterventionCommandInput{
   513  					// Note deduplication occurs here
   514  					{Name: "ls"},
   515  				},
   516  
   517  				"rules": []model.UpdateOperatorInterventionRuleInput{
   518  					{
   519  						Privilege: &model.OperatorInterventionPrivilegeInput{Name: "ea-admin"},
   520  						// Note deduplication does not occur here
   521  						Commands: []*model.OperatorInterventionCommandInput{{Name: "ls"}, {Name: "ls"}},
   522  					},
   523  				},
   524  
   525  				// Only role mappings are skipped
   526  				"skipRoleMappings": graphqlTypes.Boolean(true),
   527  
   528  				"skipCommands":   graphqlTypes.Boolean(false),
   529  				"skipPrivileges": graphqlTypes.Boolean(false),
   530  				"skipRules":      graphqlTypes.Boolean(false),
   531  			},
   532  		},
   533  		"No privileges in role mapping": {
   534  			conf: config{
   535  				RoleMappings: map[string][]string{
   536  					"EDGE_ORG_ADMIN": {},
   537  				},
   538  			},
   539  			expVariables: map[string]interface{}{
   540  				"commands": []model.OperatorInterventionCommandInput{},
   541  				"rules":    []model.UpdateOperatorInterventionRuleInput{},
   542  
   543  				"privileges": []model.OperatorInterventionPrivilegeInput{},
   544  				"roleMappings": []*model.UpdateOperatorInterventionRoleMappingInput{
   545  					{
   546  						Role:       "EDGE_ORG_ADMIN",
   547  						Privileges: []*model.OperatorInterventionPrivilegeInput{},
   548  					},
   549  				},
   550  
   551  				"skipCommands":   graphqlTypes.Boolean(true),
   552  				"skipRules":      graphqlTypes.Boolean(true),
   553  				"skipPrivileges": graphqlTypes.Boolean(true),
   554  
   555  				"skipRoleMappings": graphqlTypes.Boolean(false),
   556  			},
   557  		},
   558  		"No commands in rule": {
   559  			conf: config{
   560  				Rules: map[string][]string{
   561  					"ea-admin": {},
   562  				},
   563  			},
   564  			expVariables: map[string]interface{}{
   565  				"roleMappings": []*model.UpdateOperatorInterventionRoleMappingInput{},
   566  
   567  				"commands": []model.OperatorInterventionCommandInput{},
   568  
   569  				"privileges": []model.OperatorInterventionPrivilegeInput{
   570  					{Name: "ea-admin"},
   571  				},
   572  				"rules": []model.UpdateOperatorInterventionRuleInput{
   573  					{
   574  						Privilege: &model.OperatorInterventionPrivilegeInput{Name: "ea-admin"},
   575  						Commands:  []*model.OperatorInterventionCommandInput{},
   576  					},
   577  				},
   578  
   579  				"skipRoleMappings": graphqlTypes.Boolean(true),
   580  				"skipCommands":     graphqlTypes.Boolean(true),
   581  
   582  				"skipRules":      graphqlTypes.Boolean(false),
   583  				"skipPrivileges": graphqlTypes.Boolean(false),
   584  			},
   585  		},
   586  	}
   587  
   588  	for name, tc := range tests {
   589  		tc := tc
   590  		t.Run(name, func(t *testing.T) {
   591  			t.Parallel()
   592  
   593  			vars := createVariables(tc.conf)
   594  
   595  			// Sort the rules for deterministic test output
   596  			slices.SortFunc(vars["rules"].([]model.UpdateOperatorInterventionRuleInput), func(a, b model.UpdateOperatorInterventionRuleInput) int {
   597  				return cmp.Compare(a.Privilege.Name, b.Privilege.Name)
   598  			})
   599  
   600  			require.Equal(t, tc.expVariables, vars)
   601  		})
   602  	}
   603  }
   604  
   605  func TestMissingFlagFile(t *testing.T) {
   606  	testConfig := edgecli.Config{}
   607  	cmd := NewCmd(&testConfig)
   608  
   609  	require.ErrorContains(t, cmd.Command().Exec(context.Background(), []string{}), "Flag 'file' is required")
   610  
   611  	require.NoError(t, flagutil.SetFlag(cmd.Rags, flagutil.LoadData, "test-cluster-0"))
   612  }
   613  
   614  func TestLoadData(t *testing.T) {
   615  	t.Parallel()
   616  
   617  	server := utils.NewMockHTTPTestServer().AddAllowedContentType("application/json").DefaultNotFound()
   618  	t.Cleanup(server.Server.Close)
   619  
   620  	tests := map[string]struct {
   621  		data map[string]interface{} // data api will return
   622  
   623  		expError require.ErrorAssertionFunc
   624  	}{
   625  		"Error Response": {
   626  			data: map[string]interface{}{
   627  				"updateOperatorInterventionRoleMappings": model.UpdateOperatorInterventionRoleMappingResponse{
   628  					Errors: []*model.OperatorInterventionErrorResponse{
   629  						{Type: model.OperatorInterventionErrorTypeUnknownRole},
   630  					},
   631  				},
   632  				"createOperatorInterventionPrivileges": model.CreateOperatorInterventionPrivilegeResponse{
   633  					Errors: []*model.OperatorInterventionErrorResponse{
   634  						{Type: model.OperatorInterventionErrorTypeUnknownPrivilege},
   635  					},
   636  				},
   637  			},
   638  			expError: ErrorEqualMsg("error during mutation"),
   639  		},
   640  		"No Error Response": {
   641  			data: map[string]interface{}{
   642  				"updateOperatorInterventionRoleMappings": model.UpdateOperatorInterventionRoleMappingResponse{},
   643  				"createOperatorInterventionPrivileges":   model.CreateOperatorInterventionPrivilegeResponse{},
   644  			},
   645  			expError: require.NoError,
   646  		},
   647  	}
   648  
   649  	for name, tc := range tests {
   650  		name := name
   651  		tc := tc
   652  		t.Run(name, func(t *testing.T) {
   653  			t.Parallel()
   654  
   655  			// Setup
   656  			dir := t.TempDir()
   657  			filePath := filepath.Join(dir, "config.json")
   658  			require.NoError(t, os.WriteFile(
   659  				filePath,
   660  				[]byte(defaultConfig),
   661  				0644,
   662  			))
   663  
   664  			server.Any(name, apiRequestCallback(tc.data), func(_ http.ResponseWriter, r *http.Request) bool {
   665  				return strings.HasPrefix(r.URL.String(), "/"+url.PathEscape(name))
   666  			})
   667  
   668  			// Set a dummy token in a fake banner context so that we bypass the
   669  			// ValidateConnectionFlags check which would otherwise do a login
   670  			// mutation to the api server, something which is not supported by
   671  			// the fake servert
   672  			future := time.Now().Add(time.Hour * 24)
   673  			testConfig := edgecli.Config{
   674  				CurrentBannerContext: "fakeBanner",
   675  				BannerContexts: map[string]*edgecli.BannerContext{
   676  					"fakeBanner": {
   677  						TokenTime: future.Format(time.RFC3339),
   678  						Token:     "fakeToken",
   679  						Endpoint:  server.Server.URL + "/" + url.PathEscape(name),
   680  					},
   681  				},
   682  			}
   683  
   684  			cmd := NewCmd(&testConfig)
   685  			cmd.Command() // Required to initialise cmd.Rags
   686  
   687  			require.NoError(t, flagutil.SetFlag(cmd.Rags, flagutil.LoadData, filePath))
   688  
   689  			// Test
   690  			err := cmd.Command().Exec(context.Background(), []string{})
   691  			tc.expError(t, err)
   692  		})
   693  	}
   694  }
   695  
   696  func apiRequestCallback(data interface{}) func(w http.ResponseWriter, r *http.Request) {
   697  	return func(w http.ResponseWriter, r *http.Request) {
   698  		body, err := io.ReadAll(r.Body)
   699  		if err != nil {
   700  			utils.WriteBadResponse(w, nil)
   701  			return
   702  		}
   703  
   704  		mutationCorrect := assertMutationOrder(body)
   705  		if !mutationCorrect {
   706  			utils.WriteBadResponse(w, nil)
   707  			return
   708  		}
   709  
   710  		data, err := wrapAsGraphqlResponse(data)
   711  		if err != nil {
   712  			utils.WriteBadResponse(w, nil)
   713  			return
   714  		}
   715  		utils.WriteOkResponse(w, data)
   716  	}
   717  }
   718  
   719  // assertMutationOrder asserts the mutations are ordered as we expect them to be.
   720  // commands and privileges must be added before rules and role mappings to
   721  // ensure that any new privs/commands have been committed to the DB before they
   722  // are referenced
   723  func assertMutationOrder(body []byte) bool {
   724  	return all(
   725  		assertOnce(string(body), "createOperatorInterventionCommands"),
   726  		assertOnce(string(body), "createOperatorInterventionPrivileges"),
   727  		assertOnce(string(body), "updateOperatorInterventionRules"),
   728  		assertOnce(string(body), "updateOperatorInterventionRoleMappings"),
   729  
   730  		assertInOrder(string(body), "createOperatorInterventionPrivileges", "updateOperatorInterventionRoleMappings"),
   731  		assertInOrder(string(body), "createOperatorInterventionPrivileges", "updateOperatorInterventionRules"),
   732  		assertInOrder(string(body), "createOperatorInterventionCommands", "updateOperatorInterventionRules"),
   733  	)
   734  }
   735  
   736  // all returns true when every parameter is true otherwise returns false
   737  func all(vals ...bool) bool {
   738  	for _, val := range vals {
   739  		if !val {
   740  			return false
   741  		}
   742  	}
   743  	return true
   744  }
   745  
   746  // assertOnce asserts the value only appears in body once
   747  func assertOnce(body string, value string) bool {
   748  	return strings.Count(body, value) == 1
   749  }
   750  
   751  // assertInOrder asserts that first appears before second within the data
   752  func assertInOrder(data string, first string, second string) bool {
   753  	firstIdx := strings.Index(data, first)
   754  	if firstIdx == -1 {
   755  		return false
   756  	}
   757  	secondIdx := strings.Index(data, second)
   758  	if secondIdx == -1 {
   759  		return false
   760  	}
   761  
   762  	return second > first
   763  }
   764  
   765  func wrapAsGraphqlResponse(v interface{}) ([]byte, error) {
   766  	res, err := json.Marshal(v)
   767  	if err != nil {
   768  		return nil, err
   769  	}
   770  	resp := graphql.Response{Data: res}
   771  	return json.Marshal(resp)
   772  }
   773  
   774  func TestGenerateAllOutput(t *testing.T) {
   775  	t.Parallel()
   776  
   777  	var invalidCommand = "invalidCommand"
   778  	var invalidRole = "invalidRole"
   779  	var invalidPriv = "invalidPriv"
   780  
   781  	tests := map[string]struct {
   782  		variables map[string]interface{}
   783  		mutation  oiLoadDataMutation
   784  
   785  		exp            string
   786  		errorAssertion require.ErrorAssertionFunc
   787  	}{
   788  		"No Mutations, No Errors": {
   789  			variables: map[string]interface{}{
   790  				"skipCommands":     graphqlTypes.Boolean(true),
   791  				"skipPrivileges":   graphqlTypes.Boolean(true),
   792  				"skipRules":        graphqlTypes.Boolean(true),
   793  				"skipRoleMappings": graphqlTypes.Boolean(true),
   794  			},
   795  			mutation: oiLoadDataMutation{
   796  				CreateOperatorInterventionCommands: struct {
   797  					model.CreateOperatorInterventionCommandResponse
   798  				}{
   799  					CreateOperatorInterventionCommandResponse: model.CreateOperatorInterventionCommandResponse{
   800  						Errors: nil,
   801  					},
   802  				},
   803  				CreateOperatorInterventionPrivileges: struct {
   804  					model.CreateOperatorInterventionPrivilegeResponse
   805  				}{
   806  					CreateOperatorInterventionPrivilegeResponse: model.CreateOperatorInterventionPrivilegeResponse{
   807  						Errors: nil,
   808  					},
   809  				},
   810  				UpdateOperatorInterventionRules: struct {
   811  					model.UpdateOperatorInterventionRuleResponse
   812  				}{
   813  					UpdateOperatorInterventionRuleResponse: model.UpdateOperatorInterventionRuleResponse{
   814  						Errors: nil,
   815  					},
   816  				},
   817  				UpdateOperatorInterventionRoleMappings: struct {
   818  					model.UpdateOperatorInterventionRoleMappingResponse
   819  				}{
   820  					UpdateOperatorInterventionRoleMappingResponse: model.UpdateOperatorInterventionRoleMappingResponse{
   821  						Errors: nil,
   822  					},
   823  				},
   824  			},
   825  			exp:            "",
   826  			errorAssertion: require.NoError,
   827  		},
   828  		"No Mutations, All Errors": {
   829  			variables: map[string]interface{}{
   830  				"skipCommands":     graphqlTypes.Boolean(true),
   831  				"skipPrivileges":   graphqlTypes.Boolean(true),
   832  				"skipRules":        graphqlTypes.Boolean(true),
   833  				"skipRoleMappings": graphqlTypes.Boolean(true),
   834  			},
   835  			mutation: oiLoadDataMutation{
   836  				CreateOperatorInterventionCommands: struct {
   837  					model.CreateOperatorInterventionCommandResponse
   838  				}{
   839  					CreateOperatorInterventionCommandResponse: model.CreateOperatorInterventionCommandResponse{
   840  						Errors: []*model.OperatorInterventionErrorResponse{
   841  							{Type: model.OperatorInterventionErrorTypeUnknownPrivilege},
   842  						},
   843  					},
   844  				},
   845  				CreateOperatorInterventionPrivileges: struct {
   846  					model.CreateOperatorInterventionPrivilegeResponse
   847  				}{
   848  					CreateOperatorInterventionPrivilegeResponse: model.CreateOperatorInterventionPrivilegeResponse{
   849  						Errors: []*model.OperatorInterventionErrorResponse{
   850  							{Type: model.OperatorInterventionErrorTypeUnknownPrivilege},
   851  						},
   852  					},
   853  				},
   854  				UpdateOperatorInterventionRules: struct {
   855  					model.UpdateOperatorInterventionRuleResponse
   856  				}{
   857  					UpdateOperatorInterventionRuleResponse: model.UpdateOperatorInterventionRuleResponse{
   858  						Errors: []*model.OperatorInterventionErrorResponse{
   859  							{Type: model.OperatorInterventionErrorTypeUnknownPrivilege},
   860  						},
   861  					},
   862  				},
   863  				UpdateOperatorInterventionRoleMappings: struct {
   864  					model.UpdateOperatorInterventionRoleMappingResponse
   865  				}{
   866  					UpdateOperatorInterventionRoleMappingResponse: model.UpdateOperatorInterventionRoleMappingResponse{
   867  						Errors: []*model.OperatorInterventionErrorResponse{
   868  							{Type: model.OperatorInterventionErrorTypeUnknownPrivilege},
   869  						},
   870  					},
   871  				},
   872  			},
   873  			exp:            "",
   874  			errorAssertion: require.NoError,
   875  		},
   876  		"All Mutations, No Errors": {
   877  			variables: map[string]interface{}{
   878  				"skipCommands":     graphqlTypes.Boolean(false),
   879  				"skipPrivileges":   graphqlTypes.Boolean(false),
   880  				"skipRules":        graphqlTypes.Boolean(false),
   881  				"skipRoleMappings": graphqlTypes.Boolean(false),
   882  			},
   883  			mutation: oiLoadDataMutation{
   884  				CreateOperatorInterventionCommands: struct {
   885  					model.CreateOperatorInterventionCommandResponse
   886  				}{
   887  					CreateOperatorInterventionCommandResponse: model.CreateOperatorInterventionCommandResponse{
   888  						Errors: nil,
   889  					},
   890  				},
   891  				CreateOperatorInterventionPrivileges: struct {
   892  					model.CreateOperatorInterventionPrivilegeResponse
   893  				}{
   894  					CreateOperatorInterventionPrivilegeResponse: model.CreateOperatorInterventionPrivilegeResponse{
   895  						Errors: nil,
   896  					},
   897  				},
   898  				UpdateOperatorInterventionRules: struct {
   899  					model.UpdateOperatorInterventionRuleResponse
   900  				}{
   901  					UpdateOperatorInterventionRuleResponse: model.UpdateOperatorInterventionRuleResponse{
   902  						Errors: nil,
   903  					},
   904  				},
   905  				UpdateOperatorInterventionRoleMappings: struct {
   906  					model.UpdateOperatorInterventionRoleMappingResponse
   907  				}{
   908  					UpdateOperatorInterventionRoleMappingResponse: model.UpdateOperatorInterventionRoleMappingResponse{
   909  						Errors: nil,
   910  					},
   911  				},
   912  			},
   913  			exp:            "\nOI Commands applied\n\n\nOI Privileges applied\n\n\nOI Rules applied\n\n\nOI Role Mappings applied\n",
   914  			errorAssertion: require.NoError,
   915  		},
   916  		"All Mutations, All Errors": {
   917  			variables: map[string]interface{}{
   918  				"skipCommands":     graphqlTypes.Boolean(false),
   919  				"skipPrivileges":   graphqlTypes.Boolean(false),
   920  				"skipRules":        graphqlTypes.Boolean(false),
   921  				"skipRoleMappings": graphqlTypes.Boolean(false),
   922  			},
   923  			mutation: oiLoadDataMutation{
   924  				CreateOperatorInterventionCommands: struct {
   925  					model.CreateOperatorInterventionCommandResponse
   926  				}{
   927  					CreateOperatorInterventionCommandResponse: model.CreateOperatorInterventionCommandResponse{
   928  						Errors: []*model.OperatorInterventionErrorResponse{
   929  							{Type: model.OperatorInterventionErrorTypeUnknownPrivilege},
   930  						},
   931  					},
   932  				},
   933  				CreateOperatorInterventionPrivileges: struct {
   934  					model.CreateOperatorInterventionPrivilegeResponse
   935  				}{
   936  					CreateOperatorInterventionPrivilegeResponse: model.CreateOperatorInterventionPrivilegeResponse{
   937  						Errors: []*model.OperatorInterventionErrorResponse{
   938  							{Type: model.OperatorInterventionErrorTypeUnknownPrivilege},
   939  						},
   940  					},
   941  				},
   942  				UpdateOperatorInterventionRules: struct {
   943  					model.UpdateOperatorInterventionRuleResponse
   944  				}{
   945  					UpdateOperatorInterventionRuleResponse: model.UpdateOperatorInterventionRuleResponse{
   946  						Errors: []*model.OperatorInterventionErrorResponse{
   947  							{Type: model.OperatorInterventionErrorTypeUnknownPrivilege, Privilege: &invalidPriv},
   948  						},
   949  					},
   950  				},
   951  				UpdateOperatorInterventionRoleMappings: struct {
   952  					model.UpdateOperatorInterventionRoleMappingResponse
   953  				}{
   954  					UpdateOperatorInterventionRoleMappingResponse: model.UpdateOperatorInterventionRoleMappingResponse{
   955  						Errors: []*model.OperatorInterventionErrorResponse{
   956  							{Type: model.OperatorInterventionErrorTypeUnknownRole, Role: &invalidRole},
   957  						},
   958  					},
   959  				},
   960  			},
   961  			exp:            "\nErrors occurred when applying OI Commands: \n\tError: UNKNOWN_PRIVILEGE\n\nOI Commands not applied.\n\n\nErrors occurred when applying OI Privileges: \n\tError: UNKNOWN_PRIVILEGE\n\nOI Privileges not applied.\n\n\nErrors occurred when applying OI Rules: \n\tError: UNKNOWN_PRIVILEGE. Details: Privilege: \"invalidPriv\"\n\nOI Rules not applied.\n\n\nErrors occurred when applying OI Role Mappings: \n\tError: UNKNOWN_ROLE. Details: Role: \"invalidRole\"\n\nOI Role Mappings not applied.\n",
   962  			errorAssertion: ErrorEqualMsg("error during mutation"),
   963  		},
   964  		"All Mutations, Some Errors": {
   965  			variables: map[string]interface{}{
   966  				"skipCommands":     graphqlTypes.Boolean(false),
   967  				"skipPrivileges":   graphqlTypes.Boolean(false),
   968  				"skipRules":        graphqlTypes.Boolean(false),
   969  				"skipRoleMappings": graphqlTypes.Boolean(false),
   970  			},
   971  			mutation: oiLoadDataMutation{
   972  				CreateOperatorInterventionCommands: struct {
   973  					model.CreateOperatorInterventionCommandResponse
   974  				}{
   975  					CreateOperatorInterventionCommandResponse: model.CreateOperatorInterventionCommandResponse{
   976  						Errors: nil,
   977  					},
   978  				},
   979  				CreateOperatorInterventionPrivileges: struct {
   980  					model.CreateOperatorInterventionPrivilegeResponse
   981  				}{
   982  					CreateOperatorInterventionPrivilegeResponse: model.CreateOperatorInterventionPrivilegeResponse{
   983  						Errors: []*model.OperatorInterventionErrorResponse{
   984  							{Type: model.OperatorInterventionErrorTypeUnknownPrivilege},
   985  						},
   986  					},
   987  				},
   988  				UpdateOperatorInterventionRules: struct {
   989  					model.UpdateOperatorInterventionRuleResponse
   990  				}{
   991  					UpdateOperatorInterventionRuleResponse: model.UpdateOperatorInterventionRuleResponse{
   992  						Errors: nil,
   993  					},
   994  				},
   995  				UpdateOperatorInterventionRoleMappings: struct {
   996  					model.UpdateOperatorInterventionRoleMappingResponse
   997  				}{
   998  					UpdateOperatorInterventionRoleMappingResponse: model.UpdateOperatorInterventionRoleMappingResponse{
   999  						Errors: []*model.OperatorInterventionErrorResponse{
  1000  							{Type: model.OperatorInterventionErrorTypeUnknownRole, Role: &invalidRole},
  1001  						},
  1002  					},
  1003  				},
  1004  			},
  1005  
  1006  			exp:            "\nOI Commands applied\n\n\nErrors occurred when applying OI Privileges: \n\tError: UNKNOWN_PRIVILEGE\n\nOI Privileges not applied.\n\n\nOI Rules applied\n\n\nErrors occurred when applying OI Role Mappings: \n\tError: UNKNOWN_ROLE. Details: Role: \"invalidRole\"\n\nOI Role Mappings not applied.\n",
  1007  			errorAssertion: ErrorEqualMsg("error during mutation"),
  1008  		},
  1009  		"Some Mutations, No Errors": {
  1010  			variables: map[string]interface{}{
  1011  				"skipCommands":     graphqlTypes.Boolean(false),
  1012  				"skipPrivileges":   graphqlTypes.Boolean(true),
  1013  				"skipRules":        graphqlTypes.Boolean(false),
  1014  				"skipRoleMappings": graphqlTypes.Boolean(true),
  1015  			},
  1016  			mutation: oiLoadDataMutation{
  1017  				CreateOperatorInterventionCommands: struct {
  1018  					model.CreateOperatorInterventionCommandResponse
  1019  				}{
  1020  					CreateOperatorInterventionCommandResponse: model.CreateOperatorInterventionCommandResponse{
  1021  						Errors: nil,
  1022  					},
  1023  				},
  1024  				CreateOperatorInterventionPrivileges: struct {
  1025  					model.CreateOperatorInterventionPrivilegeResponse
  1026  				}{
  1027  					CreateOperatorInterventionPrivilegeResponse: model.CreateOperatorInterventionPrivilegeResponse{
  1028  						Errors: nil,
  1029  					},
  1030  				},
  1031  				UpdateOperatorInterventionRules: struct {
  1032  					model.UpdateOperatorInterventionRuleResponse
  1033  				}{
  1034  					UpdateOperatorInterventionRuleResponse: model.UpdateOperatorInterventionRuleResponse{
  1035  						Errors: nil,
  1036  					},
  1037  				},
  1038  				UpdateOperatorInterventionRoleMappings: struct {
  1039  					model.UpdateOperatorInterventionRoleMappingResponse
  1040  				}{
  1041  					UpdateOperatorInterventionRoleMappingResponse: model.UpdateOperatorInterventionRoleMappingResponse{
  1042  						Errors: nil,
  1043  					},
  1044  				},
  1045  			},
  1046  
  1047  			exp:            "\nOI Commands applied\n\n\nOI Rules applied\n",
  1048  			errorAssertion: require.NoError,
  1049  		},
  1050  		"Some Mutations, All Errors": {
  1051  			variables: map[string]interface{}{
  1052  				"skipCommands":     graphqlTypes.Boolean(true),
  1053  				"skipPrivileges":   graphqlTypes.Boolean(false),
  1054  				"skipRules":        graphqlTypes.Boolean(false),
  1055  				"skipRoleMappings": graphqlTypes.Boolean(true),
  1056  			},
  1057  			mutation: oiLoadDataMutation{
  1058  				CreateOperatorInterventionCommands: struct {
  1059  					model.CreateOperatorInterventionCommandResponse
  1060  				}{
  1061  					CreateOperatorInterventionCommandResponse: model.CreateOperatorInterventionCommandResponse{
  1062  						Errors: []*model.OperatorInterventionErrorResponse{
  1063  							{Type: model.OperatorInterventionErrorTypeUnknownCommand},
  1064  						},
  1065  					},
  1066  				},
  1067  				CreateOperatorInterventionPrivileges: struct {
  1068  					model.CreateOperatorInterventionPrivilegeResponse
  1069  				}{
  1070  					CreateOperatorInterventionPrivilegeResponse: model.CreateOperatorInterventionPrivilegeResponse{
  1071  						Errors: []*model.OperatorInterventionErrorResponse{
  1072  							{Type: model.OperatorInterventionErrorTypeUnknownPrivilege},
  1073  						},
  1074  					},
  1075  				},
  1076  				UpdateOperatorInterventionRules: struct {
  1077  					model.UpdateOperatorInterventionRuleResponse
  1078  				}{
  1079  					UpdateOperatorInterventionRuleResponse: model.UpdateOperatorInterventionRuleResponse{
  1080  						Errors: []*model.OperatorInterventionErrorResponse{
  1081  							{Type: model.OperatorInterventionErrorTypeUnknownRule, Privilege: &invalidPriv, Command: &invalidCommand},
  1082  						},
  1083  					},
  1084  				},
  1085  				UpdateOperatorInterventionRoleMappings: struct {
  1086  					model.UpdateOperatorInterventionRoleMappingResponse
  1087  				}{
  1088  					UpdateOperatorInterventionRoleMappingResponse: model.UpdateOperatorInterventionRoleMappingResponse{
  1089  						Errors: []*model.OperatorInterventionErrorResponse{
  1090  							{Type: model.OperatorInterventionErrorTypeUnknownRole, Role: &invalidRole},
  1091  						},
  1092  					},
  1093  				},
  1094  			},
  1095  
  1096  			exp:            "\nErrors occurred when applying OI Privileges: \n\tError: UNKNOWN_PRIVILEGE\n\nOI Privileges not applied.\n\n\nErrors occurred when applying OI Rules: \n\tError: UNKNOWN_RULE. Details: Privilege: \"invalidPriv\", Command: \"invalidCommand\"\n\nOI Rules not applied.\n",
  1097  			errorAssertion: ErrorEqualMsg("error during mutation"),
  1098  		},
  1099  		"Some Mutations, Some Errors": {
  1100  			variables: map[string]interface{}{
  1101  				"skipCommands":     graphqlTypes.Boolean(false),
  1102  				"skipPrivileges":   graphqlTypes.Boolean(false),
  1103  				"skipRules":        graphqlTypes.Boolean(true),
  1104  				"skipRoleMappings": graphqlTypes.Boolean(false),
  1105  			},
  1106  			mutation: oiLoadDataMutation{
  1107  				CreateOperatorInterventionCommands: struct {
  1108  					model.CreateOperatorInterventionCommandResponse
  1109  				}{
  1110  					CreateOperatorInterventionCommandResponse: model.CreateOperatorInterventionCommandResponse{
  1111  						Errors: []*model.OperatorInterventionErrorResponse{
  1112  							{Type: model.OperatorInterventionErrorTypeUnknownPrivilege},
  1113  						},
  1114  					},
  1115  				},
  1116  				CreateOperatorInterventionPrivileges: struct {
  1117  					model.CreateOperatorInterventionPrivilegeResponse
  1118  				}{
  1119  					CreateOperatorInterventionPrivilegeResponse: model.CreateOperatorInterventionPrivilegeResponse{
  1120  						Errors: nil,
  1121  					},
  1122  				},
  1123  				UpdateOperatorInterventionRules: struct {
  1124  					model.UpdateOperatorInterventionRuleResponse
  1125  				}{
  1126  					UpdateOperatorInterventionRuleResponse: model.UpdateOperatorInterventionRuleResponse{
  1127  						Errors: nil,
  1128  					},
  1129  				},
  1130  				UpdateOperatorInterventionRoleMappings: struct {
  1131  					model.UpdateOperatorInterventionRoleMappingResponse
  1132  				}{
  1133  					UpdateOperatorInterventionRoleMappingResponse: model.UpdateOperatorInterventionRoleMappingResponse{
  1134  						Errors: []*model.OperatorInterventionErrorResponse{
  1135  							{Type: model.OperatorInterventionErrorTypeUnknownRole, Role: &invalidRole},
  1136  						},
  1137  					},
  1138  				},
  1139  			},
  1140  
  1141  			exp:            "\nErrors occurred when applying OI Commands: \n\tError: UNKNOWN_PRIVILEGE\n\nOI Commands not applied.\n\n\nOI Privileges applied\n\n\nErrors occurred when applying OI Role Mappings: \n\tError: UNKNOWN_ROLE. Details: Role: \"invalidRole\"\n\nOI Role Mappings not applied.\n",
  1142  			errorAssertion: ErrorEqualMsg("error during mutation"),
  1143  		},
  1144  	}
  1145  
  1146  	for name, tc := range tests {
  1147  		tc := tc
  1148  		t.Run(name, func(t *testing.T) {
  1149  			t.Parallel()
  1150  
  1151  			out, err := generateAllOutput(tc.variables, tc.mutation)
  1152  			tc.errorAssertion(t, err)
  1153  
  1154  			require.Equal(t, tc.exp, out)
  1155  		})
  1156  	}
  1157  }
  1158  

View as plain text