gitmote

gitmote / internal / store / conformance_test.go  ·  history  ·  branches

4012 bytes · raw

package store

import (
	"bytes"
	"context"
	"errors"
	"io"
	"slices"
	"testing"
)

// testConformance is the shared Store contract suite. Every implementation
// must pass it — mem and s3 stay in lockstep by construction. newStore must
// return an empty store with an isolated keyspace.
func testConformance(t *testing.T, newStore func(t *testing.T) Store) {
	ctx := context.Background()

	put := func(t *testing.T, s Store, key string, content []byte) {
		t.Helper()
		if err := s.Put(ctx, key, bytes.NewReader(content)); err != nil {
			t.Fatalf("Put(%s): %v", key, err)
		}
	}
	get := func(t *testing.T, s Store, key string) []byte {
		t.Helper()
		rc, err := s.Get(ctx, key)
		if err != nil {
			t.Fatalf("Get(%s): %v", key, err)
		}
		defer rc.Close()
		content, err := io.ReadAll(rc)
		if err != nil {
			t.Fatalf("read Get(%s) body: %v", key, err)
		}
		return content
	}

	t.Run("GetMissing", func(t *testing.T) {
		s := newStore(t)
		_, err := s.Get(ctx, "repo/objects/ab/cdef")
		if !errors.Is(err, ErrNotFound) {
			t.Errorf("Get(missing) error = %v, want ErrNotFound", err)
		}
	})

	t.Run("ExistsMissing", func(t *testing.T) {
		s := newStore(t)
		ok, err := s.Exists(ctx, "repo/objects/ab/cdef")
		if err != nil {
			t.Fatalf("Exists: %v", err)
		}
		if ok {
			t.Error("Exists(missing) = true, want false")
		}
	})

	t.Run("PutGetRoundtrip", func(t *testing.T) {
		s := newStore(t)
		// Git objects are binary: zlib streams with NUL and high bytes.
		content := []byte("blob 12\x00\x00\x01\xff\xfebinary data")
		put(t, s, "repo/objects/ab/cdef", content)

		if got := get(t, s, "repo/objects/ab/cdef"); !bytes.Equal(got, content) {
			t.Errorf("Get returned %q, want %q", got, content)
		}
		ok, err := s.Exists(ctx, "repo/objects/ab/cdef")
		if err != nil {
			t.Fatalf("Exists: %v", err)
		}
		if !ok {
			t.Error("Exists(present) = false, want true")
		}
	})

	t.Run("RePutIsNoOp", func(t *testing.T) {
		s := newStore(t)
		content := []byte("same content, same hash")
		put(t, s, "repo/objects/ab/cdef", content)
		put(t, s, "repo/objects/ab/cdef", content)

		if got := get(t, s, "repo/objects/ab/cdef"); !bytes.Equal(got, content) {
			t.Errorf("Get after re-Put returned %q, want %q", got, content)
		}
		keys, err := s.List(ctx, "repo/")
		if err != nil {
			t.Fatalf("List: %v", err)
		}
		if want := []string{"repo/objects/ab/cdef"}; !slices.Equal(keys, want) {
			t.Errorf("List after re-Put = %v, want %v", keys, want)
		}
	})

	t.Run("ExistsIsNotPrefixMatch", func(t *testing.T) {
		s := newStore(t)
		put(t, s, "repo/objects/ab/cdef", []byte("x"))

		ok, err := s.Exists(ctx, "repo/objects/ab")
		if err != nil {
			t.Fatalf("Exists: %v", err)
		}
		if ok {
			t.Error(`Exists("repo/objects/ab") = true, want false — a key is not its prefix`)
		}
	})

	t.Run("ListPrefixBoundary", func(t *testing.T) {
		s := newStore(t)
		for _, key := range []string{
			"repo1/objects/pack/pack-1.pack",
			"repo1/objects/aa/111",
			"repo10/objects/bb/222",
		} {
			put(t, s, key, []byte(key))
		}

		// "repo1/" must not match "repo10/…": the slash is the boundary.
		keys, err := s.List(ctx, "repo1/")
		if err != nil {
			t.Fatalf("List: %v", err)
		}
		want := []string{"repo1/objects/aa/111", "repo1/objects/pack/pack-1.pack"}
		if !slices.Equal(keys, want) {
			t.Errorf("List(repo1/) = %v, want %v (sorted)", keys, want)
		}

		keys, err = s.List(ctx, "repo1/objects/pack/")
		if err != nil {
			t.Fatalf("List: %v", err)
		}
		if want := []string{"repo1/objects/pack/pack-1.pack"}; !slices.Equal(keys, want) {
			t.Errorf("List(repo1/objects/pack/) = %v, want %v", keys, want)
		}
	})

	t.Run("ListNoMatch", func(t *testing.T) {
		s := newStore(t)
		put(t, s, "repo/objects/ab/cdef", []byte("x"))

		keys, err := s.List(ctx, "other/")
		if err != nil {
			t.Fatalf("List: %v", err)
		}
		if len(keys) != 0 {
			t.Errorf("List(other/) = %v, want empty", keys)
		}
	})
}

func TestMemConformance(t *testing.T) {
	testConformance(t, func(t *testing.T) Store { return NewMem() })
}