package test import ( "RenewCMS/internal/infrastructure/persistence/database" "bytes" "errors" "os" "path/filepath" "strings" "testing" "time" ) type fakeDumpTable struct { create string columns []database.DumpColumn rows [][][]byte } type fakeDumpSource struct { order []string tables map[string]fakeDumpTable failOn string closed bool rowsError error } var _ database.DumpSource = &fakeDumpSource{} type fakeDumpRows struct { table fakeDumpTable index int failed error } var _ database.DumpRows = &fakeDumpRows{} func (s *fakeDumpSource) Tables() ([]string, error) { return s.order, nil } func (s *fakeDumpSource) CreateStatement(table string) (string, error) { if s.failOn == table { return "", errors.New("show create failed") } return s.tables[table].create, nil } func (s *fakeDumpSource) Rows(table string) (database.DumpRows, error) { return &fakeDumpRows{table: s.tables[table], index: -1, failed: s.rowsError}, nil } func (s *fakeDumpSource) Close() error { s.closed = true return nil } func (r *fakeDumpRows) Columns() ([]database.DumpColumn, error) { return r.table.columns, nil } func (r *fakeDumpRows) Next() bool { r.index++ return r.index < len(r.table.rows) } func (r *fakeDumpRows) Values() ([][]byte, error) { return r.table.rows[r.index], nil } func (r *fakeDumpRows) Err() error { return r.failed } func (r *fakeDumpRows) Close() error { return nil } func newFakeDumpSource() *fakeDumpSource { return &fakeDumpSource{ order: []string{"notes", "plain"}, tables: map[string]fakeDumpTable{ "notes": { create: "CREATE TABLE `notes` (\n `id` int unsigned NOT NULL,\n `body` longtext,\n `blob` longblob,\n `at` datetime(3),\n `ok` tinyint(1)\n)", columns: []database.DumpColumn{ {Name: "id", Type: "UNSIGNED INT"}, {Name: "body", Type: "LONGTEXT"}, {Name: "blob", Type: "LONGBLOB"}, {Name: "at", Type: "DATETIME"}, {Name: "ok", Type: "TINYINT"}, }, rows: [][][]byte{ {[]byte("1"), []byte("it's a \"test\"\nline\\two\r\x00\x1a"), []byte{0x00, 0xff, 0x10}, []byte("2026-01-02 03:04:05.123000"), []byte("1")}, {[]byte("2"), nil, []byte{}, []byte("0000-00-00 00:00:00"), []byte("0")}, }, }, "plain": { create: "CREATE TABLE `plain` (\n `a` int\n)", columns: []database.DumpColumn{{Name: "a", Type: "INT"}}, }, }, } } func TestDumpWriter(t *testing.T) { t.Run("the dump holds the schema and the rows of every table", func(t *testing.T) { var output bytes.Buffer err := database.WriteDump(&output, newFakeDumpSource()) if err != nil { t.Fatalf("write: %v", err) } dump := output.String() for _, expected := range []string{ "CREATE TABLE `notes` (", "CREATE TABLE `plain` (", "INSERT INTO `notes` (`id`,`body`,`blob`,`at`,`ok`) VALUES (1,'it\\'s a \"test\"\\nline\\\\two\\r\\0\\Z',0x00ff10,'2026-01-02 03:04:05.123000',1);", "INSERT INTO `notes` (`id`,`body`,`blob`,`at`,`ok`) VALUES (2,NULL,'','0000-00-00 00:00:00',0);", "SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';", } { if !strings.Contains(dump, expected) { t.Errorf("the dump must contain %q, got:\n%s", expected, dump) } } if strings.Contains(dump, "INSERT INTO `plain`") { t.Error("an empty table has no rows to insert") } }) t.Run("every statement after the schema stays on one line", func(t *testing.T) { var output bytes.Buffer _ = database.WriteDump(&output, newFakeDumpSource()) for _, line := range strings.Split(output.String(), "\n") { if strings.HasPrefix(line, "INSERT") && !strings.HasSuffix(line, ");") { t.Errorf("insert split over several lines: %q", line) } } }) t.Run("a failing table stops the dump with the error", func(t *testing.T) { source := newFakeDumpSource() source.failOn = "plain" err := database.WriteDump(&bytes.Buffer{}, source) if err == nil || err.Error() != "show create failed" { t.Errorf("unexpected error %v", err) } }) t.Run("a row error stops the dump with the error", func(t *testing.T) { source := newFakeDumpSource() source.rowsError = errors.New("connection lost") err := database.WriteDump(&bytes.Buffer{}, source) if err == nil || err.Error() != "connection lost" { t.Errorf("unexpected error %v", err) } }) } type dumpBackupRig struct { directory string source *fakeDumpSource clock time.Time backup *database.DumpBackup } func newDumpBackupRig(t *testing.T) *dumpBackupRig { t.Helper() rig := &dumpBackupRig{ directory: filepath.Join(t.TempDir(), "nested", "backups"), source: newFakeDumpSource(), clock: time.Date(2026, 3, 4, 5, 6, 7, 0, time.UTC), } rig.backup = database.NewDumpBackup(func() (database.DumpSource, error) { return rig.source, nil }, rig.directory, func() time.Time { rig.clock = rig.clock.Add(time.Second) return rig.clock }) return rig } func (r *dumpBackupRig) names(t *testing.T) []string { t.Helper() entries, err := os.ReadDir(r.directory) if err != nil { t.Fatalf("read backups: %v", err) } names := make([]string, 0, len(entries)) for _, entry := range entries { names = append(names, entry.Name()) } return names } func TestDumpBackup(t *testing.T) { t.Run("a file renewcms-