Artifact b8a4de374d3e4658984c52156d26edb84a54d75133df520e398a361f40c372cb:
- File
internal/sync/cluster_integration_test.go
— part of check-in
[821cf115ce]
at
2026-10-12 22:48:22
on branch main
— fix(sync): refuse push and pull cards naming another project (#309) (#353)
fossil's page_xfer compares a push or pull card's PROJECTCODE with the repository's project-code before any capability check and, on a mismatch, resets the reply, answers "error wrong project" and breaks out of the card loop (section 8.5). The handler accepted either card whatever its project code.
The refusal goes through a new breakLoop, which the undecodable file card path now shares: the reply is reset to the one error, no later card is processed, and the post-loop work and timestamp still follow. Tests that synced repositories created with different project codes now create them with a shared one, and the DST simulator gives its leaves the upstream's. (user: [email protected], size: 5535) [annotate] [blame] [check-ins using]
package sync import ( "context" "fmt" "path/filepath" "testing" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/repo" _ "github.com/danmestas/go-libfossil/internal/testdriver" "github.com/danmestas/go-libfossil/internal/xfer" "github.com/danmestas/go-libfossil/simio" ) // TestPushOnly_DrainsServerGimmes verifies that when the client pushes // without pulling (SyncOpts{Push:true, Pull:false}), the server still // requests blobs it does not have via gimme cards and the client services // them across multiple rounds. // // Regression: in v0.4.0 the server only emitted gimme cards when pullOK // was true (driven by the client's pull card). Push-only clients exited // after one round even when the server was missing artifacts, leaving // the server with an incomplete blob store. func TestPushOnly_DrainsServerGimmes(t *testing.T) { clientPath := filepath.Join(t.TempDir(), "client.fossil") clientRepo, err := repo.Create(clientPath, "testuser", simio.CryptoRand{}, testPC) if err != nil { t.Fatalf("repo.Create client: %v", err) } t.Cleanup(func() { clientRepo.Close() }) const numBlobs = 50 for i := range numBlobs { content := []byte(fmt.Sprintf("push-only-blob-%04d", i)) if _, _, err := blob.Store(clientRepo.DB(), content); err != nil { t.Fatalf("blob.Store[%d]: %v", i, err) } } serverPath := filepath.Join(t.TempDir(), "server.fossil") serverRepo, err := repo.Create(serverPath, "testuser", simio.CryptoRand{}, testPC) if err != nil { t.Fatalf("repo.Create server: %v", err) } t.Cleanup(func() { serverRepo.Close() }) grantNobodyPush(t, serverRepo) transport := &MockTransport{ Handler: func(req *xfer.Message) *xfer.Message { resp, err := HandleSync(context.Background(), serverRepo, req) if err != nil { t.Fatalf("HandleSync: %v", err) } return resp }, } var projCode, srvCode string clientRepo.DB().QueryRow("SELECT value FROM config WHERE name='project-code'").Scan(&projCode) clientRepo.DB().QueryRow("SELECT value FROM config WHERE name='server-code'").Scan(&srvCode) result, err := Sync(context.Background(), clientRepo, transport, SyncOpts{ Push: true, Pull: false, ProjectCode: projCode, ServerCode: srvCode, }) if err != nil { t.Fatalf("Sync: %v", err) } var serverCount int if err := serverRepo.DB().QueryRow("SELECT count(*) FROM blob WHERE size >= 0").Scan(&serverCount); err != nil { t.Fatalf("count blobs: %v", err) } t.Logf("push-only sync completed: rounds=%d sent=%d server_blobs=%d", result.Rounds, result.FilesSent, serverCount) if serverCount < numBlobs { t.Fatalf("server has %d blobs, want >= %d (push-only did not drain server gimmes)", serverCount, numBlobs) } if result.FilesSent < numBlobs { t.Fatalf("FilesSent = %d, want >= %d (multi-round push-only failed to send all)", result.FilesSent, numBlobs) } // Multi-round behaviour: the protocol needs at least 2 rounds (igot then file). if result.Rounds < 2 { t.Fatalf("Rounds = %d, want >= 2 (push-only converged before draining gimmes)", result.Rounds) } } // TestClusterSync_RoundTrip creates a client repo with 200 blobs, syncs them // to an empty server repo via MockTransport wrapping HandleSync, and verifies // all 200 blobs arrive within 10 rounds. func TestClusterSync_RoundTrip(t *testing.T) { // --- Setup: client repo with 200 blobs --- clientPath := filepath.Join(t.TempDir(), "client.fossil") clientRepo, err := repo.Create(clientPath, "testuser", simio.CryptoRand{}, testPC) if err != nil { t.Fatalf("repo.Create client: %v", err) } t.Cleanup(func() { clientRepo.Close() }) for i := range 200 { content := []byte(fmt.Sprintf("cluster-roundtrip-blob-%04d", i)) _, _, err := blob.Store(clientRepo.DB(), content) if err != nil { t.Fatalf("blob.Store[%d]: %v", i, err) } } // --- Setup: empty server repo --- serverPath := filepath.Join(t.TempDir(), "server.fossil") serverRepo, err := repo.Create(serverPath, "testuser", simio.CryptoRand{}, testPC) if err != nil { t.Fatalf("repo.Create server: %v", err) } t.Cleanup(func() { serverRepo.Close() }) grantNobodyPush(t, serverRepo) // --- Transport: MockTransport wrapping HandleSync --- transport := &MockTransport{ Handler: func(req *xfer.Message) *xfer.Message { resp, err := HandleSync(context.Background(), serverRepo, req) if err != nil { t.Fatalf("HandleSync: %v", err) } return resp }, } // --- Sync: client pushes to server --- var projCode, srvCode string clientRepo.DB().QueryRow("SELECT value FROM config WHERE name='project-code'").Scan(&projCode) clientRepo.DB().QueryRow("SELECT value FROM config WHERE name='server-code'").Scan(&srvCode) result, err := Sync(context.Background(), clientRepo, transport, SyncOpts{ Push: true, Pull: true, ProjectCode: projCode, ServerCode: srvCode, }) if err != nil { t.Fatalf("Sync: %v", err) } // --- Verify: all 200 blobs arrived --- var serverCount int err = serverRepo.DB().QueryRow("SELECT count(*) FROM blob WHERE size >= 0").Scan(&serverCount) if err != nil { t.Fatalf("count blobs: %v", err) } t.Logf("sync completed: rounds=%d sent=%d recv=%d server_blobs=%d", result.Rounds, result.FilesSent, result.FilesRecvd, serverCount) if serverCount < 200 { t.Fatalf("server has %d blobs, want >= 200", serverCount) } // --- Verify: converged within 10 rounds --- if result.Rounds > 10 { t.Fatalf("took %d rounds, want <= 10", result.Rounds) } }