| index 936a409ac..c1d0bb82e 100644 |
| --- a/auth_e2e_test.go |
| +++ b/auth_e2e_test.go |
| @@ -134,6 +134,35 @@ func TestAccountsCreateNeedsAnOwner(t *testing.T) { |
| } |
| } |
| |
| +// A non-slug name is refused before anything is minted — this is the third |
| +// (and only bucket-writing) door, so a shape mistake here must not reach |
| +// domains.MintAccount at all. -owner is supplied so the refusal is |
| +// unambiguously about the NAME, not the ownerless-account path above. |
| +func TestAccountsCreateRefusesANonSlugName(t *testing.T) { |
| + bin := buildCarlos(t) |
| + tmp := t.TempDir() |
| + storeDir := filepath.Join(tmp, "store") |
| + env := append(os.Environ(), |
| + "CARLOS_DEPLOYMENT_DIR="+storeDir, |
| + "CARLOS_CREDENTIALS="+filepath.Join(tmp, "credentials"), |
| + "CARLOS_DATA_DIR="+tmp, |
| + ) |
| + cmd := exec.Command(bin, "accounts", "create", "-name", "Paul's Stuff!", "-owner", "paul@keymail.dev") |
| + cmd.Env = env |
| + out, err := cmd.CombinedOutput() |
| + if err == nil { |
| + t.Fatalf("non-slug accounts create exited 0:\n%s", out) |
| + } |
| + if !strings.Contains(string(out), "not a usable account name") { |
| + t.Fatalf("refusal does not name the shape rule:\n%s", out) |
| + } |
| + // Nothing was minted: a rejected name must not burn an id or leave a |
| + // half-made account in the bucket. |
| + if keys, err := release.NewDirStore(storeDir).List(t.Context(), "accounts/"); err != nil || len(keys) != 0 { |
| + t.Fatalf("a refused create left %v behind (%v)", keys, err) |
| + } |
| +} |
| + |
| // The headless path: -owner names the owner outright, no login, no browser, |
| // and the address lands in the manifest every box materializes identity from. |
| func TestAccountsCreateWithOwnerFlag(t *testing.T) { |
| @@ -171,6 +200,43 @@ func TestAccountsCreateWithOwnerFlag(t *testing.T) { |
| } |
| } |
| |
| +// A padded name is trimmed BEFORE it is validated or minted — the gate and |
| +// the write must agree on what "the name" is. Before this fix, -name " |
| +// kass " passed ValidAccountName on the trimmed copy but handed the |
| +// untrimmed original to MintAccount, landing padding in the manifest |
| +// through the one edge that writes straight to the bucket. |
| +func TestAccountsCreateTrimsTheNameBeforeMinting(t *testing.T) { |
| + bin := buildCarlos(t) |
| + tmp := t.TempDir() |
| + storeDir := filepath.Join(tmp, "store") |
| + cmd := exec.Command(bin, "accounts", "create", "-name", " kass ", "-owner", "paul@keymail.dev") |
| + cmd.Env = append(os.Environ(), |
| + "CARLOS_DEPLOYMENT_DIR="+storeDir, |
| + "CARLOS_CREDENTIALS="+filepath.Join(tmp, "credentials"), |
| + "CARLOS_DATA_DIR="+tmp, |
| + ) |
| + out, err := cmd.CombinedOutput() |
| + if err != nil { |
| + t.Fatalf("carlos accounts create -name ' kass ': %v\n%s", err, out) |
| + } |
| + store := release.NewDirStore(storeDir) |
| + keys, err := store.List(t.Context(), "accounts/") |
| + if err != nil || len(keys) != 1 { |
| + t.Fatalf("account manifests = %v (%v), want exactly one", keys, err) |
| + } |
| + body, _, err := store.Get(t.Context(), keys[0]) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var m domains.AccountManifest |
| + if err := json.Unmarshal(body, &m); err != nil { |
| + t.Fatal(err) |
| + } |
| + if m.Name != "kass" { |
| + t.Fatalf("manifest name = %q, want the trimmed %q", m.Name, "kass") |
| + } |
| +} |
| + |
| // Logged in, no -owner: the account is created THROUGH the console, so the |
| // person at the terminal is its owner from the first instant. |
| func TestAccountsCreateUsesTheLoggedInIdentity(t *testing.T) { |