Skip to content
go-libfossil Artifact [ac031b1230]
Login

Artifact [ac031b1230]

Artifact ac031b1230a102b453495766fa32fd380162aa38d557adf1e682bbfe093df5f6:


package sync

import (
	"context"
	"errors"
	"strings"
	"testing"
	"time"

	"github.com/danmestas/go-libfossil/db"
	"github.com/danmestas/go-libfossil/internal/manifest"

	"github.com/danmestas/go-libfossil/internal/blob"
	"github.com/danmestas/go-libfossil/internal/cone"
	"github.com/danmestas/go-libfossil/internal/content"
	"github.com/danmestas/go-libfossil/internal/deck"
	"github.com/danmestas/go-libfossil/internal/delta"
	"github.com/danmestas/go-libfossil/internal/hash"
	"github.com/danmestas/go-libfossil/internal/xfer"
)

func requiredLeaf(t *testing.T, name string) ([]byte, string) {
	t.Helper()
	data, err := (&deck.Deck{F: []deck.FileCard{{Name: name, UUID: hash.SHA3([]byte(name))}}}).Marshal()
	if err != nil {
		t.Fatal(err)
	}
	return data, hash.SHA3(data)
}

func TestFetchRequiredArtifactsNoPeer(t *testing.T) {
	r := setupSyncTestRepo(t)
	_, uuid := requiredLeaf(t, "outside/a")
	if _, err := blob.StorePhantom(r.DB(), uuid); err != nil {
		t.Fatal(err)
	}
	_, err := FetchRequiredArtifacts(context.Background(), r, nil, []string{uuid}, SyncOpts{})
	if err == nil || !strings.Contains(err.Error(), "elided") || !strings.Contains(err.Error(), "no configured peer") || strings.Contains(err.Error(), "missing") || strings.Contains(err.Error(), "corrupt") {
		t.Fatalf("no-peer diagnostic: %v", err)
	}
}

func TestFetchRequiredArtifactsExactDeltaClosure(t *testing.T) {
	r := setupSyncTestRepo(t)
	c, err := cone.New([]string{"inside"}, nil)
	if err != nil {
		t.Fatal(err)
	}
	if err := content.SetStoreCone(r.DB(), c); err != nil {
		t.Fatal(err)
	}
	leaf, uuid := requiredLeaf(t, "outside/a")
	basis := []byte("a transfer-only basis, not structural content")
	basisUUID := hash.SHA3(basis)
	diff, err := delta.Create(basis, leaf)
	if err != nil {
		t.Fatal(err)
	}
	unwanted := []byte("outside file payload must never be stored")
	unwantedUUID := hash.SHA3(unwanted)
	calls := 0
	peer := &MockTransport{Handler: func(req *xfer.Message) *xfer.Message {
		calls++
		resp := &xfer.Message{Cards: []xfer.Card{&xfer.FileCard{UUID: unwantedUUID, Content: unwanted}}}
		for _, card := range req.Cards {
			if p, ok := card.(*xfer.PragmaCard); ok && (p.Name == "cone" || p.Name == "cone-not") {
				t.Fatal("targeted request inherited the store cone")
			}
			if g, ok := card.(*xfer.GimmeCard); ok {
				switch g.UUID {
				case uuid:
					resp.Cards = append(resp.Cards, &xfer.FileCard{UUID: uuid, DeltaSrc: basisUUID, Content: diff})
				case basisUUID:
					resp.Cards = append(resp.Cards, &xfer.FileCard{UUID: basisUUID, Content: basis})
				default:
					t.Fatalf("unrelated gimme %s", g.UUID)
				}
			}
		}
		return resp
	}}
	result, err := FetchRequiredArtifacts(context.Background(), r, peer, []string{uuid}, SyncOpts{})
	if err != nil {
		t.Fatal(err)
	}
	if calls != 2 || result.FilesRecvd != 1 {
		t.Fatalf("calls=%d received=%d, want 2/1", calls, result.FilesRecvd)
	}
	if rid, ok := content.AvailableByUUID(r.DB(), uuid); !ok {
		t.Fatal("required submanifest not recovered")
	} else if got, err := content.Expand(r.DB(), rid); err != nil || string(got) != string(leaf) {
		t.Fatalf("recovered content: %q, %v", got, err)
	}
	for _, outside := range []string{basisUUID, unwantedUUID} {
		if _, ok := blob.Exists(r.DB(), outside); ok {
			t.Fatalf("transfer-only or unrequested content escaped: %s", outside)
		}
	}
	if got, err := content.StoreCone(r.DB()); err != nil || got == nil || !got.Equal(c) {
		t.Fatalf("targeted recovery changed policy: %v, %v", got, err)
	}
}

