usernames: operator reserved-username blocklist
A plain blocklist for names like @support - separate from the collectible
system, so a reservation has no owner, no price and no "bought on Fragment"
badge.
- reserved_usernames table + migration.
- Enforced in replacePeerUsernameTx (the single editable-username write point:
account.updateUsername, channels.updateUsername, @BotFather /setusername) and
in the collectible mint path; a reserved name returns USERNAME_OCCUPIED.
- admin.Service: ReserveUsername / UnreserveUsername (journalled commands) and
the ReservedUsernames listing.
- adminapi: /v1/reserved-usernames{,/reserve,/unreserve}.
- telesrv-admin panel + a "Reserved Usernames" page in the web UI (dist rebuilt).
- Postgres and in-memory store implementations; the memory registry gains an
optional reserved-name check so tests exercise the same rule.
This commit is contained in:
parent
22846e340f
commit
2bdb1ecf37
21 changed files with 843 additions and 6 deletions
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
usernamesapp "telesrv/internal/app/usernames"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
// Compile-time proof that the shipped use-case services satisfy the admin ports.
|
||||
|
|
@ -1427,3 +1428,76 @@ func TestDeleteCollectibleUsernameCommand(t *testing.T) {
|
|||
t.Fatalf("delete of invalid name = nil error, want rejection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReserveAndUnreserveUsername(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
reserved := memory.NewReservedUsernameStore()
|
||||
svc := NewService(Dependencies{
|
||||
Commands: newMemoryCommandRepo(),
|
||||
ReservedUsernames: reserved,
|
||||
Now: fixedNow,
|
||||
})
|
||||
|
||||
dry, err := svc.ReserveUsername(ctx, ReserveUsernameRequest{
|
||||
CommandMeta: CommandMeta{CommandID: "rv-dry", Actor: "ops", Reason: "official handle", DryRun: true},
|
||||
Username: "@Support",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("dry-run reserve: %v", err)
|
||||
}
|
||||
if got, _ := reserved.IsReserved(ctx, "support"); got {
|
||||
t.Fatal("dry-run reserved the name")
|
||||
}
|
||||
if dry.Details["username"] != "Support" {
|
||||
t.Fatalf("dry-run details = %+v", dry.Details)
|
||||
}
|
||||
|
||||
if _, err := svc.ReserveUsername(ctx, ReserveUsernameRequest{
|
||||
CommandMeta: CommandMeta{CommandID: "rv-exec", Actor: "ops", Reason: "official handle"},
|
||||
Username: "support",
|
||||
}); err != nil {
|
||||
t.Fatalf("reserve: %v", err)
|
||||
}
|
||||
if got, _ := reserved.IsReserved(ctx, "support"); !got {
|
||||
t.Fatal("name not reserved after exec")
|
||||
}
|
||||
|
||||
if _, err := svc.UnreserveUsername(ctx, UnreserveUsernameRequest{
|
||||
CommandMeta: CommandMeta{CommandID: "urv-exec", Actor: "ops", Reason: "no longer needed"},
|
||||
Username: "SUPPORT",
|
||||
}); err != nil {
|
||||
t.Fatalf("unreserve: %v", err)
|
||||
}
|
||||
if got, _ := reserved.IsReserved(ctx, "support"); got {
|
||||
t.Fatal("name still reserved after unreserve")
|
||||
}
|
||||
|
||||
if _, err := svc.ReserveUsername(ctx, ReserveUsernameRequest{
|
||||
CommandMeta: CommandMeta{CommandID: "bad", Actor: "ops", Reason: "x"},
|
||||
Username: "ab",
|
||||
}); err == nil {
|
||||
t.Fatal("reserve of a too-short name = nil error, want rejection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemoryRegistryRefusesReservedName(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
reserved := memory.NewReservedUsernameStore()
|
||||
if _, err := reserved.ReserveUsername(ctx, "support", "", "ops"); err != nil {
|
||||
t.Fatalf("seed reserve: %v", err)
|
||||
}
|
||||
registry := memory.NewCollectibleUsernameStore().WithReservedUsernames(reserved)
|
||||
|
||||
if _, err := registry.SetEditableUsername(ctx, domain.Peer{Type: domain.PeerTypeUser, ID: 1}, "support"); !errors.Is(err, domain.ErrUsernameOccupied) {
|
||||
t.Fatalf("SetEditableUsername(reserved) err = %v, want ErrUsernameOccupied", err)
|
||||
}
|
||||
if _, _, err := registry.MintCollectibleUsername(ctx, domain.MintCollectibleUsernameRequest{
|
||||
Username: "support", Currency: domain.CollectibleCurrencyUSD, Amount: 0, CommandKey: "k1",
|
||||
}); !errors.Is(err, domain.ErrUsernameOccupied) {
|
||||
t.Fatalf("Mint(reserved) err = %v, want ErrUsernameOccupied", err)
|
||||
}
|
||||
// A different name is unaffected.
|
||||
if _, err := registry.SetEditableUsername(ctx, domain.Peer{Type: domain.PeerTypeUser, ID: 1}, "freename"); err != nil {
|
||||
t.Fatalf("SetEditableUsername(free) err = %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue