diff options
| author | 2026-06-08 13:01:49 +0000 | |
|---|---|---|
| committer | 2026-06-08 13:01:49 +0000 | |
| commit | 7ca63924bf09525c3ee2be27521b009ef8c7783f (patch) | |
| tree | d1628aa6e8523cf7d47b73f3a7ee9700577db732 /object | |
| parent | object/store/loose: Add (diff) | |
| signature | No signature | |
object/store/loose: Add better tests
Diffstat (limited to 'object')
| -rw-r--r-- | object/store/loose/helpers_test.go | 152 | ||||
| -rw-r--r-- | object/store/loose/loose_test.go | 26 | ||||
| -rw-r--r-- | object/store/loose/quarantine_test.go | 116 | ||||
| -rw-r--r-- | object/store/loose/read_test.go | 264 | ||||
| -rw-r--r-- | object/store/loose/roundtrip_test.go | 138 | ||||
| -rw-r--r-- | object/store/loose/write_test.go | 158 |
6 files changed, 854 insertions, 0 deletions
diff --git a/object/store/loose/helpers_test.go b/object/store/loose/helpers_test.go new file mode 100644 index 00000000..22641049 --- /dev/null +++ b/object/store/loose/helpers_test.go @@ -0,0 +1,152 @@ +package loose_test + +import ( + "os" + "strings" + "testing" + + "lindenii.org/go/furgit/internal/testgit" + "lindenii.org/go/furgit/object/header" + "lindenii.org/go/furgit/object/id" + "lindenii.org/go/furgit/object/store/loose" + "lindenii.org/go/furgit/object/typ" +) + +// gitOracleObject is one object created by git, +// paired with its expected content body and full serialized form. +type gitOracleObject struct { + name string + ty typ.Type + id id.ObjectID + body []byte + raw []byte +} + +// openLooseStore opens a loose store over the repository's objects directory. +func openLooseStore(t *testing.T, repo *testgit.Repo) *loose.Loose { + t.Helper() + + repoRoot := repo.Root(t) + + objectsRoot, err := repoRoot.OpenRoot(".git/objects") + if err != nil { + _ = repoRoot.Close() + + t.Fatalf("OpenRoot(.git/objects): %v", err) + } + + _ = repoRoot.Close() + + t.Cleanup(func() { _ = objectsRoot.Close() }) + + looseStore, err := loose.New(objectsRoot, repo.ObjectFormat(t)) + if err != nil { + t.Fatalf("loose.New: %v", err) + } + + return looseStore +} + +// gitOracleObjects builds one object of each kind with git +// and precomputes each one's expected content body and full serialized form. +func gitOracleObjects(t *testing.T, repo *testgit.Repo) []gitOracleObject { + t.Helper() + + blobID, err := repo.HashObject(t, typ.TypeBlob, strings.NewReader("blob body\n")) + if err != nil { + t.Fatalf("HashObject(blob): %v", err) + } + + treeID, err := repo.MkTree(t, []testgit.TreeEntry{ + {Mode: "100644", Type: typ.TypeBlob, OID: blobID, Name: "file"}, + }) + if err != nil { + t.Fatalf("MkTree: %v", err) + } + + commitID, err := repo.CommitTree(t, treeID, testgit.CommitTreeOptions{Message: "subject\n\nbody"}) + if err != nil { + t.Fatalf("CommitTree: %v", err) + } + + tagID, err := repo.TagAnnotated(t, "v1", commitID, testgit.TagAnnotatedOptions{Message: "tag message"}) + if err != nil { + t.Fatalf("TagAnnotated: %v", err) + } + + kinds := []struct { + name string + ty typ.Type + id id.ObjectID + }{ + {name: "blob", ty: typ.TypeBlob, id: blobID}, + {name: "tree", ty: typ.TypeTree, id: treeID}, + {name: "commit", ty: typ.TypeCommit, id: commitID}, + {name: "tag", ty: typ.TypeTag, id: tagID}, + } + + objects := make([]gitOracleObject, 0, len(kinds)) + + for _, kind := range kinds { + body, err := repo.CatFile(t, kind.ty, kind.id) + if err != nil { + t.Fatalf("CatFile(%s): %v", kind.name, err) + } + + raw := header.Append(nil, kind.ty, uint64(len(body))) + raw = append(raw, body...) + + objects = append(objects, gitOracleObject{ + name: kind.name, + ty: kind.ty, + id: kind.id, + body: body, + raw: raw, + }) + } + + return objects +} + +// corruptLooseObjectTrailer flips the final byte of a loose object file, +// damaging the zlib Adler-32 trailer. +func corruptLooseObjectTrailer(t *testing.T, repo *testgit.Repo, objectID id.ObjectID) { + t.Helper() + + root := repo.Root(t) + + defer func() { _ = root.Close() }() + + hex := objectID.String() + relPath := ".git/objects/" + hex[:2] + "/" + hex[2:] + + file, err := root.OpenFile(relPath, os.O_RDWR, 0) + if err != nil { + t.Fatalf("OpenFile(%q): %v", relPath, err) + } + + defer func() { _ = file.Close() }() + + info, err := file.Stat() + if err != nil { + t.Fatalf("Stat(%q): %v", relPath, err) + } + + if info.Size() == 0 { + t.Fatalf("corrupt trailer on empty file %q", relPath) + } + + last := make([]byte, 1) + + _, err = file.ReadAt(last, info.Size()-1) + if err != nil { + t.Fatalf("ReadAt(%q): %v", relPath, err) + } + + last[0] ^= 0xff + + _, err = file.WriteAt(last, info.Size()-1) + if err != nil { + t.Fatalf("WriteAt(%q): %v", relPath, err) + } +} diff --git a/object/store/loose/loose_test.go b/object/store/loose/loose_test.go new file mode 100644 index 00000000..d55c87c5 --- /dev/null +++ b/object/store/loose/loose_test.go @@ -0,0 +1,26 @@ +package loose_test + +import ( + "errors" + "os" + "testing" + + "lindenii.org/go/furgit/object/id" + "lindenii.org/go/furgit/object/store/loose" +) + +func TestNewRejectsUnknownObjectFormat(t *testing.T) { + t.Parallel() + + root, err := os.OpenRoot(t.TempDir()) + if err != nil { + t.Fatalf("OpenRoot: %v", err) + } + + defer func() { _ = root.Close() }() + + _, err = loose.New(root, id.ObjectFormatUnknown) + if !errors.Is(err, id.ErrInvalidObjectFormat) { + t.Fatalf("loose.New(unknown) = %v, want ErrInvalidObjectFormat", err) + } +} diff --git a/object/store/loose/quarantine_test.go b/object/store/loose/quarantine_test.go new file mode 100644 index 00000000..2aab6566 --- /dev/null +++ b/object/store/loose/quarantine_test.go @@ -0,0 +1,116 @@ +package loose_test + +import ( + "bytes" + "errors" + "testing" + + "lindenii.org/go/furgit/internal/testgit" + "lindenii.org/go/furgit/object/id" + "lindenii.org/go/furgit/object/store" + "lindenii.org/go/furgit/object/typ" +) + +func TestQuarantinePromote(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) + if err != nil { + t.Fatalf("NewRepo: %v", err) + } + + looseStore := openLooseStore(t, repo) + + quarantine, err := looseStore.BeginObjectQuarantine(store.ObjectQuarantineOptions{}) + if err != nil { + t.Fatalf("BeginObjectQuarantine: %v", err) + } + + content := []byte("quarantined object\n") + + objectID, err := quarantine.WriteBytesContent(typ.TypeBlob, content) + if err != nil { + t.Fatalf("quarantine.WriteBytesContent: %v", err) + } + + ty, got, err := quarantine.ReadBytesContent(objectID) + if err != nil { + t.Fatalf("quarantine.ReadBytesContent: %v", err) + } + + if ty != typ.TypeBlob { + t.Fatalf("quarantine type = %v, want %v", ty, typ.TypeBlob) + } + + if !bytes.Equal(got, content) { + t.Fatalf("quarantine body mismatch") + } + + _, _, err = looseStore.ReadBytesContent(objectID) + if !errors.Is(err, store.ErrObjectNotFound) { + t.Fatalf("parent saw quarantined object before promote: %v", err) + } + + err = quarantine.Promote() + if err != nil { + t.Fatalf("Promote: %v", err) + } + + ty, got, err = looseStore.ReadBytesContent(objectID) + if err != nil { + t.Fatalf("parent ReadBytesContent after promote: %v", err) + } + + if ty != typ.TypeBlob { + t.Fatalf("parent type = %v, want %v", ty, typ.TypeBlob) + } + + if !bytes.Equal(got, content) { + t.Fatalf("parent body mismatch") + } + }) + } +} + +func TestQuarantineDiscard(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) + if err != nil { + t.Fatalf("NewRepo: %v", err) + } + + looseStore := openLooseStore(t, repo) + + quarantine, err := looseStore.BeginObjectQuarantine(store.ObjectQuarantineOptions{}) + if err != nil { + t.Fatalf("BeginObjectQuarantine: %v", err) + } + + content := []byte("discarded object\n") + + objectID, err := quarantine.WriteBytesContent(typ.TypeBlob, content) + if err != nil { + t.Fatalf("quarantine.WriteBytesContent: %v", err) + } + + err = quarantine.Discard() + if err != nil { + t.Fatalf("Discard: %v", err) + } + + _, _, err = looseStore.ReadBytesContent(objectID) + if !errors.Is(err, store.ErrObjectNotFound) { + t.Fatalf("parent saw discarded object: %v", err) + } + }) + } +} diff --git a/object/store/loose/read_test.go b/object/store/loose/read_test.go new file mode 100644 index 00000000..fe8b8e7c --- /dev/null +++ b/object/store/loose/read_test.go @@ -0,0 +1,264 @@ +package loose_test + +import ( + "bytes" + "errors" + "io" + "strings" + "testing" + + "lindenii.org/go/furgit/internal/testgit" + "lindenii.org/go/furgit/object/id" + "lindenii.org/go/furgit/object/store" + "lindenii.org/go/furgit/object/typ" +) + +func TestRead(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) + if err != nil { + t.Fatalf("NewRepo: %v", err) + } + + objects := gitOracleObjects(t, repo) + looseStore := openLooseStore(t, repo) + + t.Run("BytesFull", func(t *testing.T) { + t.Parallel() + + for _, o := range objects { + got, err := looseStore.ReadBytesFull(o.id) + if err != nil { + t.Fatalf("%s: ReadBytesFull: %v", o.name, err) + } + + if !bytes.Equal(got, o.raw) { + t.Fatalf("%s: ReadBytesFull mismatch", o.name) + } + } + }) + + t.Run("BytesContent", func(t *testing.T) { + t.Parallel() + + for _, o := range objects { + gotType, gotBody, err := looseStore.ReadBytesContent(o.id) + if err != nil { + t.Fatalf("%s: ReadBytesContent: %v", o.name, err) + } + + if gotType != o.ty { + t.Fatalf("%s: ReadBytesContent type = %v, want %v", o.name, gotType, o.ty) + } + + if !bytes.Equal(gotBody, o.body) { + t.Fatalf("%s: ReadBytesContent body mismatch", o.name) + } + } + }) + + t.Run("Header", func(t *testing.T) { + t.Parallel() + + for _, o := range objects { + gotType, gotSize, err := looseStore.ReadHeader(o.id) + if err != nil { + t.Fatalf("%s: ReadHeader: %v", o.name, err) + } + + if gotType != o.ty { + t.Fatalf("%s: ReadHeader type = %v, want %v", o.name, gotType, o.ty) + } + + if gotSize != uint64(len(o.body)) { + t.Fatalf("%s: ReadHeader size = %d, want %d", o.name, gotSize, len(o.body)) + } + } + }) + + t.Run("ReaderFull", func(t *testing.T) { + t.Parallel() + + for _, o := range objects { + reader, err := looseStore.ReadReaderFull(o.id) + if err != nil { + t.Fatalf("%s: ReadReaderFull: %v", o.name, err) + } + + got, err := io.ReadAll(reader) + if err != nil { + _ = reader.Close() + + t.Fatalf("%s: ReadReaderFull ReadAll: %v", o.name, err) + } + + err = reader.Close() + if err != nil { + t.Fatalf("%s: ReadReaderFull Close: %v", o.name, err) + } + + if !bytes.Equal(got, o.raw) { + t.Fatalf("%s: ReadReaderFull mismatch", o.name) + } + } + }) + + t.Run("ReaderContent", func(t *testing.T) { + t.Parallel() + + for _, o := range objects { + gotType, gotSize, reader, err := looseStore.ReadReaderContent(o.id) + if err != nil { + t.Fatalf("%s: ReadReaderContent: %v", o.name, err) + } + + got, err := io.ReadAll(reader) + if err != nil { + _ = reader.Close() + + t.Fatalf("%s: ReadReaderContent ReadAll: %v", o.name, err) + } + + err = reader.Close() + if err != nil { + t.Fatalf("%s: ReadReaderContent Close: %v", o.name, err) + } + + if gotType != o.ty { + t.Fatalf("%s: ReadReaderContent type = %v, want %v", o.name, gotType, o.ty) + } + + if gotSize != uint64(len(o.body)) { + t.Fatalf("%s: ReadReaderContent size = %d, want %d", o.name, gotSize, len(o.body)) + } + + if !bytes.Equal(got, o.body) { + t.Fatalf("%s: ReadReaderContent mismatch", o.name) + } + } + }) + }) + } +} + +func TestReadNotFound(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) + if err != nil { + t.Fatalf("NewRepo: %v", err) + } + + looseStore := openLooseStore(t, repo) + + missingID, err := objectFormat.FromString(strings.Repeat("0", objectFormat.HexLen())) + if err != nil { + t.Fatalf("FromString(missing): %v", err) + } + + _, err = looseStore.ReadBytesFull(missingID) + if !errors.Is(err, store.ErrObjectNotFound) { + t.Fatalf("ReadBytesFull not-found = %v", err) + } + + _, _, err = looseStore.ReadBytesContent(missingID) + if !errors.Is(err, store.ErrObjectNotFound) { + t.Fatalf("ReadBytesContent not-found = %v", err) + } + + _, _, err = looseStore.ReadHeader(missingID) + if !errors.Is(err, store.ErrObjectNotFound) { + t.Fatalf("ReadHeader not-found = %v", err) + } + + _, err = looseStore.ReadReaderFull(missingID) + if !errors.Is(err, store.ErrObjectNotFound) { + t.Fatalf("ReadReaderFull not-found = %v", err) + } + + _, _, _, err = looseStore.ReadReaderContent(missingID) + if !errors.Is(err, store.ErrObjectNotFound) { + t.Fatalf("ReadReaderContent not-found = %v", err) + } + + otherFormat := objectFormat + + for _, candidate := range id.SupportedObjectFormats() { + if candidate != objectFormat { + otherFormat = candidate + + break + } + } + + if otherFormat == objectFormat { + return + } + + mismatchID, err := otherFormat.FromString(strings.Repeat("1", otherFormat.HexLen())) + if err != nil { + t.Fatalf("FromString(mismatch): %v", err) + } + + _, err = looseStore.ReadBytesFull(mismatchID) + if !errors.Is(err, id.ErrInvalidObjectFormat) { + t.Fatalf("ReadBytesFull format mismatch = %v, want ErrInvalidObjectFormat", err) + } + }) + } +} + +func TestReadCorruptTrailer(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) + if err != nil { + t.Fatalf("NewRepo: %v", err) + } + + looseStore := openLooseStore(t, repo) + + content := []byte("corrupt-trailer-check\n") + + objectID, err := looseStore.WriteBytesContent(typ.TypeBlob, content) + if err != nil { + t.Fatalf("WriteBytesContent: %v", err) + } + + corruptLooseObjectTrailer(t, repo, objectID) + + // Stops before the trailer. + ty, size, err := looseStore.ReadHeader(objectID) + if err != nil { + t.Fatalf("ReadHeader: %v", err) + } + + if ty != typ.TypeBlob { + t.Fatalf("ReadHeader type = %v, want %v", ty, typ.TypeBlob) + } + + if size != uint64(len(content)) { + t.Fatalf("ReadHeader size = %d, want %d", size, len(content)) + } + + // Consumes the whole stream. + _, err = looseStore.ReadBytesFull(objectID) + if err == nil { + t.Fatalf("ReadBytesFull on corrupt trailer succeeded") + } + }) + } +} diff --git a/object/store/loose/roundtrip_test.go b/object/store/loose/roundtrip_test.go new file mode 100644 index 00000000..e99da0f2 --- /dev/null +++ b/object/store/loose/roundtrip_test.go @@ -0,0 +1,138 @@ +package loose_test + +import ( + "bytes" + "io" + "testing" + + "lindenii.org/go/furgit/internal/testgit" + "lindenii.org/go/furgit/object/header" + "lindenii.org/go/furgit/object/id" + "lindenii.org/go/furgit/object/typ" +) + +func TestRoundTrip(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + ty typ.Type + content []byte + }{ + {name: "blob", ty: typ.TypeBlob, content: []byte("roundtrip blob\n")}, + {name: "empty blob", ty: typ.TypeBlob, content: []byte{}}, + {name: "tree", ty: typ.TypeTree, content: []byte("roundtrip tree bytes")}, + {name: "commit", ty: typ.TypeCommit, content: []byte("roundtrip commit bytes")}, + {name: "tag", ty: typ.TypeTag, content: []byte("roundtrip tag bytes")}, + } + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) + if err != nil { + t.Fatalf("NewRepo: %v", err) + } + + looseStore := openLooseStore(t, repo) + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + wantRaw := header.Append(nil, tc.ty, uint64(len(tc.content))) + wantRaw = append(wantRaw, tc.content...) + + objectID, err := looseStore.WriteBytesContent(tc.ty, tc.content) + if err != nil { + t.Fatalf("WriteBytesContent: %v", err) + } + + gotRaw, err := looseStore.ReadBytesFull(objectID) + if err != nil { + t.Fatalf("ReadBytesFull: %v", err) + } + + if !bytes.Equal(gotRaw, wantRaw) { + t.Fatalf("ReadBytesFull mismatch") + } + + gotType, gotBody, err := looseStore.ReadBytesContent(objectID) + if err != nil { + t.Fatalf("ReadBytesContent: %v", err) + } + + if gotType != tc.ty { + t.Fatalf("ReadBytesContent type = %v, want %v", gotType, tc.ty) + } + + if !bytes.Equal(gotBody, tc.content) { + t.Fatalf("ReadBytesContent body mismatch") + } + + headType, headSize, err := looseStore.ReadHeader(objectID) + if err != nil { + t.Fatalf("ReadHeader: %v", err) + } + + if headType != tc.ty { + t.Fatalf("ReadHeader type = %v, want %v", headType, tc.ty) + } + + if headSize != uint64(len(tc.content)) { + t.Fatalf("ReadHeader size = %d, want %d", headSize, len(tc.content)) + } + + fullReader, err := looseStore.ReadReaderFull(objectID) + if err != nil { + t.Fatalf("ReadReaderFull: %v", err) + } + + gotFull, err := io.ReadAll(fullReader) + if err != nil { + _ = fullReader.Close() + + t.Fatalf("ReadReaderFull ReadAll: %v", err) + } + + err = fullReader.Close() + if err != nil { + t.Fatalf("ReadReaderFull Close: %v", err) + } + + if !bytes.Equal(gotFull, wantRaw) { + t.Fatalf("ReadReaderFull mismatch") + } + + contentType, contentSize, contentReader, err := looseStore.ReadReaderContent(objectID) + if err != nil { + t.Fatalf("ReadReaderContent: %v", err) + } + + gotContent, err := io.ReadAll(contentReader) + if err != nil { + _ = contentReader.Close() + + t.Fatalf("ReadReaderContent ReadAll: %v", err) + } + + err = contentReader.Close() + if err != nil { + t.Fatalf("ReadReaderContent Close: %v", err) + } + + if contentType != tc.ty { + t.Fatalf("ReadReaderContent type = %v, want %v", contentType, tc.ty) + } + + if contentSize != uint64(len(tc.content)) { + t.Fatalf("ReadReaderContent size = %d, want %d", contentSize, len(tc.content)) + } + + if !bytes.Equal(gotContent, tc.content) { + t.Fatalf("ReadReaderContent mismatch") + } + }) + } + }) + } +} diff --git a/object/store/loose/write_test.go b/object/store/loose/write_test.go new file mode 100644 index 00000000..631b5fbc --- /dev/null +++ b/object/store/loose/write_test.go @@ -0,0 +1,158 @@ +package loose_test + +import ( + "bytes" + "errors" + "testing" + + "lindenii.org/go/furgit/internal/testgit" + "lindenii.org/go/furgit/object/header" + "lindenii.org/go/furgit/object/id" + "lindenii.org/go/furgit/object/store" + "lindenii.org/go/furgit/object/store/loose" + "lindenii.org/go/furgit/object/typ" +) + +func TestWrite(t *testing.T) { + t.Parallel() + + writes := []struct { + name string + write func(looseStore *loose.Loose, content []byte) (id.ObjectID, error) + }{ + { + name: "BytesContent", + write: func(looseStore *loose.Loose, content []byte) (id.ObjectID, error) { + return looseStore.WriteBytesContent(typ.TypeBlob, content) + }, + }, + { + name: "ReaderContent", + write: func(looseStore *loose.Loose, content []byte) (id.ObjectID, error) { + return looseStore.WriteReaderContent(typ.TypeBlob, uint64(len(content)), bytes.NewReader(content)) + }, + }, + { + name: "BytesFull", + write: func(looseStore *loose.Loose, content []byte) (id.ObjectID, error) { + raw := header.Append(nil, typ.TypeBlob, uint64(len(content))) + raw = append(raw, content...) + + return looseStore.WriteBytesFull(raw) + }, + }, + { + name: "ReaderFull", + write: func(looseStore *loose.Loose, content []byte) (id.ObjectID, error) { + raw := header.Append(nil, typ.TypeBlob, uint64(len(content))) + raw = append(raw, content...) + + return looseStore.WriteReaderFull(bytes.NewReader(raw)) + }, + }, + } + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) + if err != nil { + t.Fatalf("NewRepo: %v", err) + } + + looseStore := openLooseStore(t, repo) + + for _, w := range writes { + t.Run(w.name, func(t *testing.T) { + content := []byte("written via " + w.name + "\n") + + want, err := repo.HashObject(t, typ.TypeBlob, bytes.NewReader(content)) + if err != nil { + t.Fatalf("HashObject: %v", err) + } + + got, err := w.write(looseStore, content) + if err != nil { + t.Fatalf("write: %v", err) + } + + if got != want { + t.Fatalf("id = %s, want %s", got, want) + } + + gotBody, err := repo.CatFile(t, typ.TypeBlob, got) + if err != nil { + t.Fatalf("CatFile: %v", err) + } + + if !bytes.Equal(gotBody, content) { + t.Fatalf("git cat-file body mismatch") + } + + regot, err := w.write(looseStore, content) + if err != nil { + t.Fatalf("rewrite: %v", err) + } + + if regot != want { + t.Fatalf("rewrite id = %s, want %s", regot, want) + } + }) + } + }) + } +} + +func TestWriteRejects(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) + if err != nil { + t.Fatalf("NewRepo: %v", err) + } + + looseStore := openLooseStore(t, repo) + + t.Run("ContentOverflow", func(t *testing.T) { + t.Parallel() + + _, err := looseStore.WriteReaderContent(typ.TypeBlob, 1, bytes.NewReader([]byte("hello"))) + if !errors.Is(err, store.ErrInvalidObject) { + t.Fatalf("err = %v, want ErrInvalidObject", err) + } + }) + + t.Run("ContentShort", func(t *testing.T) { + t.Parallel() + + _, err := looseStore.WriteReaderContent(typ.TypeBlob, 5, bytes.NewReader([]byte("x"))) + if !errors.Is(err, store.ErrInvalidObject) { + t.Fatalf("err = %v, want ErrInvalidObject", err) + } + }) + + t.Run("FullMalformedHeader", func(t *testing.T) { + t.Parallel() + + _, err := looseStore.WriteReaderFull(bytes.NewReader([]byte("not-a-header"))) + if !errors.Is(err, store.ErrInvalidObject) { + t.Fatalf("err = %v, want ErrInvalidObject", err) + } + }) + + t.Run("FullSizeMismatch", func(t *testing.T) { + t.Parallel() + + _, err := looseStore.WriteReaderFull(bytes.NewReader([]byte("blob 1\x00hello"))) + if !errors.Is(err, store.ErrInvalidObject) { + t.Fatalf("err = %v, want ErrInvalidObject", err) + } + }) + }) + } +} |
