refactor(domain): unexport the principal kind and expose a label
- commit
- 851827caf8314a1f5e5ad5a3c823eb6601e35639
- author
- brice · · 2026-10-10 07:10 UTC
- parent
- 2bea74e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +37 -38
M internal/app/tokens.go +2 -2
| @@ -101,7 +101,7 @@ func (t *Tokens) Create(ctx context.Context, p domain.Principal, params CreateTo | |||
| 101 | 101 | log.Info(). | |
| 102 | 102 | Str("token_id", tok.ID).Str("label", tok.Label). | |
| 103 | 103 | Int("scopes", len(tok.Scopes)).Int("repos", len(tok.Repos)). | |
| 104 | - | Str("created_by", p.Kind().String()).Str("created_by_token", p.TokenID()). | |
| 104 | + | Str("created_by", p.Label()).Str("created_by_token", p.TokenID()). | |
| 105 | 105 | Msg("token created") | |
| 106 | 106 | return CreatedToken{Token: tok, Secret: secret}, nil | |
| 107 | 107 | } | |
| @@ -205,7 +205,7 @@ func (t *Tokens) Revoke(ctx context.Context, p domain.Principal, id string) erro | |||
| 205 | 205 | return err | |
| 206 | 206 | } | |
| 207 | 207 | log := zerowrap.FromCtx(ctx) | |
| 208 | - | log.Info().Str("token_id", id).Str("revoked_by", p.Kind().String()).Str("revoked_by_token", p.TokenID()).Msg("token revoked") | |
| 208 | + | log.Info().Str("token_id", id).Str("revoked_by", p.Label()).Str("revoked_by_token", p.TokenID()).Msg("token revoked") | |
| 209 | 209 | return nil | |
| 210 | 210 | } | |
| 211 | 211 | ||
| @@ -101,7 +101,7 @@ func (t *Tokens) Create(ctx context.Context, p domain.Principal, params CreateTo | |||||
| 101 | log.Info(). | 101 | log.Info(). | ||
| 102 | Str("token_id", tok.ID).Str("label", tok.Label). | 102 | Str("token_id", tok.ID).Str("label", tok.Label). | ||
| 103 | Int("scopes", len(tok.Scopes)).Int("repos", len(tok.Repos)). | 103 | Int("scopes", len(tok.Scopes)).Int("repos", len(tok.Repos)). | ||
| 104 | - | Str("created_by", p.Kind().String()).Str("created_by_token", p.TokenID()). | 104 | + | Str("created_by", p.Label()).Str("created_by_token", p.TokenID()). |
| 105 | Msg("token created") | 105 | Msg("token created") | ||
| 106 | return CreatedToken{Token: tok, Secret: secret}, nil | 106 | return CreatedToken{Token: tok, Secret: secret}, nil | ||
| 107 | } | 107 | } | ||
| @@ -205,7 +205,7 @@ func (t *Tokens) Revoke(ctx context.Context, p domain.Principal, id string) erro | |||||
| 205 | return err | 205 | return err | ||
| 206 | } | 206 | } | ||
| 207 | log := zerowrap.FromCtx(ctx) | 207 | log := zerowrap.FromCtx(ctx) | ||
| 208 | - | log.Info().Str("token_id", id).Str("revoked_by", p.Kind().String()).Str("revoked_by_token", p.TokenID()).Msg("token revoked") | 208 | + | log.Info().Str("token_id", id).Str("revoked_by", p.Label()).Str("revoked_by_token", p.TokenID()).Msg("token revoked") |
| 209 | return nil | 209 | return nil | ||
| 210 | } | 210 | } | ||
| 211 | 211 | ||||
M internal/app/tokens_test.go +1 -1
| @@ -256,7 +256,7 @@ func TestAuthenticate(t *testing.T) { | |||
| 256 | 256 | f.store.EXPECT().Touch(ctx, "t1", now).Return(nil) | |
| 257 | 257 | p, err := f.auth.Authenticate(ctx, secret) | |
| 258 | 258 | require.NoError(t, err) | |
| 259 | - | assert.Equal(t, domain.PrincipalToken, p.Kind()) | |
| 259 | + | assert.True(t, p.IsToken()) | |
| 260 | 260 | assert.Equal(t, "t1", p.TokenID()) | |
| 261 | 261 | assert.True(t, p.Allows(domain.ScopeRepoWrite, name(t, "a"))) | |
| 262 | 262 | assert.False(t, p.Allows(domain.ScopeRepoWrite, name(t, "b"))) | |
| @@ -256,7 +256,7 @@ func TestAuthenticate(t *testing.T) { | |||||
| 256 | f.store.EXPECT().Touch(ctx, "t1", now).Return(nil) | 256 | f.store.EXPECT().Touch(ctx, "t1", now).Return(nil) | ||
| 257 | p, err := f.auth.Authenticate(ctx, secret) | 257 | p, err := f.auth.Authenticate(ctx, secret) | ||
| 258 | require.NoError(t, err) | 258 | require.NoError(t, err) | ||
| 259 | - | assert.Equal(t, domain.PrincipalToken, p.Kind()) | 259 | + | assert.True(t, p.IsToken()) |
| 260 | assert.Equal(t, "t1", p.TokenID()) | 260 | assert.Equal(t, "t1", p.TokenID()) | ||
| 261 | assert.True(t, p.Allows(domain.ScopeRepoWrite, name(t, "a"))) | 261 | assert.True(t, p.Allows(domain.ScopeRepoWrite, name(t, "a"))) | ||
| 262 | assert.False(t, p.Allows(domain.ScopeRepoWrite, name(t, "b"))) | 262 | assert.False(t, p.Allows(domain.ScopeRepoWrite, name(t, "b"))) | ||
M internal/domain/name_test.go +2 -1
| @@ -108,5 +108,6 @@ func TestPrincipal(t *testing.T) { | |||
| 108 | 108 | assert.True(t, zero.IsAnonymous()) | |
| 109 | 109 | assert.False(t, zero.IsRoot()) | |
| 110 | 110 | assert.True(t, domain.Root().IsRoot()) | |
| 111 | - | assert.Equal(t, domain.PrincipalAnonymous, domain.Anonymous().Kind()) | |
| 111 | + | assert.Equal(t, "anonymous", domain.Anonymous().Label()) | |
| 112 | + | assert.False(t, domain.Root().IsToken()) | |
| 112 | 113 | } | |
| @@ -108,5 +108,6 @@ func TestPrincipal(t *testing.T) { | |||||
| 108 | assert.True(t, zero.IsAnonymous()) | 108 | assert.True(t, zero.IsAnonymous()) | ||
| 109 | assert.False(t, zero.IsRoot()) | 109 | assert.False(t, zero.IsRoot()) | ||
| 110 | assert.True(t, domain.Root().IsRoot()) | 110 | assert.True(t, domain.Root().IsRoot()) | ||
| 111 | - | assert.Equal(t, domain.PrincipalAnonymous, domain.Anonymous().Kind()) | 111 | + | assert.Equal(t, "anonymous", domain.Anonymous().Label()) |
| 112 | + | assert.False(t, domain.Root().IsToken()) | |||
| 112 | } | 113 | } | ||
M internal/domain/principal.go +28 -30
| @@ -7,34 +7,20 @@ import ( | |||
| 7 | 7 | "time" | |
| 8 | 8 | ) | |
| 9 | 9 | ||
| 10 | - | // PrincipalKind tells who a caller is. | |
| 11 | - | type PrincipalKind uint8 | |
| 10 | + | // principalKind tells who a caller is. | |
| 11 | + | type principalKind uint8 | |
| 12 | 12 | ||
| 13 | 13 | // Principal kinds. The zero value is anonymous, the least privileged. | |
| 14 | 14 | const ( | |
| 15 | - | // PrincipalAnonymous is a caller without credentials. | |
| 16 | - | PrincipalAnonymous PrincipalKind = iota | |
| 17 | - | // PrincipalRoot is the holder of the admin token. | |
| 18 | - | PrincipalRoot | |
| 19 | - | // PrincipalToken is the holder of a stored token. | |
| 20 | - | PrincipalToken | |
| 15 | + | kindAnonymous principalKind = iota | |
| 16 | + | kindRoot | |
| 17 | + | kindToken | |
| 21 | 18 | ) | |
| 22 | 19 | ||
| 23 | - | // String returns "anonymous", "root" or "token". | |
| 24 | - | func (k PrincipalKind) String() string { | |
| 25 | - | switch k { | |
| 26 | - | case PrincipalRoot: | |
| 27 | - | return "root" | |
| 28 | - | case PrincipalToken: | |
| 29 | - | return "token" | |
| 30 | - | } | |
| 31 | - | return "anonymous" | |
| 32 | - | } | |
| 33 | - | ||
| 34 | 20 | // Principal is the authenticated identity of a caller: nobody, the root | |
| 35 | 21 | // token, or a stored token with its scopes and repositories. | |
| 36 | 22 | type Principal struct { | |
| 37 | - | kind PrincipalKind | |
| 23 | + | kind principalKind | |
| 38 | 24 | tokenID string | |
| 39 | 25 | scopes []Scope | |
| 40 | 26 | repos []RepoName | |
| @@ -42,15 +28,15 @@ type Principal struct { | |||
| 42 | 28 | } | |
| 43 | 29 | ||
| 44 | 30 | // Anonymous returns the principal of a caller without credentials. | |
| 45 | - | func Anonymous() Principal { return Principal{kind: PrincipalAnonymous} } | |
| 31 | + | func Anonymous() Principal { return Principal{kind: kindAnonymous} } | |
| 46 | 32 | ||
| 47 | 33 | // Root returns the principal of the admin token holder. | |
| 48 | - | func Root() Principal { return Principal{kind: PrincipalRoot} } | |
| 34 | + | func Root() Principal { return Principal{kind: kindRoot} } | |
| 49 | 35 | ||
| 50 | 36 | // TokenPrincipal returns the principal of the holder of a stored token. | |
| 51 | 37 | func TokenPrincipal(t Token) Principal { | |
| 52 | 38 | return Principal{ | |
| 53 | - | kind: PrincipalToken, | |
| 39 | + | kind: kindToken, | |
| 54 | 40 | tokenID: t.ID, | |
| 55 | 41 | scopes: slices.Clone(t.Scopes), | |
| 56 | 42 | repos: slices.Clone(t.Repos), | |
| @@ -58,14 +44,26 @@ func TokenPrincipal(t Token) Principal { | |||
| 58 | 44 | } | |
| 59 | 45 | } | |
| 60 | 46 | ||
| 61 | - | // Kind returns the kind of principal. | |
| 62 | - | func (p Principal) Kind() PrincipalKind { return p.kind } | |
| 47 | + | // Label names the kind of principal for logs and messages: "anonymous", | |
| 48 | + | // "root" or "token". | |
| 49 | + | func (p Principal) Label() string { | |
| 50 | + | switch p.kind { | |
| 51 | + | case kindRoot: | |
| 52 | + | return "root" | |
| 53 | + | case kindToken: | |
| 54 | + | return "token" | |
| 55 | + | } | |
| 56 | + | return "anonymous" | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // IsToken reports whether p holds a stored token. | |
| 60 | + | func (p Principal) IsToken() bool { return p.kind == kindToken } | |
| 63 | 61 | ||
| 64 | 62 | // IsRoot reports whether p may do everything. | |
| 65 | - | func (p Principal) IsRoot() bool { return p.kind == PrincipalRoot } | |
| 63 | + | func (p Principal) IsRoot() bool { return p.kind == kindRoot } | |
| 66 | 64 | ||
| 67 | 65 | // IsAnonymous reports whether p has no credentials. | |
| 68 | - | func (p Principal) IsAnonymous() bool { return p.kind == PrincipalAnonymous } | |
| 66 | + | func (p Principal) IsAnonymous() bool { return p.kind == kindAnonymous } | |
| 69 | 67 | ||
| 70 | 68 | // TokenID returns the ID of the stored token, or "" for other principals. | |
| 71 | 69 | func (p Principal) TokenID() string { return p.tokenID } | |
| @@ -84,9 +82,9 @@ func (p Principal) ExpiresAt() time.Time { return p.expiresAt } | |||
| 84 | 82 | // every scope; anonymous none. | |
| 85 | 83 | func (p Principal) HasScope(s Scope) bool { | |
| 86 | 84 | switch p.kind { | |
| 87 | - | case PrincipalRoot: | |
| 85 | + | case kindRoot: | |
| 88 | 86 | return true | |
| 89 | - | case PrincipalToken: | |
| 87 | + | case kindToken: | |
| 90 | 88 | return slices.Contains(p.scopes, s) | |
| 91 | 89 | } | |
| 92 | 90 | return false | |
| @@ -98,7 +96,7 @@ func (p Principal) Allows(s Scope, repo RepoName) bool { | |||
| 98 | 96 | if !p.HasScope(s) { | |
| 99 | 97 | return false | |
| 100 | 98 | } | |
| 101 | - | if p.kind == PrincipalToken && len(p.repos) > 0 { | |
| 99 | + | if p.kind == kindToken && len(p.repos) > 0 { | |
| 102 | 100 | return slices.Contains(p.repos, repo) | |
| 103 | 101 | } | |
| 104 | 102 | return true | |
| @@ -7,34 +7,20 @@ import ( | |||||
| 7 | "time" | 7 | "time" | ||
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
| 10 | - | // PrincipalKind tells who a caller is. | 10 | + | // principalKind tells who a caller is. |
| 11 | - | type PrincipalKind uint8 | 11 | + | type principalKind uint8 |
| 12 | 12 | ||||
| 13 | // Principal kinds. The zero value is anonymous, the least privileged. | 13 | // Principal kinds. The zero value is anonymous, the least privileged. | ||
| 14 | const ( | 14 | const ( | ||
| 15 | - | // PrincipalAnonymous is a caller without credentials. | 15 | + | kindAnonymous principalKind = iota |
| 16 | - | PrincipalAnonymous PrincipalKind = iota | 16 | + | kindRoot |
| 17 | - | // PrincipalRoot is the holder of the admin token. | 17 | + | kindToken |
| 18 | - | PrincipalRoot | |||
| 19 | - | // PrincipalToken is the holder of a stored token. | |||
| 20 | - | PrincipalToken | |||
| 21 | ) | 18 | ) | ||
| 22 | 19 | ||||
| 23 | - | // String returns "anonymous", "root" or "token". | |||
| 24 | - | func (k PrincipalKind) String() string { | |||
| 25 | - | switch k { | |||
| 26 | - | case PrincipalRoot: | |||
| 27 | - | return "root" | |||
| 28 | - | case PrincipalToken: | |||
| 29 | - | return "token" | |||
| 30 | - | } | |||
| 31 | - | return "anonymous" | |||
| 32 | - | } | |||
| 33 | - | ||||
| 34 | // Principal is the authenticated identity of a caller: nobody, the root | 20 | // Principal is the authenticated identity of a caller: nobody, the root | ||
| 35 | // token, or a stored token with its scopes and repositories. | 21 | // token, or a stored token with its scopes and repositories. | ||
| 36 | type Principal struct { | 22 | type Principal struct { | ||
| 37 | - | kind PrincipalKind | 23 | + | kind principalKind |
| 38 | tokenID string | 24 | tokenID string | ||
| 39 | scopes []Scope | 25 | scopes []Scope | ||
| 40 | repos []RepoName | 26 | repos []RepoName | ||
| @@ -42,15 +28,15 @@ type Principal struct { | |||||
| 42 | } | 28 | } | ||
| 43 | 29 | ||||
| 44 | // Anonymous returns the principal of a caller without credentials. | 30 | // Anonymous returns the principal of a caller without credentials. | ||
| 45 | - | func Anonymous() Principal { return Principal{kind: PrincipalAnonymous} } | 31 | + | func Anonymous() Principal { return Principal{kind: kindAnonymous} } |
| 46 | 32 | ||||
| 47 | // Root returns the principal of the admin token holder. | 33 | // Root returns the principal of the admin token holder. | ||
| 48 | - | func Root() Principal { return Principal{kind: PrincipalRoot} } | 34 | + | func Root() Principal { return Principal{kind: kindRoot} } |
| 49 | 35 | ||||
| 50 | // TokenPrincipal returns the principal of the holder of a stored token. | 36 | // TokenPrincipal returns the principal of the holder of a stored token. | ||
| 51 | func TokenPrincipal(t Token) Principal { | 37 | func TokenPrincipal(t Token) Principal { | ||
| 52 | return Principal{ | 38 | return Principal{ | ||
| 53 | - | kind: PrincipalToken, | 39 | + | kind: kindToken, |
| 54 | tokenID: t.ID, | 40 | tokenID: t.ID, | ||
| 55 | scopes: slices.Clone(t.Scopes), | 41 | scopes: slices.Clone(t.Scopes), | ||
| 56 | repos: slices.Clone(t.Repos), | 42 | repos: slices.Clone(t.Repos), | ||
| @@ -58,14 +44,26 @@ func TokenPrincipal(t Token) Principal { | |||||
| 58 | } | 44 | } | ||
| 59 | } | 45 | } | ||
| 60 | 46 | ||||
| 61 | - | // Kind returns the kind of principal. | 47 | + | // Label names the kind of principal for logs and messages: "anonymous", |
| 62 | - | func (p Principal) Kind() PrincipalKind { return p.kind } | 48 | + | // "root" or "token". |
| 49 | + | func (p Principal) Label() string { | |||
| 50 | + | switch p.kind { | |||
| 51 | + | case kindRoot: | |||
| 52 | + | return "root" | |||
| 53 | + | case kindToken: | |||
| 54 | + | return "token" | |||
| 55 | + | } | |||
| 56 | + | return "anonymous" | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // IsToken reports whether p holds a stored token. | |||
| 60 | + | func (p Principal) IsToken() bool { return p.kind == kindToken } | |||
| 63 | 61 | ||||
| 64 | // IsRoot reports whether p may do everything. | 62 | // IsRoot reports whether p may do everything. | ||
| 65 | - | func (p Principal) IsRoot() bool { return p.kind == PrincipalRoot } | 63 | + | func (p Principal) IsRoot() bool { return p.kind == kindRoot } |
| 66 | 64 | ||||
| 67 | // IsAnonymous reports whether p has no credentials. | 65 | // IsAnonymous reports whether p has no credentials. | ||
| 68 | - | func (p Principal) IsAnonymous() bool { return p.kind == PrincipalAnonymous } | 66 | + | func (p Principal) IsAnonymous() bool { return p.kind == kindAnonymous } |
| 69 | 67 | ||||
| 70 | // TokenID returns the ID of the stored token, or "" for other principals. | 68 | // TokenID returns the ID of the stored token, or "" for other principals. | ||
| 71 | func (p Principal) TokenID() string { return p.tokenID } | 69 | func (p Principal) TokenID() string { return p.tokenID } | ||
| @@ -84,9 +82,9 @@ func (p Principal) ExpiresAt() time.Time { return p.expiresAt } | |||||
| 84 | // every scope; anonymous none. | 82 | // every scope; anonymous none. | ||
| 85 | func (p Principal) HasScope(s Scope) bool { | 83 | func (p Principal) HasScope(s Scope) bool { | ||
| 86 | switch p.kind { | 84 | switch p.kind { | ||
| 87 | - | case PrincipalRoot: | 85 | + | case kindRoot: |
| 88 | return true | 86 | return true | ||
| 89 | - | case PrincipalToken: | 87 | + | case kindToken: |
| 90 | return slices.Contains(p.scopes, s) | 88 | return slices.Contains(p.scopes, s) | ||
| 91 | } | 89 | } | ||
| 92 | return false | 90 | return false | ||
| @@ -98,7 +96,7 @@ func (p Principal) Allows(s Scope, repo RepoName) bool { | |||||
| 98 | if !p.HasScope(s) { | 96 | if !p.HasScope(s) { | ||
| 99 | return false | 97 | return false | ||
| 100 | } | 98 | } | ||
| 101 | - | if p.kind == PrincipalToken && len(p.repos) > 0 { | 99 | + | if p.kind == kindToken && len(p.repos) > 0 { |
| 102 | return slices.Contains(p.repos, repo) | 100 | return slices.Contains(p.repos, repo) | ||
| 103 | } | 101 | } | ||
| 104 | return true | 102 | return true | ||
M internal/domain/token_test.go +4 -4
| @@ -48,8 +48,8 @@ func TestPrincipalScopes(t *testing.T) { | |||
| 48 | 48 | tok := domain.Token{ID: "t1", Scopes: []domain.Scope{domain.ScopeRepoRead, domain.ScopeRepoWrite}, Repos: []domain.RepoName{a}} | |
| 49 | 49 | p := domain.TokenPrincipal(tok) | |
| 50 | 50 | ||
| 51 | - | assert.Equal(t, domain.PrincipalToken, p.Kind()) | |
| 52 | - | assert.Equal(t, "token", p.Kind().String()) | |
| 51 | + | assert.True(t, p.IsToken()) | |
| 52 | + | assert.Equal(t, "token", p.Label()) | |
| 53 | 53 | assert.Equal(t, "t1", p.TokenID()) | |
| 54 | 54 | assert.False(t, p.IsRoot()) | |
| 55 | 55 | assert.False(t, p.IsAnonymous()) | |
| @@ -73,8 +73,8 @@ func TestPrincipalScopes(t *testing.T) { | |||
| 73 | 73 | assert.True(t, domain.Root().Allows(domain.ScopeTokenAdmin, a)) | |
| 74 | 74 | assert.False(t, domain.Anonymous().HasScope(domain.ScopeRepoRead)) | |
| 75 | 75 | assert.False(t, domain.Anonymous().Allows(domain.ScopeRepoRead, a)) | |
| 76 | - | assert.Equal(t, "root", domain.Root().Kind().String()) | |
| 77 | - | assert.Equal(t, "anonymous", domain.Anonymous().Kind().String()) | |
| 76 | + | assert.Equal(t, "root", domain.Root().Label()) | |
| 77 | + | assert.Equal(t, "anonymous", domain.Anonymous().Label()) | |
| 78 | 78 | } | |
| 79 | 79 | ||
| 80 | 80 | func mustName(t *testing.T, s string) domain.RepoName { | |
| @@ -48,8 +48,8 @@ func TestPrincipalScopes(t *testing.T) { | |||||
| 48 | tok := domain.Token{ID: "t1", Scopes: []domain.Scope{domain.ScopeRepoRead, domain.ScopeRepoWrite}, Repos: []domain.RepoName{a}} | 48 | tok := domain.Token{ID: "t1", Scopes: []domain.Scope{domain.ScopeRepoRead, domain.ScopeRepoWrite}, Repos: []domain.RepoName{a}} | ||
| 49 | p := domain.TokenPrincipal(tok) | 49 | p := domain.TokenPrincipal(tok) | ||
| 50 | 50 | ||||
| 51 | - | assert.Equal(t, domain.PrincipalToken, p.Kind()) | 51 | + | assert.True(t, p.IsToken()) |
| 52 | - | assert.Equal(t, "token", p.Kind().String()) | 52 | + | assert.Equal(t, "token", p.Label()) |
| 53 | assert.Equal(t, "t1", p.TokenID()) | 53 | assert.Equal(t, "t1", p.TokenID()) | ||
| 54 | assert.False(t, p.IsRoot()) | 54 | assert.False(t, p.IsRoot()) | ||
| 55 | assert.False(t, p.IsAnonymous()) | 55 | assert.False(t, p.IsAnonymous()) | ||
| @@ -73,8 +73,8 @@ func TestPrincipalScopes(t *testing.T) { | |||||
| 73 | assert.True(t, domain.Root().Allows(domain.ScopeTokenAdmin, a)) | 73 | assert.True(t, domain.Root().Allows(domain.ScopeTokenAdmin, a)) | ||
| 74 | assert.False(t, domain.Anonymous().HasScope(domain.ScopeRepoRead)) | 74 | assert.False(t, domain.Anonymous().HasScope(domain.ScopeRepoRead)) | ||
| 75 | assert.False(t, domain.Anonymous().Allows(domain.ScopeRepoRead, a)) | 75 | assert.False(t, domain.Anonymous().Allows(domain.ScopeRepoRead, a)) | ||
| 76 | - | assert.Equal(t, "root", domain.Root().Kind().String()) | 76 | + | assert.Equal(t, "root", domain.Root().Label()) |
| 77 | - | assert.Equal(t, "anonymous", domain.Anonymous().Kind().String()) | 77 | + | assert.Equal(t, "anonymous", domain.Anonymous().Label()) |
| 78 | } | 78 | } | ||
| 79 | 79 | ||||
| 80 | func mustName(t *testing.T, s string) domain.RepoName { | 80 | func mustName(t *testing.T, s string) domain.RepoName { | ||