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
88 a.NoError(err)
89 a.True(c.CommandCompleted)
90 a.Equal(ExitCodeOK, c.ExitCode)
91 })
92
93 }
94
95
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
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
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
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
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
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
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
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
487 a.Error(err)
488 a.Equal(ExitCodeConfigError, opts.ExitCode)
489 })
490 }
491