Artifact c2c39d9442435582fcf5b7f2123742385b11889b3f77bbee54c597afeed0b37a:
- File
internal/auth/user_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: 5166) [annotate] [blame] [check-ins using]
package auth import ( "path/filepath" "testing" "github.com/danmestas/libfossil/db" _ "github.com/danmestas/libfossil/internal/testdriver" "github.com/danmestas/libfossil/internal/repo" "github.com/danmestas/libfossil/simio" ) func setupTestDB(t *testing.T) *db.DB { t.Helper() path := filepath.Join(t.TempDir(), "test.fossil") r, err := repo.Create(path, "admin", simio.CryptoRand{}, "") if err != nil { t.Fatalf("repo.Create: %v", err) } t.Cleanup(func() { r.Close() }) return r.DB() } func projectCode(t *testing.T, d *db.DB) string { t.Helper() var code string if err := d.QueryRow("SELECT value FROM config WHERE name='project-code'").Scan(&code); err != nil { t.Fatalf("project-code: %v", err) } return code } func TestCreateAndGetUser(t *testing.T) { d := setupTestDB(t) code := projectCode(t, d) err := CreateUser(d, code, "alice", "secret123", "ios") if err != nil { t.Fatalf("CreateUser: %v", err) } user, err := GetUser(d, "alice") if err != nil { t.Fatalf("GetUser: %v", err) } if user.Login != "alice" { t.Errorf("Login = %q, want alice", user.Login) } if user.Cap != "ios" { t.Errorf("Cap = %q, want ios", user.Cap) } // Verify password hash was stored correctly var storedPW string err = d.QueryRow("SELECT pw FROM user WHERE login='alice'").Scan(&storedPW) if err != nil { t.Fatalf("query stored pw: %v", err) } expected := hashPassword(code, "alice", "secret123") if storedPW != expected { t.Errorf("stored pw = %q, want %q", storedPW, expected) } } func TestCreateUserDuplicate(t *testing.T) { d := setupTestDB(t) code := projectCode(t, d) err := CreateUser(d, code, "alice", "secret123", "o") if err != nil { t.Fatalf("CreateUser first: %v", err) } err = CreateUser(d, code, "alice", "different", "i") if err == nil { t.Fatal("CreateUser duplicate: expected error, got nil") } } func TestGetUserNotFound(t *testing.T) { d := setupTestDB(t) _, err := GetUser(d, "doesnotexist") if err == nil { t.Fatal("GetUser nonexistent: expected error, got nil") } } func TestListUsers(t *testing.T) { d := setupTestDB(t) code := projectCode(t, d) err := CreateUser(d, code, "alice", "pw1", "o") if err != nil { t.Fatalf("CreateUser alice: %v", err) } err = CreateUser(d, code, "bob", "pw2", "i") if err != nil { t.Fatalf("CreateUser bob: %v", err) } users, err := ListUsers(d) if err != nil { t.Fatalf("ListUsers: %v", err) } // Should have at least 3: admin + alice + bob if len(users) < 3 { t.Errorf("ListUsers returned %d users, want >= 3", len(users)) } // Verify alice and bob are in the list found := make(map[string]bool) for _, u := range users { found[u.Login] = true } if !found["alice"] { t.Error("alice not in ListUsers result") } if !found["bob"] { t.Error("bob not in ListUsers result") } } func TestUpdateCaps(t *testing.T) { d := setupTestDB(t) code := projectCode(t, d) err := CreateUser(d, code, "alice", "pw", "o") if err != nil { t.Fatalf("CreateUser: %v", err) } err = UpdateCaps(d, "alice", "ois") if err != nil { t.Fatalf("UpdateCaps: %v", err) } user, err := GetUser(d, "alice") if err != nil { t.Fatalf("GetUser: %v", err) } if user.Cap != "ois" { t.Errorf("Cap after update = %q, want ois", user.Cap) } } func TestUpdateCapsNotFound(t *testing.T) { d := setupTestDB(t) err := UpdateCaps(d, "doesnotexist", "o") if err == nil { t.Fatal("UpdateCaps nonexistent: expected error, got nil") } } func TestSetPassword(t *testing.T) { d := setupTestDB(t) code := projectCode(t, d) err := CreateUser(d, code, "alice", "oldpw", "o") if err != nil { t.Fatalf("CreateUser: %v", err) } // Get original password hash var oldPW string err = d.QueryRow("SELECT pw FROM user WHERE login='alice'").Scan(&oldPW) if err != nil { t.Fatalf("query old pw: %v", err) } err = SetPassword(d, code, "alice", "newpw") if err != nil { t.Fatalf("SetPassword: %v", err) } // Verify password hash changed var newPW string err = d.QueryRow("SELECT pw FROM user WHERE login='alice'").Scan(&newPW) if err != nil { t.Fatalf("query new pw: %v", err) } if newPW == oldPW { t.Error("password hash did not change") } expected := hashPassword(code, "alice", "newpw") if newPW != expected { t.Errorf("new pw = %q, want %q", newPW, expected) } } func TestSetPasswordNotFound(t *testing.T) { d := setupTestDB(t) code := projectCode(t, d) err := SetPassword(d, code, "doesnotexist", "pw") if err == nil { t.Fatal("SetPassword nonexistent: expected error, got nil") } } func TestDeleteUser(t *testing.T) { d := setupTestDB(t) code := projectCode(t, d) err := CreateUser(d, code, "alice", "pw", "o") if err != nil { t.Fatalf("CreateUser: %v", err) } err = DeleteUser(d, "alice") if err != nil { t.Fatalf("DeleteUser: %v", err) } // Verify user is gone _, err = GetUser(d, "alice") if err == nil { t.Fatal("GetUser after delete: expected error, got nil") } } func TestDeleteUserNotFound(t *testing.T) { d := setupTestDB(t) err := DeleteUser(d, "doesnotexist") if err == nil { t.Fatal("DeleteUser nonexistent: expected error, got nil") } }