Artifact a22f825be7c029a0d1f83ee4a8d1c8eacec2b3fadb89865d553db9547174715d:
- File
internal/sync/private_test.go
— part of check-in
[1c02c944ec]
at
2026-08-07 17:33:21
on branch trunk
— fix(db): seed hash-policy so new repos name artifacts like fossil init (#225) (#226)
SeedConfig never wrote hash-policy, so naming fell back to deriving from the repo's own artifacts — and an empty repo has none, settling on SHA1 permanently. Seed sha3, which is what fossil init writes.
Clone drops the row the same way it drops project-code: a clone adopts its source's policy, and an absent row derives naming from the artifacts that arrive.
Moves the policy enum to db so the seed and hash.NamingFor read one definition, and updates fixtures that hardcoded SHA1 names to ask the repo how it names artifacts. (user: [email protected], size: 8295) [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/content" "github.com/danmestas/go-libfossil/internal/hash" "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" ) // newPrivateTestPair creates a server and client repo with matching project code. // The server's nobody user is granted the given capabilities. func newPrivateTestPair(t *testing.T, nobodyCaps string) (server, client *repo.Repo) { t.Helper() dir := t.TempDir() sPath := filepath.Join(dir, "server.fossil") s, err := repo.Create(sPath, "test", simio.CryptoRand{}, "") if err != nil { t.Fatalf("server repo: %v", err) } cPath := filepath.Join(dir, "client.fossil") c, err := repo.Create(cPath, "test", simio.CryptoRand{}, "") if err != nil { t.Fatalf("client repo: %v", err) } // Match project codes so sync works. var projCode string s.DB().QueryRow("SELECT value FROM config WHERE name='project-code'").Scan(&projCode) c.DB().Exec("UPDATE config SET value=? WHERE name='project-code'", projCode) // Set nobody capabilities on server. s.DB().Exec("UPDATE user SET cap=? WHERE login='nobody'", nobodyCaps) t.Cleanup(func() { s.Close() c.Close() }) return s, c } // syncViaHandler runs sync.Sync using a MockTransport backed by HandleSync. func syncViaHandler(t *testing.T, serverRepo, clientRepo *repo.Repo, opts SyncOpts) *SyncResult { t.Helper() 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 srvCode string serverRepo.DB().QueryRow("SELECT value FROM config WHERE name='server-code'").Scan(&srvCode) opts.ServerCode = srvCode var projCode string serverRepo.DB().QueryRow("SELECT value FROM config WHERE name='project-code'").Scan(&projCode) opts.ProjectCode = projCode result, err := Sync(context.Background(), clientRepo, transport, opts) if err != nil { t.Fatalf("Sync: %v", err) } return result } func TestSyncPrivateEndToEnd(t *testing.T) { // Server has 3 public blobs and 2 private blobs. // Client with Private=false gets only public. // Client with Private=true gets all. t.Run("without_private_flag", func(t *testing.T) { serverRepo, clientRepo := newPrivateTestPair(t, "oix") // Store 3 public blobs. publicUUIDs := make([]string, 3) for i := range 3 { data := []byte(fmt.Sprintf("public-blob-%d", i)) _, uuid, err := blob.Store(serverRepo.DB(), data) if err != nil { t.Fatalf("blob.Store public: %v", err) } publicUUIDs[i] = uuid } // Store 2 private blobs. privateUUIDs := make([]string, 2) for i := range 2 { data := []byte(fmt.Sprintf("private-blob-%d", i)) rid, uuid, err := blob.Store(serverRepo.DB(), data) if err != nil { t.Fatalf("blob.Store private: %v", err) } if err := content.MakePrivate(serverRepo.DB(), int64(rid)); err != nil { t.Fatalf("MakePrivate: %v", err) } privateUUIDs[i] = uuid } // Sync WITHOUT Private flag. syncViaHandler(t, serverRepo, clientRepo, SyncOpts{ Pull: true, Push: true, Private: false, }) // Public blobs should arrive. for _, uuid := range publicUUIDs { if _, ok := blob.Exists(clientRepo.DB(), uuid); !ok { t.Errorf("client missing public blob %s", uuid) } } // Private blobs should NOT arrive. for _, uuid := range privateUUIDs { if _, ok := blob.Exists(clientRepo.DB(), uuid); ok { t.Errorf("client has private blob %s without Private flag", uuid) } } }) t.Run("with_private_flag", func(t *testing.T) { serverRepo, clientRepo := newPrivateTestPair(t, "oix") // Store 3 public blobs. publicUUIDs := make([]string, 3) for i := range 3 { data := []byte(fmt.Sprintf("public-blob-priv-%d", i)) _, uuid, err := blob.Store(serverRepo.DB(), data) if err != nil { t.Fatalf("blob.Store public: %v", err) } publicUUIDs[i] = uuid } // Store 2 private blobs. privateUUIDs := make([]string, 2) privateRIDs := make([]int64, 2) for i := range 2 { data := []byte(fmt.Sprintf("private-blob-priv-%d", i)) rid, uuid, err := blob.Store(serverRepo.DB(), data) if err != nil { t.Fatalf("blob.Store private: %v", err) } if err := content.MakePrivate(serverRepo.DB(), int64(rid)); err != nil { t.Fatalf("MakePrivate: %v", err) } privateUUIDs[i] = uuid privateRIDs[i] = int64(rid) } // Sync WITH Private flag. syncViaHandler(t, serverRepo, clientRepo, SyncOpts{ Pull: true, Push: true, Private: true, }) // All public blobs should arrive. for _, uuid := range publicUUIDs { if _, ok := blob.Exists(clientRepo.DB(), uuid); !ok { t.Errorf("client missing public blob %s", uuid) } } // All private blobs should arrive AND be marked private on client. for _, uuid := range privateUUIDs { rid, ok := blob.Exists(clientRepo.DB(), uuid) if !ok { t.Errorf("client missing private blob %s", uuid) continue } if !content.IsPrivate(clientRepo.DB(), int64(rid)) { t.Errorf("blob %s should be in client's private table", uuid) } } // Public blobs should NOT be in private table. for _, uuid := range publicUUIDs { rid, ok := blob.Exists(clientRepo.DB(), uuid) if !ok { continue } if content.IsPrivate(clientRepo.DB(), int64(rid)) { t.Errorf("public blob %s should not be in private table", uuid) } } }) t.Run("no_x_capability", func(t *testing.T) { // Server's nobody lacks 'x' capability — pragma send-private rejected. serverRepo, clientRepo := newPrivateTestPair(t, "oi") // no 'x' // Store a private blob on server. data := []byte("secret-no-cap") rid, uuid, err := blob.Store(serverRepo.DB(), data) if err != nil { t.Fatalf("blob.Store: %v", err) } content.MakePrivate(serverRepo.DB(), int64(rid)) // Also store a public blob. pubData := []byte("public-no-cap") _, pubUUID, err := blob.Store(serverRepo.DB(), pubData) if err != nil { t.Fatalf("blob.Store public: %v", err) } // Sync with Private=true, but server denies it. syncViaHandler(t, serverRepo, clientRepo, SyncOpts{ Pull: true, Push: true, Private: true, }) // Public blob should arrive. if _, ok := blob.Exists(clientRepo.DB(), pubUUID); !ok { t.Error("client missing public blob") } // Private blob should NOT arrive (server denied send-private). if _, ok := blob.Exists(clientRepo.DB(), uuid); ok { t.Error("client has private blob despite lacking 'x' capability") } }) } func TestSyncPrivateArtifactTransition(t *testing.T) { // Server has a private blob. Client syncs with Private=true. // Server makes it public. Client syncs again — private status clears. serverRepo, clientRepo := newPrivateTestPair(t, "oix") // Store a blob and mark private on server. data := []byte("transitioning-artifact") uuid := hash.NamingFor(serverRepo.DB()).New.Hash(data) rid, _, err := blob.Store(serverRepo.DB(), data) if err != nil { t.Fatalf("blob.Store: %v", err) } content.MakePrivate(serverRepo.DB(), int64(rid)) // Phase 1: sync with Private=true — client gets the private blob. syncViaHandler(t, serverRepo, clientRepo, SyncOpts{ Pull: true, Push: true, Private: true, }) clientRID, ok := blob.Exists(clientRepo.DB(), uuid) if !ok { t.Fatal("client missing blob after first sync") } if !content.IsPrivate(clientRepo.DB(), int64(clientRID)) { t.Fatal("blob should be private on client after first sync") } // Phase 2: server makes blob public. if err := content.MakePublic(serverRepo.DB(), int64(rid)); err != nil { t.Fatalf("MakePublic: %v", err) } // Sync again — the server will emit igot without IsPrivate=true, // and the client should clear the private status. syncViaHandler(t, serverRepo, clientRepo, SyncOpts{ Pull: true, Push: true, Private: true, }) if content.IsPrivate(clientRepo.DB(), int64(clientRID)) { t.Error("blob should no longer be private on client after transition") } }