Artifact 704f7aada4066cfbb33d7f7833d037ed3bb5b1219288a39e04b8b86fda04d33e:
- File
dst/shun_private_test.go
— part of check-in
[6b66ea6b2d]
at
2026-05-12 20:01:20
on branch trunk
— feat: CreateOpts.ProjectCode — accept explicit project-code on Create (#31)
* feat: CreateOpts.ProjectCode — accept explicit project-code on Create
Downstream EdgeSync coordinates a shared project-code across sibling Fossil repos created on different machines so its NATS sync subject ("<prefix>.<project-code>.sync") matches across leaves. Today every Create generates a fresh project-code, so commits never propagate.
Add CreateOpts.ProjectCode: empty preserves current behavior; non-empty must be 40-char lowercase hex (^[0-9a-f]{40}$) — the same format generated project-codes use. Validation runs at the top of libfossil.Create before any disk I/O, so invalid input cannot leave a half-written file behind. Server-code stays auto-generated.
Threaded through repo.Create / repo.CreateWithEnv / db.SeedConfig. SeedConfig validates as defense-in-depth (public package, multiple callers possible).
Clone accepting ProjectCode is out of scope; will follow separately if EdgeSync needs it.
* test: round-trip explicit ProjectCode through fossil rebuild
Asserts that vanilla `fossil` (2.28) accepts a Go-side repo created with an explicit ProjectCode, and that `fossil sql` reads back the same value we set. Closes the gap where the existing TestCreate_FossilValidation only covered the generated-project-code path. (user: [email protected], size: 6610) [annotate] [blame] [check-ins using]
package dst import ( "context" "path/filepath" "testing" "github.com/danmestas/libfossil/internal/blob" "github.com/danmestas/libfossil/internal/content" "github.com/danmestas/libfossil/internal/repo" "github.com/danmestas/libfossil/internal/shun" libsync "github.com/danmestas/libfossil/internal/sync" "github.com/danmestas/libfossil/simio" _ "github.com/danmestas/libfossil/internal/testdriver" ) // TestShunnedBlobDoesNotPropagate verifies that shunned and private blobs // are excluded from sync, while normal blobs propagate correctly. func TestShunnedBlobDoesNotPropagate(t *testing.T) { tmpDir := t.TempDir() // Create server repo and seed blobs. serverPath := filepath.Join(tmpDir, "server.fossil") serverRepo, err := repo.Create(serverPath, "testuser", simio.CryptoRand{}, "") if err != nil { t.Fatalf("create server repo: %v", err) } t.Cleanup(func() { serverRepo.Close() }) // Store normal blob. _, normalUUID, err := blob.Store(serverRepo.DB(), []byte("normal-blob-content")) if err != nil { t.Fatalf("store normal blob: %v", err) } // Store shunned blob. _, shunnedUUID, err := blob.Store(serverRepo.DB(), []byte("shunned-blob-content")) if err != nil { t.Fatalf("store shunned blob: %v", err) } if err := shun.Add(serverRepo.DB(), shunnedUUID, "test shun"); err != nil { t.Fatalf("shun.Add: %v", err) } // Store private blob. privRid, _, err := blob.Store(serverRepo.DB(), []byte("private-blob-content")) if err != nil { t.Fatalf("store private blob: %v", err) } if err := content.MakePrivate(serverRepo.DB(), int64(privRid)); err != nil { t.Fatalf("MakePrivate: %v", err) } var privateUUID string if err := serverRepo.DB().QueryRow("SELECT uuid FROM blob WHERE rid=?", privRid).Scan(&privateUUID); err != nil { t.Fatalf("query private UUID: %v", err) } // Read server's project-code and server-code. var projCode, srvCode string serverRepo.DB().QueryRow("SELECT value FROM config WHERE name='project-code'").Scan(&projCode) serverRepo.DB().QueryRow("SELECT value FROM config WHERE name='server-code'").Scan(&srvCode) // Create client repo with matching project-code. clientPath := filepath.Join(tmpDir, "client.fossil") clientRepo, err := repo.Create(clientPath, "testuser", simio.CryptoRand{}, "") if err != nil { t.Fatalf("create client repo: %v", err) } t.Cleanup(func() { clientRepo.Close() }) clientRepo.DB().Exec("UPDATE config SET value=? WHERE name='project-code'", projCode) // Sync client from server via MockFossil. mf := NewMockFossil(serverRepo) ctx := context.Background() for round := 0; round < 10; round++ { result, err := libsync.Sync(ctx, clientRepo, mf, libsync.SyncOpts{ Pull: true, ProjectCode: projCode, ServerCode: srvCode, }) if err != nil { t.Fatalf("sync round %d: %v", round, err) } if result.FilesSent == 0 && result.FilesRecvd == 0 { t.Logf("converged at round %d", round) break } } // Assert: client HAS the normal blob. if !HasBlob(clientRepo, normalUUID) { t.Error("client should have the normal blob") } // Assert: client does NOT have the shunned blob. if HasBlob(clientRepo, shunnedUUID) { t.Error("client should NOT have the shunned blob") } // Assert: client does NOT have the private blob. if HasBlob(clientRepo, privateUUID) { t.Error("client should NOT have the private blob") } } // TestPrivateBlobPropagatesWithFlag verifies that private blobs propagate // when Private=true is set in SyncOpts and the server grants 'x' capability, // while shunned blobs are always excluded regardless. func TestPrivateBlobPropagatesWithFlag(t *testing.T) { tmpDir := t.TempDir() // Create server repo and seed blobs. serverPath := filepath.Join(tmpDir, "server.fossil") serverRepo, err := repo.Create(serverPath, "testuser", simio.CryptoRand{}, "") if err != nil { t.Fatalf("create server repo: %v", err) } t.Cleanup(func() { serverRepo.Close() }) // Grant 'x' capability to nobody for private sync. serverRepo.DB().Exec("UPDATE user SET cap='oix' WHERE login='nobody'") // Store normal blob. _, normalUUID, err := blob.Store(serverRepo.DB(), []byte("normal-blob-content")) if err != nil { t.Fatalf("store normal blob: %v", err) } // Store shunned blob. _, shunnedUUID, err := blob.Store(serverRepo.DB(), []byte("shunned-blob-content")) if err != nil { t.Fatalf("store shunned blob: %v", err) } if err := shun.Add(serverRepo.DB(), shunnedUUID, "test shun"); err != nil { t.Fatalf("shun.Add: %v", err) } // Store private blob. privRid, _, err := blob.Store(serverRepo.DB(), []byte("private-blob-content")) if err != nil { t.Fatalf("store private blob: %v", err) } if err := content.MakePrivate(serverRepo.DB(), int64(privRid)); err != nil { t.Fatalf("MakePrivate: %v", err) } var privateUUID string if err := serverRepo.DB().QueryRow("SELECT uuid FROM blob WHERE rid=?", privRid).Scan(&privateUUID); err != nil { t.Fatalf("query private UUID: %v", err) } // Read server's project-code and server-code. var projCode, srvCode string serverRepo.DB().QueryRow("SELECT value FROM config WHERE name='project-code'").Scan(&projCode) serverRepo.DB().QueryRow("SELECT value FROM config WHERE name='server-code'").Scan(&srvCode) // Create client repo with matching project-code. clientPath := filepath.Join(tmpDir, "client.fossil") clientRepo, err := repo.Create(clientPath, "testuser", simio.CryptoRand{}, "") if err != nil { t.Fatalf("create client repo: %v", err) } t.Cleanup(func() { clientRepo.Close() }) clientRepo.DB().Exec("UPDATE config SET value=? WHERE name='project-code'", projCode) // Sync client from server with Private=true. mf := NewMockFossil(serverRepo) ctx := context.Background() for round := 0; round < 10; round++ { result, err := libsync.Sync(ctx, clientRepo, mf, libsync.SyncOpts{ Pull: true, Private: true, ProjectCode: projCode, ServerCode: srvCode, }) if err != nil { t.Fatalf("sync round %d: %v", round, err) } if result.FilesSent == 0 && result.FilesRecvd == 0 { t.Logf("converged at round %d", round) break } } // Assert: client HAS the normal blob. if !HasBlob(clientRepo, normalUUID) { t.Error("client should have the normal blob") } // Assert: client does NOT have the shunned blob (shun always blocks). if HasBlob(clientRepo, shunnedUUID) { t.Error("client should NOT have the shunned blob") } // Assert: client DOES have the private blob (Private flag allows it). if !HasBlob(clientRepo, privateUUID) { t.Error("client should have the private blob when Private=true") } }