Skip to content
go-libfossil Artifact [dd7ddf6368]
Login

Artifact [dd7ddf6368]

Artifact dd7ddf6368d117cc7f9e8c55f535d4cd47a623d72e01759ae1e2914a8afcff4f:


package sync

import (
	"context"
	"path/filepath"
	"testing"

	"github.com/danmestas/go-libfossil/internal/repo"
	"github.com/danmestas/go-libfossil/internal/uv"
	"github.com/danmestas/go-libfossil/internal/xfer"
	"github.com/danmestas/go-libfossil/simio"
)

// newTestRepoPair creates a server and client repo, both with UV schema.
func newTestRepoPair(t *testing.T) (server, client *repo.Repo) {
	t.Helper()
	dir := t.TempDir()

	sPath := filepath.Join(dir, "server.fossil")
	s, err := repo.Create(sPath, "test", simio.CryptoRand{}, testPC)
	if err != nil {
		t.Fatalf("server repo: %v", err)
	}
	uv.EnsureSchema(s.DB())

	cPath := filepath.Join(dir, "client.fossil")
	c, err := repo.Create(cPath, "test", simio.CryptoRand{}, testPC)
	if err != nil {
		t.Fatalf("client repo: %v", err)
	}
	uv.EnsureSchema(c.DB())

	t.Cleanup(func() {
		s.Close()
		c.Close()
	})
	return s, c
}

func TestSyncUV_PullFromServer(t *testing.T) {
	serverRepo, clientRepo := newTestRepoPair(t)

	// Server has a UV file.
	uv.Write(serverRepo.DB(), "wiki/page.txt", []byte("hello wiki"), 100)

	transport := &MockTransport{Handler: func(req *xfer.Message) *xfer.Message {
		resp, _ := HandleSync(context.Background(), serverRepo, req)
		return resp
	}}
	_, err := Sync(context.Background(), clientRepo, transport, SyncOpts{
		Pull: true, Push: true, UV: true,
		ProjectCode: testPC, ServerCode: "s",
	})
	if err != nil {
		t.Fatalf("Sync: %v", err)
	}

	// Client should now have the file.
	content, mtime, hash, err := uv.Read(clientRepo.DB(), "wiki/page.txt")
	if err != nil {
		t.Fatalf("Read: %v", err)
	}
	if string(content) != "hello wiki" || mtime != 100 || hash == "" {
		t.Errorf("got content=%q mtime=%d hash=%q", content, mtime, hash)
	}
}

func TestSyncUV_PushToServer(t *testing.T) {
	serverRepo, clientRepo := newTestRepoPair(t)
	grantNobodyPush(t, serverRepo)

	// Client has a UV file.
	uv.Write(clientRepo.DB(), "data/config.json", []byte(`{"key":"val"}`), 200)

	transport := &MockTransport{Handler: func(req *xfer.Message) *xfer.Message {
		resp, _ := HandleSync(context.Background(), serverRepo, req)
		return resp
	}}
	_, err := Sync(context.Background(), clientRepo, transport, SyncOpts{
		Pull: true, Push: true, UV: true,
		ProjectCode: testPC, ServerCode: "s",
	})
	if err != nil {
		t.Fatalf("Sync: %v", err)
	}

	// Server should now have the file.
	content, _, _, err := uv.Read(serverRepo.DB(), "data/config.json")
	if err != nil {
		t.Fatalf("Read: %v", err)
	}
	if string(content) != `{"key":"val"}` {
		t.Errorf("server content = %q", content)
	}
}

func TestSyncUV_Bidirectional(t *testing.T) {
	serverRepo, clientRepo := newTestRepoPair(t)
	grantNobodyPush(t, serverRepo)

	uv.Write(serverRepo.DB(), "server-file.txt", []byte("from server"), 100)
	uv.Write(clientRepo.DB(), "client-file.txt", []byte("from client"), 200)

	transport := &MockTransport{Handler: func(req *xfer.Message) *xfer.Message {
		resp, _ := HandleSync(context.Background(), serverRepo, req)
		return resp
	}}
	_, err := Sync(context.Background(), clientRepo, transport, SyncOpts{
		Pull: true, Push: true, UV: true,
		ProjectCode: testPC, ServerCode: "s",
	})
	if err != nil {
		t.Fatalf("Sync: %v", err)
	}

	// Both should have both files.
	sc, _, _, _ := uv.Read(serverRepo.DB(), "client-file.txt")
	cc, _, _, _ := uv.Read(clientRepo.DB(), "server-file.txt")
	if string(sc) != "from client" {
		t.Errorf("server missing client file: %q", sc)
	}
	if string(cc) != "from server" {
		t.Errorf("client missing server file: %q", cc)
	}
}

