...

Source file src/github.com/cybertec-postgresql/pgwatch/v5/internal/cmdopts/cmdconfig_test.go

Documentation: github.com/cybertec-postgresql/pgwatch/v5/internal/cmdopts

     1  package cmdopts
     2  
     3  import (
     4  	"bytes"
     5  	"io"
     6  	"os"
     7  	"testing"
     8  
     9  	"github.com/cybertec-postgresql/pgwatch/v5/internal/metrics"
    10  	"github.com/cybertec-postgresql/pgwatch/v5/internal/sinks"
    11  	"github.com/cybertec-postgresql/pgwatch/v5/internal/sources"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  )
    15  
    16  func TestConfigInitCommand_Execute(t *testing.T) {
    17  	a := assert.New(t)
    18  	t.Run("subcommand is missing", func(*testing.T) {
    19  		os.Args = []string{0: "config_test", "config"}
    20  		_, err := New(io.Discard)
    21  		a.Error(err)
    22  	})
    23  
    24  	t.Run("subcommand is invalid", func(*testing.T) {
    25  		os.Args = []string{0: "config_test", "config", "invalid"}
    26  		_, err := New(io.Discard)
    27  		a.Error(err)
    28  	})
    29  
    30  	t.Run("sources and metrics are empty", func(*testing.T) {
    31  		os.Args = []string{0: "config_test", "config", "init"}
    32  		_, err := New(io.Discard)
    33  		a.Error(err)
    34  	})
    35  
    36  	t.Run("metrics is a proper file name", func(*testing.T) {
    37  		fname := t.TempDir() + "/metrics.yaml"
    38  		os.Args = []string{0: "config_test", "--metrics=" + fname, "config", "init"}
    39  		_, err := New(io.Discard)
    40  		a.NoError(err)
    41  		a.FileExists(fname)
    42  		fi, err := os.Stat(fname)
    43  		require.NoError(t, err)
    44  		a.True(fi.Size() > 0)
    45  	})
    46  
    47  	t.Run("sources is a proper file name", func(*testing.T) {
    48  		fname := t.TempDir() + "/sources.yaml"
    49  		os.Args = []string{0: "config_test", "--sources=" + fname, "config", "init"}
    50  		_, err := New(io.Discard)
    51  		a.NoError(err)
    52  		a.FileExists(fname)
    53  		fi, err := os.Stat(fname)
    54  		require.NoError(t, err)
    55  		a.True(fi.Size() > 0)
    56  	})
    57  
    58  	t.Run("metrics is an invalid file name", func(*testing.T) {
    59  		os.Args = []string{0: "config_test", "--metrics=/", "config", "init"}
    60  		opts, err := New(io.Discard)
    61  		a.Error(err)
    62  		a.Equal(ExitCodeConfigError, opts.ExitCode)
    63  	})
    64  
    65  	t.Run("metrics is proper postgres connectin string", func(*testing.T) {
    66  		os.Args = []string{0: "config_test", "--metrics=postgresql://foo@bar/baz", "config", "init"}
    67  		opts, err := New(io.Discard)
    68  		a.Error(err)
    69  		a.Equal(ExitCodeConfigError, opts.ExitCode)
    70  	})
    71  
    72  }
    73  
    74  func TestConfigUpgradeCommand_Execute(t *testing.T) {
    75  	t.Run("sources and metrics are empty", func(t *testing.T) {
    76  		a := assert.New(t)
    77  		os.Args = []string{0: "config_test", "config", "upgrade"}
    78  		_, err := New(io.Discard)
    79  		a.Error(err)
    80  	})
    81  
    82  	t.Run("metrics is a proper file name but files are not upgradable", func(t *testing.T) {
    83  		a := assert.New(t)
    84  		fname := t.TempDir() + "/metrics.yaml"
    85  		os.Args = []string{0: "config_test", "--metrics=" + fname, "config", "upgrade"}
    86  		c, err := New(io.Discard)
    87  		// File-based configs return nil (all unsupported) and complete with OK
    88  		a.NoError(err)
    89  		a.True(c.CommandCompleted)
    90  		a.Equal(ExitCodeOK, c.ExitCode)
    91  	})
    92  
    93  }
    94  
    95  // Mock types for testing Migrator interface with proper interface implementations
    96  
    97  type mockMigratableSourcesReader struct {
    98  	migrateErr        error
    99  	needsMigration    bool
   100  	needsMigrationErr error
   101  }
   102  
   103  func (m *mockMigratableSourcesReader) Migrate() error { return m.migrateErr }
   104  func (m *mockMigratableSourcesReader) NeedsMigration() (bool, error) {
   105  	return m.needsMigration, m.needsMigrationErr
   106  }
   107  func (m *mockMigratableSourcesReader) GetSources() (sources.Sources, error) {
   108  	return sources.Sources{}, nil
   109  }
   110  func (m *mockMigratableSourcesReader) WriteSources(sources.Sources) error { return nil }
   111  func (m *mockMigratableSourcesReader) DeleteSource(string) error          { return nil }
   112  func (m *mockMigratableSourcesReader) UpdateSource(sources.Source) error  { return nil }
   113  func (m *mockMigratableSourcesReader) CreateSource(sources.Source) error  { return nil }
   114  
   115  type mockMigratableMetricsReader struct {
   116  	migrateErr        error
   117  	needsMigration    bool
   118  	needsMigrationErr error
   119  }
   120  
   121  func (m *mockMigratableMetricsReader) Migrate() error { return m.migrateErr }
   122  func (m *mockMigratableMetricsReader) NeedsMigration() (bool, error) {
   123  	return m.needsMigration, m.needsMigrationErr
   124  }
   125  func (m *mockMigratableMetricsReader) GetMetrics() (*metrics.Metrics, error) {
   126  	return &metrics.Metrics{}, nil
   127  }
   128  func (m *mockMigratableMetricsReader) WriteMetrics(*metrics.Metrics) error       { return nil }
   129  func (m *mockMigratableMetricsReader) DeleteMetric(string) error                 { return nil }
   130  func (m *mockMigratableMetricsReader) UpdateMetric(string, metrics.Metric) error { return nil }
   131  func (m *mockMigratableMetricsReader) CreateMetric(string, metrics.Metric) error { return nil }
   132  func (m *mockMigratableMetricsReader) DeletePreset(string) error                 { return nil }
   133  func (m *mockMigratableMetricsReader) UpdatePreset(string, metrics.Preset) error { return nil }
   134  func (m *mockMigratableMetricsReader) CreatePreset(string, metrics.Preset) error { return nil }
   135  
   136  type mockMigratableSinksWriter struct {
   137  	migrateErr        error
   138  	needsMigration    bool
   139  	needsMigrationErr error
   140  }
   141  
   142  func (m *mockMigratableSinksWriter) Migrate() error { return m.migrateErr }
   143  func (m *mockMigratableSinksWriter) NeedsMigration() (bool, error) {
   144  	return m.needsMigration, m.needsMigrationErr
   145  }
   146  func (m *mockMigratableSinksWriter) SyncMetric(string, string, sinks.SyncOp) error { return nil }
   147  func (m *mockMigratableSinksWriter) Write(metrics.MeasurementEnvelope) error       { return nil }
   148  
   149  func TestNeedsSchemaUpgrade(t *testing.T) {
   150  	tests := []struct {
   151  		name          string
   152  		setupMocks    func(*Options)
   153  		expectUpgrade bool
   154  		expectError   bool
   155  	}{
   156  		{
   157  			name: "sources needs migration",
   158  			setupMocks: func(opts *Options) {
   159  				opts.SourcesReaderWriter = &mockMigratableSourcesReader{needsMigration: true}
   160  			},
   161  			expectUpgrade: true,
   162  			expectError:   false,
   163  		},
   164  		{
   165  			name: "metrics needs migration",
   166  			setupMocks: func(opts *Options) {
   167  				opts.SourcesReaderWriter = &mockMigratableSourcesReader{needsMigration: false}
   168  				opts.MetricsReaderWriter = &mockMigratableMetricsReader{needsMigration: true}
   169  			},
   170  			expectUpgrade: true,
   171  			expectError:   false,
   172  		},
   173  		{
   174  			name: "sinks needs migration",
   175  			setupMocks: func(opts *Options) {
   176  				opts.SourcesReaderWriter = &mockMigratableSourcesReader{needsMigration: false}
   177  				opts.MetricsReaderWriter = &mockMigratableMetricsReader{needsMigration: false}
   178  				opts.SinksWriter = &mockMigratableSinksWriter{needsMigration: true}
   179  			},
   180  			expectUpgrade: true,
   181  			expectError:   false,
   182  		},
   183  		{
   184  			name: "no migration needed",
   185  			setupMocks: func(opts *Options) {
   186  				opts.SourcesReaderWriter = &mockMigratableSourcesReader{needsMigration: false}
   187  				opts.MetricsReaderWriter = &mockMigratableMetricsReader{needsMigration: false}
   188  				opts.SinksWriter = &mockMigratableSinksWriter{needsMigration: false}
   189  			},
   190  			expectUpgrade: false,
   191  			expectError:   false,
   192  		},
   193  		{
   194  			name: "error checking sources migration",
   195  			setupMocks: func(opts *Options) {
   196  				opts.SourcesReaderWriter = &mockMigratableSourcesReader{needsMigrationErr: assert.AnError}
   197  			},
   198  			expectUpgrade: false,
   199  			expectError:   true,
   200  		},
   201  		{
   202  			name: "error checking metrics migration",
   203  			setupMocks: func(opts *Options) {
   204  				opts.SourcesReaderWriter = &mockMigratableSourcesReader{needsMigration: false}
   205  				opts.MetricsReaderWriter = &mockMigratableMetricsReader{needsMigrationErr: assert.AnError}
   206  			},
   207  			expectUpgrade: false,
   208  			expectError:   true,
   209  		},
   210  		{
   211  			name: "error checking sinks migration",
   212  			setupMocks: func(opts *Options) {
   213  				opts.SourcesReaderWriter = &mockMigratableSourcesReader{needsMigration: false}
   214  				opts.MetricsReaderWriter = &mockMigratableMetricsReader{needsMigration: false}
   215  				opts.SinksWriter = &mockMigratableSinksWriter{needsMigrationErr: assert.AnError}
   216  			},
   217  			expectUpgrade: false,
   218  			expectError:   true,
   219  		},
   220  	}
   221  
   222  	for _, tt := range tests {
   223  		t.Run(tt.name, func(t *testing.T) {
   224  			opts := &Options{}
   225  			if tt.setupMocks != nil {
   226  				tt.setupMocks(opts)
   227  			}
   228  
   229  			upgrade, err := opts.NeedsSchemaUpgrade()
   230  
   231  			assert.Equal(t, tt.expectUpgrade, upgrade)
   232  			if tt.expectError {
   233  				assert.Error(t, err)
   234  			} else {
   235  				assert.NoError(t, err)
   236  			}
   237  		})
   238  	}
   239  }
   240  
   241  func TestConfigInitCommand_InitSources(t *testing.T) {
   242  	a := assert.New(t)
   243  
   244  	t.Run("yaml file creation", func(*testing.T) {
   245  		fname := t.TempDir() + "/sources.yaml"
   246  		opts := &Options{
   247  			Sources: sources.CmdOpts{Sources: fname},
   248  		}
   249  		cmd := ConfigInitCommand{owner: opts}
   250  		err := cmd.InitSources()
   251  		a.NoError(err)
   252  		a.FileExists(fname)
   253  	})
   254  
   255  	t.Run("postgres connection - error without setup", func(*testing.T) {
   256  		opts := &Options{
   257  			Sources: sources.CmdOpts{Sources: "postgresql://user@host/db"},
   258  		}
   259  		cmd := ConfigInitCommand{owner: opts}
   260  		err := cmd.InitSources()
   261  		a.Error(err)
   262  	})
   263  }
   264  
   265  func TestConfigInitCommand_InitMetrics(t *testing.T) {
   266  	a := assert.New(t)
   267  
   268  	t.Run("yaml file creation with default metrics", func(*testing.T) {
   269  		fname := t.TempDir() + "/metrics.yaml"
   270  		opts := &Options{
   271  			Metrics: metrics.CmdOpts{Metrics: fname},
   272  		}
   273  		cmd := ConfigInitCommand{owner: opts}
   274  		err := cmd.InitMetrics()
   275  		a.NoError(err)
   276  		a.FileExists(fname)
   277  	})
   278  
   279  	t.Run("postgres connection - error without setup", func(*testing.T) {
   280  		opts := &Options{
   281  			Metrics: metrics.CmdOpts{Metrics: "postgresql://user@host/db"},
   282  		}
   283  		cmd := ConfigInitCommand{owner: opts}
   284  		err := cmd.InitMetrics()
   285  		a.Error(err)
   286  	})
   287  }
   288  
   289  func TestConfigInitCommand_InitSinks(t *testing.T) {
   290  	a := assert.New(t)
   291  
   292  	t.Run("postgres connection - error without setup", func(*testing.T) {
   293  		opts := &Options{
   294  			Sinks: sinks.CmdOpts{Sinks: []string{"postgresql://user@host/db"}},
   295  		}
   296  		cmd := ConfigInitCommand{owner: opts}
   297  		err := cmd.InitSinks()
   298  		a.Error(err)
   299  	})
   300  }
   301  
   302  func TestConfigUpgradeCommand_Errors(t *testing.T) {
   303  	a := assert.New(t)
   304  
   305  	t.Run("non-postgres configuration not supported", func(*testing.T) {
   306  		opts := &Options{
   307  			Metrics:      metrics.CmdOpts{Metrics: "/tmp/metrics.yaml"},
   308  			Sources:      sources.CmdOpts{Sources: "/tmp/sources.yaml", Refresh: 120, MaxParallelConnectionsPerDb: 1},
   309  			Sinks:        sinks.CmdOpts{},
   310  			OutputWriter: t.Output(),
   311  		}
   312  		cmd := ConfigUpgradeCommand{owner: opts}
   313  		err := cmd.Execute(nil)
   314  		if err != nil {
   315  			a.Contains(err.Error(), "cannot updrage storage")
   316  		}
   317  		a.Equal(ExitCodeOK, opts.ExitCode)
   318  	})
   319  
   320  	t.Run("init metrics reader fails", func(*testing.T) {
   321  		opts := &Options{
   322  			Metrics:      metrics.CmdOpts{Metrics: "postgresql://invalid@host/db"},
   323  			Sources:      sources.CmdOpts{Sources: "postgresql://invalid@host/db", Refresh: 120, MaxParallelConnectionsPerDb: 1},
   324  			Sinks:        sinks.CmdOpts{},
   325  			OutputWriter: t.Output(),
   326  		}
   327  		cmd := ConfigUpgradeCommand{owner: opts}
   328  		err := cmd.Execute(nil)
   329  		a.Error(err)
   330  		a.Equal(ExitCodeConfigError, opts.ExitCode)
   331  	})
   332  }
   333  
   334  func TestConfigUpgradeCommand_Execute_Coverage(t *testing.T) {
   335  	a := assert.New(t)
   336  
   337  	t.Run("no components specified - returns error", func(*testing.T) {
   338  		opts := &Options{
   339  			OutputWriter: io.Discard,
   340  		}
   341  		cmd := ConfigUpgradeCommand{owner: opts}
   342  		err := cmd.Execute(nil)
   343  		a.Error(err)
   344  		a.ErrorContains(err, "at least one of --sources, --metrics, or --sink must be specified")
   345  		a.Equal(ExitCodeConfigError, opts.ExitCode)
   346  	})
   347  
   348  	t.Run("yaml sources/metrics - logs warning", func(*testing.T) {
   349  		var output bytes.Buffer
   350  		opts := &Options{
   351  			Metrics:      metrics.CmdOpts{Metrics: "/tmp/metrics.yaml"},
   352  			Sources:      sources.CmdOpts{Sources: "/tmp/sources.yaml"},
   353  			OutputWriter: &output,
   354  		}
   355  		cmd := ConfigUpgradeCommand{owner: opts}
   356  		err := cmd.Execute(nil)
   357  		// Execute will return errors for unsupported storage types
   358  		if err != nil {
   359  			a.Contains(err.Error(), "cannot updrage storage")
   360  			a.Contains(err.Error(), "unsupported operation")
   361  		}
   362  		a.Equal(ExitCodeOK, opts.ExitCode)
   363  	})
   364  
   365  	t.Run("successful sink upgrade only - connection fails", func(*testing.T) {
   366  		opts := &Options{
   367  			Sinks:        sinks.CmdOpts{Sinks: []string{"postgresql://localhost/db"}},
   368  			OutputWriter: io.Discard,
   369  		}
   370  		cmd := ConfigUpgradeCommand{owner: opts}
   371  		err := cmd.Execute(nil)
   372  		// Will fail to connect to postgres since it's not running
   373  		a.Error(err)
   374  		a.Equal(ExitCodeConfigError, opts.ExitCode)
   375  	})
   376  
   377  	t.Run("sink upgrade with postgres connection string - connection fails", func(*testing.T) {
   378  		opts := &Options{
   379  			Sinks:        sinks.CmdOpts{Sinks: []string{"postgresql://localhost/db"}},
   380  			OutputWriter: io.Discard,
   381  		}
   382  		cmd := ConfigUpgradeCommand{owner: opts}
   383  		err := cmd.Execute(nil)
   384  		// Connection will fail
   385  		a.Error(err)
   386  		a.Equal(ExitCodeConfigError, opts.ExitCode)
   387  	})
   388  
   389  	t.Run("non-postgres sink - unsupported error", func(*testing.T) {
   390  		var output bytes.Buffer
   391  		opts := &Options{
   392  			Sinks:        sinks.CmdOpts{Sinks: []string{"jsonfile://test.json"}},
   393  			OutputWriter: &output,
   394  		}
   395  		cmd := ConfigUpgradeCommand{owner: opts}
   396  		err := cmd.Execute(nil)
   397  		// Non-postgres URIs return unsupported error
   398  		if err != nil {
   399  			a.Contains(err.Error(), "cannot updrage storage")
   400  			a.Contains(err.Error(), "unsupported operation")
   401  		}
   402  		a.Equal(ExitCodeOK, opts.ExitCode)
   403  	})
   404  
   405  	t.Run("yaml sources/metrics with postgres sink - connection fails", func(*testing.T) {
   406  		var output bytes.Buffer
   407  		opts := &Options{
   408  			Metrics:      metrics.CmdOpts{Metrics: "/tmp/metrics.yaml"},
   409  			Sources:      sources.CmdOpts{Sources: "/tmp/sources.yaml"},
   410  			Sinks:        sinks.CmdOpts{Sinks: []string{"postgresql://localhost/db"}},
   411  			OutputWriter: &output,
   412  		}
   413  		cmd := ConfigUpgradeCommand{owner: opts}
   414  		err := cmd.Execute(nil)
   415  		// Will have errors for yaml configs and connection failure for sink
   416  		a.Error(err)
   417  		a.Equal(ExitCodeConfigError, opts.ExitCode)
   418  	})
   419  
   420  	t.Run("postgres sources/metrics with non-postgres sink - connection fails", func(*testing.T) {
   421  		var output bytes.Buffer
   422  		opts := &Options{
   423  			Metrics:      metrics.CmdOpts{Metrics: "postgresql://localhost/db"},
   424  			Sources:      sources.CmdOpts{Sources: "postgresql://localhost/db"},
   425  			Sinks:        sinks.CmdOpts{Sinks: []string{"jsonfile://test.json"}},
   426  			OutputWriter: &output,
   427  		}
   428  		cmd := ConfigUpgradeCommand{owner: opts}
   429  		err := cmd.Execute(nil)
   430  		// Connection will fail for postgres URIs
   431  		a.Error(err)
   432  		a.Equal(ExitCodeConfigError, opts.ExitCode)
   433  	})
   434  
   435  	t.Run("only metrics specified as yaml - unsupported error", func(*testing.T) {
   436  		var output bytes.Buffer
   437  		opts := &Options{
   438  			Metrics:      metrics.CmdOpts{Metrics: "/tmp/metrics.yaml"},
   439  			OutputWriter: &output,
   440  		}
   441  		cmd := ConfigUpgradeCommand{owner: opts}
   442  		err := cmd.Execute(nil)
   443  		if err != nil {
   444  			a.Contains(err.Error(), "cannot updrage storage")
   445  		}
   446  		a.Equal(ExitCodeOK, opts.ExitCode)
   447  	})
   448  
   449  	t.Run("only sources specified as yaml - unsupported error", func(*testing.T) {
   450  		var output bytes.Buffer
   451  		opts := &Options{
   452  			Sources:      sources.CmdOpts{Sources: "/tmp/sources.yaml"},
   453  			OutputWriter: &output,
   454  		}
   455  		cmd := ConfigUpgradeCommand{owner: opts}
   456  		err := cmd.Execute(nil)
   457  		if err != nil {
   458  			a.Contains(err.Error(), "cannot updrage storage")
   459  		}
   460  		a.Equal(ExitCodeOK, opts.ExitCode)
   461  	})
   462  
   463  	t.Run("both metrics and sources specified, only metrics is postgres", func(*testing.T) {
   464  		var output bytes.Buffer
   465  		opts := &Options{
   466  			Metrics:      metrics.CmdOpts{Metrics: "postgresql://localhost/db"},
   467  			Sources:      sources.CmdOpts{Sources: "/tmp/sources.yaml"},
   468  			OutputWriter: &output,
   469  		}
   470  		cmd := ConfigUpgradeCommand{owner: opts}
   471  		err := cmd.Execute(nil)
   472  		// Will fail to connect to postgres
   473  		a.Error(err)
   474  		a.Equal(ExitCodeConfigError, opts.ExitCode)
   475  	})
   476  
   477  	t.Run("postgres metrics and sources - connection fails", func(*testing.T) {
   478  		var output bytes.Buffer
   479  		opts := &Options{
   480  			Metrics:      metrics.CmdOpts{Metrics: "postgresql://localhost/db"},
   481  			Sources:      sources.CmdOpts{Sources: "postgresql://localhost/db"},
   482  			OutputWriter: &output,
   483  		}
   484  		cmd := ConfigUpgradeCommand{owner: opts}
   485  		err := cmd.Execute(nil)
   486  		// Will fail to connect
   487  		a.Error(err)
   488  		a.Equal(ExitCodeConfigError, opts.ExitCode)
   489  	})
   490  }
   491