package loose_test import ( "bytes" "errors" "os" "strings" "testing" "codeberg.org/lindenii/furgit/internal/testgit" objectid "codeberg.org/lindenii/furgit/object/id" objectstore "codeberg.org/lindenii/furgit/object/store" "codeberg.org/lindenii/furgit/object/store/loose" objecttype "codeberg.org/lindenii/furgit/object/type" ) func TestLooseStoreReadAgainstGit(t *testing.T) { t.Parallel() testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) blobID := testRepo.HashObject(t, "blob", []byte("blob body\n")) _, treeID, commitID := testRepo.MakeCommit(t, "subject\n\nbody") tagID := testRepo.TagAnnotated(t, "v1", commitID, "tag message") store := openLooseStore(t, testRepo, algo) tests := []struct { name string id objectid.ObjectID }{ {name: "blob", id: blobID}, {name: "tree", id: treeID}, {name: "commit", id: commitID}, {name: "tag", id: tagID}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { wantType, wantBody, wantRaw := expectedRawObject(t, testRepo, tt.id) gotRaw, err := store.ReadBytesFull(tt.id) if err != nil { t.Fatalf("ReadBytesFull: %v", err) } if !bytes.Equal(gotRaw, wantRaw) { t.Fatalf("ReadBytesFull mismatch") } gotType, gotBody, err := store.ReadBytesContent(tt.id) if err != nil { t.Fatalf("ReadBytesContent: %v", err) } if gotType != wantType { t.Fatalf("ReadBytesContent type = %v, want %v", gotType, wantType) } if !bytes.Equal(gotBody, wantBody) { t.Fatalf("ReadBytesContent body mismatch") } headType, headSize, err := store.ReadHeader(tt.id) if err != nil { t.Fatalf("ReadHeader: %v", err) } if headType != wantType { t.Fatalf("ReadHeader type = %v, want %v", headType, wantType) } if headSize != int64(len(wantBody)) { t.Fatalf("ReadHeader size = %d, want %d", headSize, len(wantBody)) } fullReader, err := store.ReadReaderFull(tt.id) if err != nil { t.Fatalf("ReadReaderFull: %v", err) } got := mustReadAllAndClose(t, fullReader) if !bytes.Equal(got, wantRaw) { t.Fatalf("ReadReaderFull stream mismatch") } contentType, contentSize, contentReader, err := store.ReadReaderContent(tt.id) if err != nil { t.Fatalf("ReadReaderContent: %v", err) } if contentType != wantType { t.Fatalf("ReadReaderContent type = %v, want %v", contentType, wantType) } if contentSize != int64(len(wantBody)) { t.Fatalf("ReadReaderContent size = %d, want %d", contentSize, len(wantBody)) } got = mustReadAllAndClose(t, contentReader) if !bytes.Equal(got, wantBody) { t.Fatalf("ReadReaderContent stream mismatch") } }) } }) } func TestLooseStoreErrors(t *testing.T) { t.Parallel() testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) store := openLooseStore(t, testRepo, algo) notFoundID, err := objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen())) if err != nil { t.Fatalf("ParseHex(notFoundID): %v", err) } _, err = store.ReadBytesFull(notFoundID) if !errors.Is(err, objectstore.ErrObjectNotFound) { t.Fatalf("ReadBytesFull not-found error = %v", err) } _, _, err = store.ReadBytesContent(notFoundID) if !errors.Is(err, objectstore.ErrObjectNotFound) { t.Fatalf("ReadBytesContent not-found error = %v", err) } _, err = store.ReadReaderFull(notFoundID) if !errors.Is(err, objectstore.ErrObjectNotFound) { t.Fatalf("ReadReaderFull not-found error = %v", err) } _, _, _, err = store.ReadReaderContent(notFoundID) if !errors.Is(err, objectstore.ErrObjectNotFound) { t.Fatalf("ReadReaderContent not-found error = %v", err) } _, _, err = store.ReadHeader(notFoundID) if !errors.Is(err, objectstore.ErrObjectNotFound) { t.Fatalf("ReadHeader not-found error = %v", err) } var otherAlgo objectid.Algorithm if algo == objectid.AlgorithmSHA1 { otherAlgo = objectid.AlgorithmSHA256 } else { otherAlgo = objectid.AlgorithmSHA1 } otherID, err := objectid.ParseHex(otherAlgo, strings.Repeat("1", otherAlgo.HexLen())) if err != nil { t.Fatalf("ParseHex(otherID): %v", err) } _, err = store.ReadBytesFull(otherID) if err == nil || !strings.Contains(err.Error(), "algorithm mismatch") { t.Fatalf("ReadBytesFull algorithm-mismatch error = %v", err) } }) } func TestLooseStoreNewValidation(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, objectid.AlgorithmUnknown) if err == nil { t.Fatalf("loose.New(root, unknown) expected error") } } func TestLooseStoreReadHeaderDoesNotVerifyAdler32(t *testing.T) { t.Parallel() testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) store := openLooseStore(t, testRepo, algo) content := []byte("header-only-check\n") id, err := store.WriteBytesContent(objecttype.TypeBlob, content) if err != nil { t.Fatalf("WriteBytesContent: %v", err) } corruptLooseObjectTrailer(t, testRepo, id) ty, size, err := store.ReadHeader(id) if err != nil { t.Fatalf("ReadHeader: %v", err) } if ty != objecttype.TypeBlob { t.Fatalf("ReadHeader type = %v, want %v", ty, objecttype.TypeBlob) } if size != int64(len(content)) { t.Fatalf("ReadHeader size = %d, want %d", size, len(content)) } _, err = store.ReadBytesFull(id) if err == nil { t.Fatalf("ReadBytesFull on corrupted trailer succeeded") } }) }