func TestFetchRequiredArtifactsParentBaselineTree(t *testing.T) {
	source, target := setupSyncTestRepo(t), setupSyncTestRepo(t)
	inside, insideUUID := requiredLeaf(t, "inside/a")
	outside, outsideUUID := requiredLeaf(t, "outside/a")
	for _, data := range [][]byte{inside, outside} {
		if _, _, err := blob.Store(source.DB(), data); err != nil {
			t.Fatal(err)
		}
	}
	baseline, err := (&deck.Deck{C: "baseline tree", D: time.Unix(1, 0).UTC(), U: deck.User("test"), S: []deck.SubmanifestCard{{Name: "inside/a", UUID: insideUUID}, {Name: "outside/a", UUID: outsideUUID}}}).Marshal()
	if err != nil {
		t.Fatal(err)
	}
	_, baseUUID, err := blob.Store(source.DB(), baseline)
	if err != nil {
		t.Fatal(err)
	}
	parent, err := (&deck.Deck{C: "delta parent", D: time.Unix(2, 0).UTC(), U: deck.User("test"), B: baseUUID, F: []deck.FileCard{{Name: "inside/a", UUID: hash.SHA3([]byte("edited"))}}}).Marshal()
	if err != nil {
		t.Fatal(err)
	}
	parentRID, parentUUID, err := blob.Store(target.DB(), parent)
	if err != nil {
		t.Fatal(err)
	}
	if _, _, err := blob.Store(target.DB(), inside); err != nil {
		t.Fatal(err)
	}
	if _, err := blob.StorePhantom(target.DB(), outsideUUID); err != nil {
		t.Fatal(err)
	}
	c, err := cone.New([]string{"inside"}, nil)
	if err != nil {
		t.Fatal(err)
	}
	if err := content.SetStoreCone(target.DB(), c); err != nil {
		t.Fatal(err)
	}
	peer := &MockTransport{Handler: func(req *xfer.Message) *xfer.Message {
		// Fetch must not hold a publishing transaction during network I/O.
		if err := target.WithTx(func(tx *db.Tx) error {
			_, err := tx.Exec("SELECT count(*) FROM blob")
			return err
		}); err != nil {
			t.Fatal(err)
		}
		reply, err := HandleSync(context.Background(), source, req)
		if err != nil {
			t.Fatal(err)
		}
		return reply
	}}
	result, err := FetchRequiredArtifacts(context.Background(), target, peer, []string{parentUUID}, SyncOpts{})
	if err != nil {
		t.Fatal(err)
	}
	if result.FilesRecvd != 2 {
		t.Fatalf("received %d nodes, want baseline + absent S sibling", result.FilesRecvd)
	}
	files, err := manifest.ListFiles(target, parentRID)
	if err != nil || len(files) != 2 || files[0].UUID != hash.SHA3([]byte("edited")) || files[1].UUID != hash.SHA3([]byte("outside/a")) {
		t.Fatalf("delta-over-tree recovery: %+v, %v", files, err)
	}
	if _, held := content.AvailableByUUID(target.DB(), hash.SHA3([]byte("outside/a"))); held {
		t.Fatal("recovery fetched outside F payload")
	}
	if got, _ := content.StoreCone(target.DB()); got == nil || !got.Equal(c) {
		t.Fatal("recovery changed stored cone")
	}
}

func TestFetchRequiredArtifactsWrongProject(t *testing.T) {
	r := setupSyncTestRepo(t)
	data, uuid := requiredLeaf(t, "outside/a")
	peer := &MockTransport{Handler: func(*xfer.Message) *xfer.Message {
		return &xfer.Message{Cards: []xfer.Card{&xfer.FileCard{UUID: uuid, Content: data}, &xfer.PushCard{ProjectCode: "wrong"}}}
	}}
	if _, err := FetchRequiredArtifacts(context.Background(), r, peer, []string{uuid}, SyncOpts{}); err == nil || !strings.Contains(err.Error(), "wrong project") {
		t.Fatalf("wrong peer accepted: %v", err)
	}
	if _, held := blob.Exists(r.DB(), uuid); held {
		t.Fatal("wrong project leaked content")
	}
}

func TestFetchRequiredArtifactsFailureAtomic(t *testing.T) {
	for _, mode := range []string{"incomplete", "corrupt", "shunned", "private", "cancelled"} {
		t.Run(mode, func(t *testing.T) {
			r := setupSyncTestRepo(t)
			first, firstUUID := requiredLeaf(t, "outside/a")
			second, secondUUID := requiredLeaf(t, "outside/b")
			if mode == "shunned" {
				if _, err := r.DB().Exec("INSERT INTO shun(uuid) VALUES(?)", secondUUID); err != nil {
					t.Fatal(err)
				}
			}
			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()
			calls := 0
			peer := &MockTransport{Handler: func(req *xfer.Message) *xfer.Message {
				calls++
				cards := []xfer.Card{&xfer.FileCard{UUID: firstUUID, Content: first}}
				switch mode {
				case "corrupt":
					cards = append(cards, &xfer.FileCard{UUID: secondUUID, Content: []byte("not its hash")})
				case "private":
					cards = append(cards, &xfer.PrivateCard{}, &xfer.FileCard{UUID: secondUUID, Content: second})
				case "cancelled":
					cancel()
				}
				return &xfer.Message{Cards: cards}
			}}
			_, err := FetchRequiredArtifacts(ctx, r, peer, []string{firstUUID, secondUUID}, SyncOpts{})
			if err == nil {
				t.Fatal("invalid recovery succeeded")
			}
			if mode == "cancelled" && !errors.Is(err, context.Canceled) {
				t.Fatalf("cancellation not preserved: %v", err)
			}
			if mode == "shunned" && calls != 0 {
				t.Fatal("requested shunned content")
			}
			for _, uuid := range []string{firstUUID, secondUUID} {
				if _, ok := blob.Exists(r.DB(), uuid); ok {
					t.Fatalf("failed recovery leaked %s", uuid)
				}
			}
		})
	}
}