package backup import ( "archive/tar" "bytes" "compress/gzip" "errors" "io" "os" "path/filepath" "testing" "time" ) // writeSampleArchive builds a two-collection archive on disk and returns its path. func writeSampleArchive(t *testing.T) string { t.Helper() path := filepath.Join(t.TempDir(), "sample.tar.gz") f, err := os.Create(path) if err != nil { t.Fatalf("create: %v", err) } defer f.Close() w := NewWriter(f) servers, err := w.WriteCollection("servers", [][]byte{[]byte("one"), []byte("two")}) if err != nil { t.Fatalf("WriteCollection: %v", err) } if err := w.WriteIndexes("servers", []byte(`[{"name":"idx"}]`)); err != nil { t.Fatalf("WriteIndexes: %v", err) } keys, err := w.WriteCollection("keys", [][]byte{[]byte("k")}) if err != nil { t.Fatalf("WriteCollection: %v", err) } if err := w.Close(Manifest{ FormatVersion: FormatVersion, CreatedAt: time.Now().UTC(), MongoDB: "vantage", Collections: []CollectionEntry{servers, keys}, }); err != nil { t.Fatalf("Close: %v", err) } return path } func TestWriterRecordsCountsAndChecksums(t *testing.T) { var buf bytes.Buffer w := NewWriter(&buf) e, err := w.WriteCollection("servers", [][]byte{[]byte("one"), []byte("two")}) if err != nil { t.Fatalf("WriteCollection: %v", err) } if e.Name != "servers" { t.Fatalf("name %q", e.Name) } if e.Documents != 2 { t.Fatalf("documents %d, want 2", e.Documents) } if e.Bytes != 6 { t.Fatalf("bytes %d, want 6", e.Bytes) } if len(e.SHA256) != 64 { t.Fatalf("sha256 %q is not 64 hex chars", e.SHA256) } } func TestRoundTrip(t *testing.T) { r, err := Open(writeSampleArchive(t)) if err != nil { t.Fatalf("Open: %v", err) } defer r.Close() if r.Manifest().MongoDB != "vantage" { t.Fatalf("manifest not read back: %+v", r.Manifest()) } rc, err := r.OpenCollection("servers") if err != nil { t.Fatalf("OpenCollection: %v", err) } defer rc.Close() got, err := io.ReadAll(rc) if err != nil { t.Fatalf("read: %v", err) } if string(got) != "onetwo" { t.Fatalf("got %q, want %q", got, "onetwo") } idx, err := r.IndexesJSON("servers") if err != nil { t.Fatalf("IndexesJSON: %v", err) } if string(idx) != `[{"name":"idx"}]` { t.Fatalf("indexes round-tripped as %q", idx) } } func TestIndexesJSONAbsentIsEmptyNotError(t *testing.T) { r, err := Open(writeSampleArchive(t)) if err != nil { t.Fatalf("Open: %v", err) } defer r.Close() idx, err := r.IndexesJSON("keys") if err != nil { t.Fatalf("a collection with no index member must not error: %v", err) } if len(idx) != 0 { t.Fatalf("want empty, got %q", idx) } } func TestOpenRejectsCorruptedMember(t *testing.T) { path := writeSampleArchive(t) // Rewrite the archive with one byte of a collection member flipped, leaving // the manifest's checksum describing the original. corrupt := filepath.Join(t.TempDir(), "corrupt.tar.gz") rewriteFlippingCollectionByte(t, path, corrupt, "servers") if _, err := Open(corrupt); !errors.Is(err, ErrChecksum) { t.Fatalf("got %v, want ErrChecksum", err) } } func TestOpenRejectsUnknownFormatVersion(t *testing.T) { path := filepath.Join(t.TempDir(), "future.tar.gz") f, err := os.Create(path) if err != nil { t.Fatalf("create: %v", err) } w := NewWriter(f) if err := w.Close(Manifest{FormatVersion: 99}); err != nil { t.Fatalf("Close: %v", err) } f.Close() if _, err := Open(path); !errors.Is(err, ErrUnknownFormat) { t.Fatalf("got %v, want ErrUnknownFormat", err) } } func TestCloseRemovesTempDir(t *testing.T) { r, err := Open(writeSampleArchive(t)) if err != nil { t.Fatalf("Open: %v", err) } dir := r.dir if _, err := os.Stat(dir); err != nil { t.Fatalf("temp dir missing while open: %v", err) } if err := r.Close(); err != nil { t.Fatalf("Close: %v", err) } if _, err := os.Stat(dir); !os.IsNotExist(err) { t.Fatalf("temp dir %s survived Close", dir) } } // rewriteFlippingCollectionByte copies an archive, flipping one byte inside the // named collection's .bson member so its content no longer matches the checksum // the manifest recorded. func rewriteFlippingCollectionByte(t *testing.T, src, dst, collection string) { t.Helper() in, err := os.Open(src) if err != nil { t.Fatalf("open src: %v", err) } defer in.Close() gz, err := gzip.NewReader(in) if err != nil { t.Fatalf("gzip: %v", err) } defer gz.Close() out, err := os.Create(dst) if err != nil { t.Fatalf("create dst: %v", err) } defer out.Close() gw := gzip.NewWriter(out) defer gw.Close() tw := tar.NewWriter(gw) defer tw.Close() tr := tar.NewReader(gz) target := "collections/" + collection + ".bson" for { h, err := tr.Next() if err == io.EOF { return } if err != nil { t.Fatalf("tar next: %v", err) } body, err := io.ReadAll(tr) if err != nil { t.Fatalf("read member: %v", err) } if h.Name == target && len(body) > 0 { body[0] ^= 0xFF } h.Size = int64(len(body)) if err := tw.WriteHeader(h); err != nil { t.Fatalf("write header: %v", err) } if _, err := tw.Write(body); err != nil { t.Fatalf("write body: %v", err) } } }