Artifact f1867c6629318097d4e117fd850581a5ce8c2d89d20ab24a9e303c634e9d0576:
- File
dst/mock_fossil_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: 5445) [annotate] [blame] [check-ins using]
package dst import ( "context" "path/filepath" "testing" "github.com/danmestas/libfossil/internal/blob" "github.com/danmestas/libfossil/internal/hash" "github.com/danmestas/libfossil/internal/repo" "github.com/danmestas/libfossil/internal/xfer" "github.com/danmestas/libfossil/simio" ) func createMockFossil(t *testing.T) *MockFossil { t.Helper() path := filepath.Join(t.TempDir(), "master.fossil") r, err := repo.Create(path, "master", simio.CryptoRand{}, "") if err != nil { t.Fatalf("repo.Create: %v", err) } t.Cleanup(func() { r.Close() }) return NewMockFossil(r) } func TestMockFossilStoreArtifact(t *testing.T) { mf := createMockFossil(t) data := []byte("hello from mock fossil") uuid, err := mf.StoreArtifact(data) if err != nil { t.Fatalf("StoreArtifact: %v", err) } expected := hash.SHA1(data) if uuid != expected { t.Fatalf("UUID = %q, want %q", uuid, expected) } } func TestMockFossilPullFlow(t *testing.T) { mf := createMockFossil(t) // Seed an artifact in the master. data := []byte("artifact for pull test") uuid, _ := mf.StoreArtifact(data) ctx := context.Background() // Round 1: client sends pull — server should respond with igot. req1 := &xfer.Message{Cards: []xfer.Card{ &xfer.PullCard{ServerCode: "sc", ProjectCode: "pc"}, }} resp1, err := mf.Exchange(ctx, req1) if err != nil { t.Fatalf("Exchange round 1: %v", err) } var foundIGot bool for _, c := range resp1.Cards { if igot, ok := c.(*xfer.IGotCard); ok && igot.UUID == uuid { foundIGot = true } } if !foundIGot { t.Fatalf("expected igot card for %s in response", uuid) } // Round 2: client sends gimme — server should respond with file. req2 := &xfer.Message{Cards: []xfer.Card{ &xfer.PullCard{ServerCode: "sc", ProjectCode: "pc"}, &xfer.GimmeCard{UUID: uuid}, }} resp2, err := mf.Exchange(ctx, req2) if err != nil { t.Fatalf("Exchange round 2: %v", err) } var foundFile bool for _, c := range resp2.Cards { if fc, ok := c.(*xfer.FileCard); ok && fc.UUID == uuid { if string(fc.Content) != string(data) { t.Fatalf("file content mismatch") } foundFile = true } } if !foundFile { t.Fatalf("expected file card for %s in response", uuid) } } func TestMockFossilPushFlow(t *testing.T) { mf := createMockFossil(t) data := []byte("artifact from client push") uuid := hash.SHA1(data) ctx := context.Background() // Round 1: client sends push + igot — server should gimme. // HandleSync requires both push AND pull for igot→gimme, // since gimme is a "I need this" response to igot. req1 := &xfer.Message{Cards: []xfer.Card{ &xfer.PushCard{ServerCode: "sc", ProjectCode: "pc"}, &xfer.PullCard{ServerCode: "sc", ProjectCode: "pc"}, &xfer.IGotCard{UUID: uuid}, }} resp1, err := mf.Exchange(ctx, req1) if err != nil { t.Fatalf("Exchange round 1: %v", err) } var foundGimme bool for _, c := range resp1.Cards { if gc, ok := c.(*xfer.GimmeCard); ok && gc.UUID == uuid { foundGimme = true } } if !foundGimme { t.Fatalf("expected gimme card for %s in response", uuid) } // Round 2: client sends push + file — server should store it. req2 := &xfer.Message{Cards: []xfer.Card{ &xfer.PushCard{ServerCode: "sc", ProjectCode: "pc"}, &xfer.FileCard{UUID: uuid, Content: data}, }} resp2, err := mf.Exchange(ctx, req2) if err != nil { t.Fatalf("Exchange round 2: %v", err) } for _, c := range resp2.Cards { if ec, ok := c.(*xfer.ErrorCard); ok { t.Fatalf("unexpected error card: %s", ec.Message) } } // Verify the artifact is in the master repo. _, ok := blob.Exists(mf.Repo().DB(), uuid) if !ok { t.Fatal("blob not found after push") } } func TestMockFossilPushBadUUID(t *testing.T) { mf := createMockFossil(t) ctx := context.Background() req := &xfer.Message{Cards: []xfer.Card{ &xfer.PushCard{ServerCode: "sc", ProjectCode: "pc"}, &xfer.FileCard{UUID: "0000000000000000000000000000000000000000", Content: []byte("data")}, }} resp, err := mf.Exchange(ctx, req) if err != nil { t.Fatalf("Exchange: %v", err) } var foundError bool for _, c := range resp.Cards { if _, ok := c.(*xfer.ErrorCard); ok { foundError = true } } if !foundError { t.Fatal("expected error card for UUID mismatch") } } func TestMockFossilRejectsBadLogin(t *testing.T) { mf := createMockFossil(t) ctx := context.Background() // Bad credentials should produce an auth error. req := &xfer.Message{Cards: []xfer.Card{ &xfer.LoginCard{User: "testuser", Nonce: "abc", Signature: "def"}, &xfer.PullCard{ServerCode: "sc", ProjectCode: "pc"}, }} resp, err := mf.Exchange(ctx, req) if err != nil { t.Fatalf("Exchange: %v", err) } foundAuthError := false for _, c := range resp.Cards { if ec, ok := c.(*xfer.ErrorCard); ok && ec.Message == "authentication failed" { foundAuthError = true } } if !foundAuthError { t.Fatal("expected authentication failed error for bad credentials") } } func TestMockFossilAnonymousPull(t *testing.T) { mf := createMockFossil(t) ctx := context.Background() // Anonymous (no login card) should work — nobody user has full caps. req := &xfer.Message{Cards: []xfer.Card{ &xfer.PullCard{ServerCode: "sc", ProjectCode: "pc"}, }} resp, err := mf.Exchange(ctx, req) if err != nil { t.Fatalf("Exchange: %v", err) } for _, c := range resp.Cards { if ec, ok := c.(*xfer.ErrorCard); ok { t.Fatalf("unexpected error: %s", ec.Message) } } }