func TestSyncUV_Deletion(t *testing.T) {
	serverRepo, clientRepo := newTestRepoPair(t)

	// Both have the file initially.
	uv.Write(serverRepo.DB(), "old.txt", []byte("data"), 100)
	uv.Write(clientRepo.DB(), "old.txt", []byte("data"), 100)

	// Server deletes it (newer mtime).
	uv.Delete(serverRepo.DB(), "old.txt", 200)

	transport := &MockTransport{Handler: func(req *xfer.Message) *xfer.Message {
		resp, _ := HandleSync(context.Background(), serverRepo, req)
		return resp
	}}
	_, err := Sync(context.Background(), clientRepo, transport, SyncOpts{
		Pull: true, Push: true, UV: true,
		ProjectCode: testPC, ServerCode: "s",
	})
	if err != nil {
		t.Fatalf("Sync: %v", err)
	}

	// Client should have tombstone.
	_, mtime, hash, _ := uv.Read(clientRepo.DB(), "old.txt")
	if hash != "" {
		t.Errorf("expected tombstone, got hash=%q", hash)
	}
	if mtime != 200 {
		t.Errorf("mtime = %d, want 200", mtime)
	}
}

func TestSyncUV_NoUVFlag_SkipsUV(t *testing.T) {
	serverRepo, clientRepo := newTestRepoPair(t)
	uv.Write(serverRepo.DB(), "test.txt", []byte("data"), 100)

	transport := &MockTransport{Handler: func(req *xfer.Message) *xfer.Message {
		resp, _ := HandleSync(context.Background(), serverRepo, req)
		return resp
	}}
	_, err := Sync(context.Background(), clientRepo, transport, SyncOpts{
		Pull: true, Push: true, UV: false, // UV disabled
		ProjectCode: testPC, ServerCode: "s",
	})
	if err != nil {
		t.Fatalf("Sync: %v", err)
	}

	// Client should NOT have the UV file.
	content, _, _, _ := uv.Read(clientRepo.DB(), "test.txt")
	if content != nil {
		t.Error("UV file should not sync when UV=false")
	}
}

// TestSyncUV_SHA3GateToOldServer pins ยง7.6 on the client: an unversioned file
// named by SHA3 is not sent to a server whose server-version predates SHA3
// support; the refusal fossil's send_unversioned_file sends goes in its place.
func TestSyncUV_SHA3GateToOldServer(t *testing.T) {
	_, clientRepo := newTestRepoPair(t)
	uv.Write(clientRepo.DB(), "u.txt", []byte("unversioned"), 300)
	if _, _, h, err := uv.Read(clientRepo.DB(), "u.txt"); err != nil || len(h) <= 40 {
		t.Fatalf("uv.Read: hash %q, err %v; want a SHA3 name", h, err)
	}
	var requests []*xfer.Message
	transport := &MockTransport{Handler: func(req *xfer.Message) *xfer.Message {
		requests = append(requests, req)
		if len(requests) > 1 {
			return &xfer.Message{}
		}
		return &xfer.Message{Cards: []xfer.Card{
			&xfer.PragmaCard{Name: "server-version", Values: []string{"19999"}},
			&xfer.PragmaCard{Name: "uv-push-ok"},
		}}
	}}
	_, _ = Sync(context.Background(), clientRepo, transport, SyncOpts{
		Pull: true, Push: true, UV: true,
		ProjectCode: testPC, ServerCode: "s",
	})
	if len(requests) < 2 {
		t.Fatalf("client sent %d requests, want a second one", len(requests))
	}
	refused := false
	for _, c := range requests[1].Cards {
		switch c := c.(type) {
		case *xfer.UVFileCard:
			t.Fatalf("uvfile %s sent to a server older than SHA3 support", c.Name)
		case *xfer.ErrorCard:
			refused = refused || c.Message == sha3Refusal().Message
		}
	}
	if !refused {
		t.Fatalf("second request %v carries no SHA3 refusal", requests[1].Cards)
	}
}