github.com/mattermost/mattermost-server/server/v8@v8.0.0-20230610055354-a6d1d38b273d/config/diff_test.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package config
     5  
     6  import (
     7  	"testing"
     8  
     9  	"github.com/mattermost/mattermost-server/server/public/model"
    10  
    11  	"github.com/stretchr/testify/require"
    12  )
    13  
    14  func defaultConfigGen() *model.Config {
    15  	cfg := &model.Config{}
    16  	cfg.SetDefaults()
    17  	return cfg
    18  }
    19  
    20  func BenchmarkDiff(b *testing.B) {
    21  	b.Run("equal empty", func(b *testing.B) {
    22  		baseCfg := &model.Config{}
    23  		actualCfg := &model.Config{}
    24  		b.ResetTimer()
    25  		for i := 0; i < b.N; i++ {
    26  			_, _ = Diff(baseCfg, actualCfg)
    27  		}
    28  	})
    29  
    30  	b.Run("equal with defaults", func(b *testing.B) {
    31  		baseCfg := defaultConfigGen()
    32  		actualCfg := defaultConfigGen()
    33  		b.ResetTimer()
    34  		for i := 0; i < b.N; i++ {
    35  			_, _ = Diff(baseCfg, actualCfg)
    36  		}
    37  	})
    38  
    39  	b.Run("actual empty", func(b *testing.B) {
    40  		baseCfg := defaultConfigGen()
    41  		actualCfg := &model.Config{}
    42  		b.ResetTimer()
    43  		for i := 0; i < b.N; i++ {
    44  			_, _ = Diff(baseCfg, actualCfg)
    45  		}
    46  	})
    47  
    48  	b.Run("base empty", func(b *testing.B) {
    49  		baseCfg := &model.Config{}
    50  		actualCfg := defaultConfigGen()
    51  		b.ResetTimer()
    52  		for i := 0; i < b.N; i++ {
    53  			_, _ = Diff(baseCfg, actualCfg)
    54  		}
    55  	})
    56  
    57  	b.Run("some diffs", func(b *testing.B) {
    58  		baseCfg := defaultConfigGen()
    59  		actualCfg := defaultConfigGen()
    60  		baseCfg.ServiceSettings.SiteURL = model.NewString("http://localhost")
    61  		baseCfg.ServiceSettings.ReadTimeout = model.NewInt(300)
    62  		baseCfg.SqlSettings.QueryTimeout = model.NewInt(0)
    63  		actualCfg.PluginSettings.EnableUploads = nil
    64  		actualCfg.TeamSettings.MaxChannelsPerTeam = model.NewInt64(100000)
    65  		actualCfg.FeatureFlags = nil
    66  		actualCfg.SqlSettings.DataSourceReplicas = []string{
    67  			"ds0",
    68  			"ds1",
    69  			"ds2",
    70  		}
    71  		b.ResetTimer()
    72  		for i := 0; i < b.N; i++ {
    73  			_, _ = Diff(baseCfg, actualCfg)
    74  		}
    75  	})
    76  }
    77  
    78  func TestDiffSanitized(t *testing.T) {
    79  	tcs := []struct {
    80  		name   string
    81  		base   *model.Config
    82  		actual *model.Config
    83  		diffs  ConfigDiffs
    84  		err    string
    85  	}{
    86  		{
    87  			"nil",
    88  			nil,
    89  			nil,
    90  			nil,
    91  			"input configs should not be nil",
    92  		},
    93  		{
    94  			"empty",
    95  			&model.Config{},
    96  			&model.Config{},
    97  			nil,
    98  			"",
    99  		},
   100  		{
   101  			"defaults",
   102  			defaultConfigGen(),
   103  			defaultConfigGen(),
   104  			nil,
   105  			"",
   106  		},
   107  		{
   108  			"default base, actual empty",
   109  			defaultConfigGen(),
   110  			&model.Config{},
   111  			ConfigDiffs{
   112  				{
   113  					Path: "",
   114  					BaseVal: func() model.Config {
   115  						cfg := defaultConfigGen()
   116  						cfg.Sanitize()
   117  						return *cfg
   118  					}(),
   119  					ActualVal: model.Config{},
   120  				},
   121  			},
   122  			"",
   123  		},
   124  		{
   125  			"empty base, actual default",
   126  			&model.Config{},
   127  			defaultConfigGen(),
   128  			ConfigDiffs{
   129  				{
   130  					Path:    "",
   131  					BaseVal: model.Config{},
   132  					ActualVal: func() model.Config {
   133  						cfg := defaultConfigGen()
   134  						cfg.Sanitize()
   135  						return *cfg
   136  					}(),
   137  				},
   138  			},
   139  			"",
   140  		},
   141  		{
   142  			"sensitive LdapSettings.BindPassword",
   143  			func() *model.Config {
   144  				cfg := defaultConfigGen()
   145  				cfg.LdapSettings.BindPassword = model.NewString("base")
   146  				return cfg
   147  			}(),
   148  			func() *model.Config {
   149  				cfg := defaultConfigGen()
   150  				cfg.LdapSettings.BindPassword = model.NewString("actual")
   151  				return cfg
   152  			}(),
   153  			ConfigDiffs{
   154  				{
   155  					Path:      "LdapSettings.BindPassword",
   156  					BaseVal:   model.FakeSetting,
   157  					ActualVal: model.FakeSetting,
   158  				},
   159  			},
   160  			"",
   161  		},
   162  		{
   163  			"sensitive FileSettings.PublicLinkSalt",
   164  			func() *model.Config {
   165  				cfg := defaultConfigGen()
   166  				cfg.FileSettings.PublicLinkSalt = model.NewString("base")
   167  				return cfg
   168  			}(),
   169  			func() *model.Config {
   170  				cfg := defaultConfigGen()
   171  				cfg.FileSettings.PublicLinkSalt = model.NewString("actual")
   172  				return cfg
   173  			}(),
   174  			ConfigDiffs{
   175  				{
   176  					Path:      "FileSettings.PublicLinkSalt",
   177  					BaseVal:   model.FakeSetting,
   178  					ActualVal: model.FakeSetting,
   179  				},
   180  			},
   181  			"",
   182  		},
   183  		{
   184  			"sensitive FileSettings.AmazonS3SecretAccessKey",
   185  			func() *model.Config {
   186  				cfg := defaultConfigGen()
   187  				cfg.FileSettings.AmazonS3SecretAccessKey = model.NewString("base")
   188  				return cfg
   189  			}(),
   190  			func() *model.Config {
   191  				cfg := defaultConfigGen()
   192  				cfg.FileSettings.AmazonS3SecretAccessKey = model.NewString("actual")
   193  				return cfg
   194  			}(),
   195  			ConfigDiffs{
   196  				{
   197  					Path:      "FileSettings.AmazonS3SecretAccessKey",
   198  					BaseVal:   model.FakeSetting,
   199  					ActualVal: model.FakeSetting,
   200  				},
   201  			},
   202  			"",
   203  		},
   204  		{
   205  			"sensitive SqlSettings.DataSource",
   206  			func() *model.Config {
   207  				cfg := defaultConfigGen()
   208  				cfg.SqlSettings.DataSource = model.NewString("base")
   209  				return cfg
   210  			}(),
   211  			func() *model.Config {
   212  				cfg := defaultConfigGen()
   213  				cfg.SqlSettings.DataSource = model.NewString("actual")
   214  				return cfg
   215  			}(),
   216  			ConfigDiffs{
   217  				{
   218  					Path:      "SqlSettings.DataSource",
   219  					BaseVal:   model.FakeSetting,
   220  					ActualVal: model.FakeSetting,
   221  				},
   222  			},
   223  			"",
   224  		},
   225  		{
   226  			"sensitive SqlSettings.AtRestEncryptKey",
   227  			func() *model.Config {
   228  				cfg := defaultConfigGen()
   229  				cfg.SqlSettings.AtRestEncryptKey = model.NewString("base")
   230  				return cfg
   231  			}(),
   232  			func() *model.Config {
   233  				cfg := defaultConfigGen()
   234  				cfg.SqlSettings.AtRestEncryptKey = model.NewString("actual")
   235  				return cfg
   236  			}(),
   237  			ConfigDiffs{
   238  				{
   239  					Path:      "SqlSettings.AtRestEncryptKey",
   240  					BaseVal:   model.FakeSetting,
   241  					ActualVal: model.FakeSetting,
   242  				},
   243  			},
   244  			"",
   245  		},
   246  		{
   247  			"sensitive SqlSettings.DataSourceReplicas",
   248  			func() *model.Config {
   249  				cfg := defaultConfigGen()
   250  				cfg.SqlSettings.DataSourceReplicas = []string{
   251  					"ds0",
   252  					"ds1",
   253  				}
   254  				return cfg
   255  			}(),
   256  			func() *model.Config {
   257  				cfg := defaultConfigGen()
   258  				cfg.SqlSettings.DataSourceReplicas = []string{
   259  					"ds0",
   260  					"ds1",
   261  					"ds2",
   262  				}
   263  				return cfg
   264  			}(),
   265  			ConfigDiffs{
   266  				{
   267  					Path:      "SqlSettings.DataSourceReplicas",
   268  					BaseVal:   model.FakeSetting,
   269  					ActualVal: model.FakeSetting,
   270  				},
   271  			},
   272  			"",
   273  		},
   274  		{
   275  			"sensitive SqlSettings.DataSourceSearchReplicas",
   276  			func() *model.Config {
   277  				cfg := defaultConfigGen()
   278  				cfg.SqlSettings.DataSourceSearchReplicas = []string{
   279  					"ds0",
   280  					"ds1",
   281  				}
   282  				return cfg
   283  			}(),
   284  			func() *model.Config {
   285  				cfg := defaultConfigGen()
   286  				cfg.SqlSettings.DataSourceSearchReplicas = []string{
   287  					"ds0",
   288  					"ds1",
   289  					"ds2",
   290  				}
   291  				return cfg
   292  			}(),
   293  			ConfigDiffs{
   294  				{
   295  					Path:      "SqlSettings.DataSourceSearchReplicas",
   296  					BaseVal:   model.FakeSetting,
   297  					ActualVal: model.FakeSetting,
   298  				},
   299  			},
   300  			"",
   301  		},
   302  		{
   303  			"sensitive EmailSettings.SMTPPassword",
   304  			func() *model.Config {
   305  				cfg := defaultConfigGen()
   306  				cfg.EmailSettings.SMTPPassword = model.NewString("base")
   307  				return cfg
   308  			}(),
   309  			func() *model.Config {
   310  				cfg := defaultConfigGen()
   311  				cfg.EmailSettings.SMTPPassword = model.NewString("actual")
   312  				return cfg
   313  			}(),
   314  			ConfigDiffs{
   315  				{
   316  					Path:      "EmailSettings.SMTPPassword",
   317  					BaseVal:   model.FakeSetting,
   318  					ActualVal: model.FakeSetting,
   319  				},
   320  			},
   321  			"",
   322  		},
   323  		{
   324  			"sensitive GitLabSettings.Secret",
   325  			func() *model.Config {
   326  				cfg := defaultConfigGen()
   327  				cfg.GitLabSettings.Secret = model.NewString("base")
   328  				return cfg
   329  			}(),
   330  			func() *model.Config {
   331  				cfg := defaultConfigGen()
   332  				cfg.GitLabSettings.Secret = model.NewString("actual")
   333  				return cfg
   334  			}(),
   335  			ConfigDiffs{
   336  				{
   337  					Path:      "GitLabSettings.Secret",
   338  					BaseVal:   model.FakeSetting,
   339  					ActualVal: model.FakeSetting,
   340  				},
   341  			},
   342  			"",
   343  		},
   344  		{
   345  			"sensitive GoogleSettings.Secret",
   346  			func() *model.Config {
   347  				cfg := defaultConfigGen()
   348  				cfg.GoogleSettings.Secret = model.NewString("base")
   349  				return cfg
   350  			}(),
   351  			func() *model.Config {
   352  				cfg := defaultConfigGen()
   353  				cfg.GoogleSettings.Secret = model.NewString("actual")
   354  				return cfg
   355  			}(),
   356  			ConfigDiffs{
   357  				{
   358  					Path:      "GoogleSettings.Secret",
   359  					BaseVal:   model.FakeSetting,
   360  					ActualVal: model.FakeSetting,
   361  				},
   362  			},
   363  			"",
   364  		},
   365  		{
   366  			"sensitive Office365Settings.Secret",
   367  			func() *model.Config {
   368  				cfg := defaultConfigGen()
   369  				cfg.Office365Settings.Secret = model.NewString("base")
   370  				return cfg
   371  			}(),
   372  			func() *model.Config {
   373  				cfg := defaultConfigGen()
   374  				cfg.Office365Settings.Secret = model.NewString("actual")
   375  				return cfg
   376  			}(),
   377  			ConfigDiffs{
   378  				{
   379  					Path:      "Office365Settings.Secret",
   380  					BaseVal:   model.FakeSetting,
   381  					ActualVal: model.FakeSetting,
   382  				},
   383  			},
   384  			"",
   385  		},
   386  		{
   387  			"sensitive OpenIdSettings.Secret",
   388  			func() *model.Config {
   389  				cfg := defaultConfigGen()
   390  				cfg.OpenIdSettings.Secret = model.NewString("base")
   391  				return cfg
   392  			}(),
   393  			func() *model.Config {
   394  				cfg := defaultConfigGen()
   395  				cfg.OpenIdSettings.Secret = model.NewString("actual")
   396  				return cfg
   397  			}(),
   398  			ConfigDiffs{
   399  				{
   400  					Path:      "OpenIdSettings.Secret",
   401  					BaseVal:   model.FakeSetting,
   402  					ActualVal: model.FakeSetting,
   403  				},
   404  			},
   405  			"",
   406  		},
   407  		{
   408  			"sensitive ElasticsearchSettings.Password",
   409  			func() *model.Config {
   410  				cfg := defaultConfigGen()
   411  				cfg.ElasticsearchSettings.Password = model.NewString("base")
   412  				return cfg
   413  			}(),
   414  			func() *model.Config {
   415  				cfg := defaultConfigGen()
   416  				cfg.ElasticsearchSettings.Password = model.NewString("actual")
   417  				return cfg
   418  			}(),
   419  			ConfigDiffs{
   420  				{
   421  					Path:      "ElasticsearchSettings.Password",
   422  					BaseVal:   model.FakeSetting,
   423  					ActualVal: model.FakeSetting,
   424  				},
   425  			},
   426  			"",
   427  		},
   428  		{
   429  			"sensitive MessageExportSettings.GlobalRelaySettings",
   430  			func() *model.Config {
   431  				cfg := defaultConfigGen()
   432  				cfg.MessageExportSettings.GlobalRelaySettings = &model.GlobalRelayMessageExportSettings{
   433  					SMTPUsername: model.NewString("base"),
   434  					SMTPPassword: model.NewString("base"),
   435  					EmailAddress: model.NewString("base"),
   436  				}
   437  				return cfg
   438  			}(),
   439  			func() *model.Config {
   440  				cfg := defaultConfigGen()
   441  				cfg.MessageExportSettings.GlobalRelaySettings = &model.GlobalRelayMessageExportSettings{
   442  					SMTPUsername: model.NewString("actual"),
   443  					SMTPPassword: model.NewString("actual"),
   444  					EmailAddress: model.NewString("actual"),
   445  				}
   446  				return cfg
   447  			}(),
   448  			ConfigDiffs{
   449  				{
   450  					Path:      "MessageExportSettings.GlobalRelaySettings.SMTPUsername",
   451  					BaseVal:   model.FakeSetting,
   452  					ActualVal: model.FakeSetting,
   453  				},
   454  				{
   455  					Path:      "MessageExportSettings.GlobalRelaySettings.SMTPPassword",
   456  					BaseVal:   model.FakeSetting,
   457  					ActualVal: model.FakeSetting,
   458  				},
   459  				{
   460  					Path:      "MessageExportSettings.GlobalRelaySettings.EmailAddress",
   461  					BaseVal:   model.FakeSetting,
   462  					ActualVal: model.FakeSetting,
   463  				},
   464  			},
   465  			"",
   466  		},
   467  		{
   468  			"sensitive ServiceSettings.GfycatAPISecret",
   469  			func() *model.Config {
   470  				cfg := defaultConfigGen()
   471  				cfg.ServiceSettings.GfycatAPISecret = model.NewString("base")
   472  				return cfg
   473  			}(),
   474  			func() *model.Config {
   475  				cfg := defaultConfigGen()
   476  				cfg.ServiceSettings.GfycatAPISecret = model.NewString("actual")
   477  				return cfg
   478  			}(),
   479  			ConfigDiffs{
   480  				{
   481  					Path:      "ServiceSettings.GfycatAPISecret",
   482  					BaseVal:   model.FakeSetting,
   483  					ActualVal: model.FakeSetting,
   484  				},
   485  			},
   486  			"",
   487  		},
   488  		{
   489  			"sensitive ServiceSettings.SplitKey",
   490  			func() *model.Config {
   491  				cfg := defaultConfigGen()
   492  				cfg.ServiceSettings.SplitKey = model.NewString("base")
   493  				return cfg
   494  			}(),
   495  			func() *model.Config {
   496  				cfg := defaultConfigGen()
   497  				cfg.ServiceSettings.SplitKey = model.NewString("actual")
   498  				return cfg
   499  			}(),
   500  			ConfigDiffs{
   501  				{
   502  					Path:      "ServiceSettings.SplitKey",
   503  					BaseVal:   model.FakeSetting,
   504  					ActualVal: model.FakeSetting,
   505  				},
   506  			},
   507  			"",
   508  		},
   509  		{
   510  			"plugin config",
   511  			defaultConfigGen(),
   512  			func() *model.Config {
   513  				cfg := defaultConfigGen()
   514  				cfg.PluginSettings.Plugins = map[string]map[string]any{
   515  					"com.mattermost.newplugin": {
   516  						"key": true,
   517  					},
   518  				}
   519  				return cfg
   520  			}(),
   521  			ConfigDiffs{
   522  				{
   523  					Path:      "PluginSettings.Plugins",
   524  					BaseVal:   model.FakeSetting,
   525  					ActualVal: model.FakeSetting,
   526  				},
   527  			},
   528  			"",
   529  		},
   530  	}
   531  
   532  	for _, tc := range tcs {
   533  		t.Run(tc.name, func(t *testing.T) {
   534  			diffs, err := Diff(tc.base, tc.actual)
   535  			if tc.err != "" {
   536  				require.EqualError(t, err, tc.err)
   537  				require.Nil(t, diffs)
   538  			} else {
   539  				require.NoError(t, err)
   540  			}
   541  			require.Equal(t, tc.diffs, diffs.Sanitize())
   542  		})
   543  	}
   544  }
   545  
   546  func TestDiff(t *testing.T) {
   547  	tcs := []struct {
   548  		name   string
   549  		base   *model.Config
   550  		actual *model.Config
   551  		diffs  ConfigDiffs
   552  		err    string
   553  	}{
   554  		{
   555  			"nil",
   556  			nil,
   557  			nil,
   558  			nil,
   559  			"input configs should not be nil",
   560  		},
   561  		{
   562  			"empty",
   563  			&model.Config{},
   564  			&model.Config{},
   565  			nil,
   566  			"",
   567  		},
   568  		{
   569  			"defaults",
   570  			defaultConfigGen(),
   571  			defaultConfigGen(),
   572  			nil,
   573  			"",
   574  		},
   575  		{
   576  			"default base, actual empty",
   577  			defaultConfigGen(),
   578  			&model.Config{},
   579  			ConfigDiffs{
   580  				{
   581  					Path:      "",
   582  					BaseVal:   *defaultConfigGen(),
   583  					ActualVal: model.Config{},
   584  				},
   585  			},
   586  			"",
   587  		},
   588  		{
   589  			"empty base, actual default",
   590  			&model.Config{},
   591  			defaultConfigGen(),
   592  			ConfigDiffs{
   593  				{
   594  					Path:      "",
   595  					BaseVal:   model.Config{},
   596  					ActualVal: *defaultConfigGen(),
   597  				},
   598  			},
   599  			"",
   600  		},
   601  		{
   602  			"string change",
   603  			defaultConfigGen(),
   604  			func() *model.Config {
   605  				cfg := defaultConfigGen()
   606  				cfg.ServiceSettings.SiteURL = model.NewString("http://changed")
   607  				return cfg
   608  			}(),
   609  			ConfigDiffs{
   610  				{
   611  					Path:      "ServiceSettings.SiteURL",
   612  					BaseVal:   *defaultConfigGen().ServiceSettings.SiteURL,
   613  					ActualVal: "http://changed",
   614  				},
   615  			},
   616  			"",
   617  		},
   618  		{
   619  			"string nil",
   620  			defaultConfigGen(),
   621  			func() *model.Config {
   622  				cfg := defaultConfigGen()
   623  				cfg.ServiceSettings.SiteURL = nil
   624  				return cfg
   625  			}(),
   626  			ConfigDiffs{
   627  				{
   628  					Path:    "ServiceSettings.SiteURL",
   629  					BaseVal: defaultConfigGen().ServiceSettings.SiteURL,
   630  					ActualVal: func() *string {
   631  						return nil
   632  					}(),
   633  				},
   634  			},
   635  			"",
   636  		},
   637  		{
   638  			"bool change",
   639  			defaultConfigGen(),
   640  			func() *model.Config {
   641  				cfg := defaultConfigGen()
   642  				cfg.PluginSettings.Enable = model.NewBool(!*cfg.PluginSettings.Enable)
   643  				return cfg
   644  			}(),
   645  			ConfigDiffs{
   646  				{
   647  					Path:      "PluginSettings.Enable",
   648  					BaseVal:   true,
   649  					ActualVal: false,
   650  				},
   651  			},
   652  			"",
   653  		},
   654  		{
   655  			"bool nil",
   656  			defaultConfigGen(),
   657  			func() *model.Config {
   658  				cfg := defaultConfigGen()
   659  				cfg.PluginSettings.Enable = nil
   660  				return cfg
   661  			}(),
   662  			ConfigDiffs{
   663  				{
   664  					Path:    "PluginSettings.Enable",
   665  					BaseVal: defaultConfigGen().PluginSettings.Enable,
   666  					ActualVal: func() *bool {
   667  						return nil
   668  					}(),
   669  				},
   670  			},
   671  			"",
   672  		},
   673  		{
   674  			"int change",
   675  			defaultConfigGen(),
   676  			func() *model.Config {
   677  				cfg := defaultConfigGen()
   678  				cfg.ServiceSettings.ReadTimeout = model.NewInt(0)
   679  				return cfg
   680  			}(),
   681  			ConfigDiffs{
   682  				{
   683  					Path:      "ServiceSettings.ReadTimeout",
   684  					BaseVal:   *defaultConfigGen().ServiceSettings.ReadTimeout,
   685  					ActualVal: 0,
   686  				},
   687  			},
   688  			"",
   689  		},
   690  		{
   691  			"int nil",
   692  			defaultConfigGen(),
   693  			func() *model.Config {
   694  				cfg := defaultConfigGen()
   695  				cfg.ServiceSettings.ReadTimeout = nil
   696  				return cfg
   697  			}(),
   698  			ConfigDiffs{
   699  				{
   700  					Path:    "ServiceSettings.ReadTimeout",
   701  					BaseVal: defaultConfigGen().ServiceSettings.ReadTimeout,
   702  					ActualVal: func() *int {
   703  						return nil
   704  					}(),
   705  				},
   706  			},
   707  			"",
   708  		},
   709  		{
   710  			"slice addition",
   711  			defaultConfigGen(),
   712  			func() *model.Config {
   713  				cfg := defaultConfigGen()
   714  				cfg.SqlSettings.DataSourceReplicas = []string{
   715  					"ds0",
   716  					"ds1",
   717  				}
   718  				return cfg
   719  			}(),
   720  			ConfigDiffs{
   721  				{
   722  					Path:    "SqlSettings.DataSourceReplicas",
   723  					BaseVal: defaultConfigGen().SqlSettings.DataSourceReplicas,
   724  					ActualVal: []string{
   725  						"ds0",
   726  						"ds1",
   727  					},
   728  				},
   729  			},
   730  			"",
   731  		},
   732  		{
   733  			"slice deletion",
   734  			func() *model.Config {
   735  				cfg := defaultConfigGen()
   736  				cfg.SqlSettings.DataSourceReplicas = []string{
   737  					"ds0",
   738  					"ds1",
   739  				}
   740  				return cfg
   741  			}(),
   742  			func() *model.Config {
   743  				cfg := defaultConfigGen()
   744  				cfg.SqlSettings.DataSourceReplicas = []string{
   745  					"ds0",
   746  				}
   747  				return cfg
   748  			}(),
   749  			ConfigDiffs{
   750  				{
   751  					Path: "SqlSettings.DataSourceReplicas",
   752  					BaseVal: []string{
   753  						"ds0",
   754  						"ds1",
   755  					},
   756  					ActualVal: []string{
   757  						"ds0",
   758  					},
   759  				},
   760  			},
   761  			"",
   762  		},
   763  		{
   764  			"slice nil",
   765  			func() *model.Config {
   766  				cfg := defaultConfigGen()
   767  				cfg.SqlSettings.DataSourceReplicas = []string{
   768  					"ds0",
   769  					"ds1",
   770  				}
   771  				return cfg
   772  			}(),
   773  			func() *model.Config {
   774  				cfg := defaultConfigGen()
   775  				cfg.SqlSettings.DataSourceReplicas = nil
   776  				return cfg
   777  			}(),
   778  			ConfigDiffs{
   779  				{
   780  					Path: "SqlSettings.DataSourceReplicas",
   781  					BaseVal: []string{
   782  						"ds0",
   783  						"ds1",
   784  					},
   785  					ActualVal: func() []string {
   786  						return nil
   787  					}(),
   788  				},
   789  			},
   790  			"",
   791  		},
   792  		{
   793  			"map change",
   794  			defaultConfigGen(),
   795  			func() *model.Config {
   796  				cfg := defaultConfigGen()
   797  				cfg.PluginSettings.PluginStates["com.mattermost.nps"] = &model.PluginState{
   798  					Enable: !cfg.PluginSettings.PluginStates["com.mattermost.nps"].Enable,
   799  				}
   800  				return cfg
   801  			}(),
   802  			ConfigDiffs{
   803  				{
   804  					Path:    "PluginSettings.PluginStates",
   805  					BaseVal: defaultConfigGen().PluginSettings.PluginStates,
   806  					ActualVal: map[string]*model.PluginState{
   807  						"com.mattermost.nps": {
   808  							Enable: !defaultConfigGen().PluginSettings.PluginStates["com.mattermost.nps"].Enable,
   809  						},
   810  						"com.mattermost.apps": {
   811  							Enable: true,
   812  						},
   813  						"com.mattermost.calls": {
   814  							Enable: true,
   815  						},
   816  					},
   817  				},
   818  			},
   819  			"",
   820  		},
   821  		{
   822  			"map addition",
   823  			defaultConfigGen(),
   824  			func() *model.Config {
   825  				cfg := defaultConfigGen()
   826  				cfg.PluginSettings.PluginStates["com.mattermost.newplugin"] = &model.PluginState{
   827  					Enable: true,
   828  				}
   829  				return cfg
   830  			}(),
   831  			ConfigDiffs{
   832  				{
   833  					Path:    "PluginSettings.PluginStates",
   834  					BaseVal: defaultConfigGen().PluginSettings.PluginStates,
   835  					ActualVal: map[string]*model.PluginState{
   836  						"com.mattermost.nps": {
   837  							Enable: defaultConfigGen().PluginSettings.PluginStates["com.mattermost.nps"].Enable,
   838  						},
   839  						"com.mattermost.newplugin": {
   840  							Enable: true,
   841  						},
   842  						"com.mattermost.apps": {
   843  							Enable: true,
   844  						},
   845  						"com.mattermost.calls": {
   846  							Enable: true,
   847  						},
   848  					},
   849  				},
   850  			},
   851  			"",
   852  		},
   853  		{
   854  			"map deletion",
   855  			defaultConfigGen(),
   856  			func() *model.Config {
   857  				cfg := defaultConfigGen()
   858  				delete(cfg.PluginSettings.PluginStates, "com.mattermost.nps")
   859  				return cfg
   860  			}(),
   861  			ConfigDiffs{
   862  				{
   863  					Path:    "PluginSettings.PluginStates",
   864  					BaseVal: defaultConfigGen().PluginSettings.PluginStates,
   865  					ActualVal: map[string]*model.PluginState{
   866  						"com.mattermost.apps": {
   867  							Enable: true,
   868  						},
   869  						"com.mattermost.calls": {
   870  							Enable: true,
   871  						},
   872  					},
   873  				},
   874  			},
   875  			"",
   876  		},
   877  		{
   878  			"map nil",
   879  			defaultConfigGen(),
   880  			func() *model.Config {
   881  				cfg := defaultConfigGen()
   882  				cfg.PluginSettings.PluginStates = nil
   883  				return cfg
   884  			}(),
   885  			ConfigDiffs{
   886  				{
   887  					Path:    "PluginSettings.PluginStates",
   888  					BaseVal: defaultConfigGen().PluginSettings.PluginStates,
   889  					ActualVal: func() map[string]*model.PluginState {
   890  						return nil
   891  					}(),
   892  				},
   893  			},
   894  			"",
   895  		},
   896  		{
   897  			"map type change",
   898  			func() *model.Config {
   899  				cfg := defaultConfigGen()
   900  				cfg.PluginSettings.Plugins = map[string]map[string]any{
   901  					"com.mattermost.newplugin": {
   902  						"key": true,
   903  					},
   904  				}
   905  				return cfg
   906  			}(),
   907  			func() *model.Config {
   908  				cfg := defaultConfigGen()
   909  				cfg.PluginSettings.Plugins = map[string]map[string]any{
   910  					"com.mattermost.newplugin": {
   911  						"key": "string",
   912  					},
   913  				}
   914  				return cfg
   915  			}(),
   916  			ConfigDiffs{
   917  				{
   918  					Path: "PluginSettings.Plugins",
   919  					BaseVal: func() any {
   920  						return map[string]map[string]any{
   921  							"com.mattermost.newplugin": {
   922  								"key": true,
   923  							},
   924  						}
   925  					}(),
   926  					ActualVal: func() any {
   927  						return map[string]map[string]any{
   928  							"com.mattermost.newplugin": {
   929  								"key": "string",
   930  							},
   931  						}
   932  					}(),
   933  				},
   934  			},
   935  			"",
   936  		},
   937  	}
   938  
   939  	for _, tc := range tcs {
   940  		t.Run(tc.name, func(t *testing.T) {
   941  			diffs, err := Diff(tc.base, tc.actual)
   942  			if tc.err != "" {
   943  				require.EqualError(t, err, tc.err)
   944  				require.Nil(t, diffs)
   945  			} else {
   946  				require.NoError(t, err)
   947  			}
   948  			require.Equal(t, tc.diffs, diffs)
   949  		})
   950  	}
   951  }