mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -17,16 +17,15 @@ import (
|
|||||||
var AuthAddFlags = []cli.Flag{
|
var AuthAddFlags = []cli.Flag{
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "name, n",
|
Name: "name, n",
|
||||||
Usage: "arbitrary `IDENTIFIER` for the new access token",
|
Usage: "arbitrary name to help identify the access `TOKEN`",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "user, u",
|
Name: "user, u",
|
||||||
Usage: "provide a `USERNAME` if a personal access token for a specific user account should be created",
|
Usage: "`USERNAME` of the account the access token belongs to (leave empty for none)",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "scope, s",
|
Name: "scope, s",
|
||||||
Usage: "authorization `SCOPE` for the access token e.g. \"metrics\" or \"photos albums\" (\"*\" to allow all scopes)",
|
Usage: "authorization `SCOPE` for the access token e.g. \"metrics\" or \"photos albums\" (\"*\" to allow all scopes)",
|
||||||
Value: "*",
|
|
||||||
},
|
},
|
||||||
cli.Int64Flag{
|
cli.Int64Flag{
|
||||||
Name: "expires, e",
|
Name: "expires, e",
|
||||||
@@ -38,7 +37,7 @@ var AuthAddFlags = []cli.Flag{
|
|||||||
// AuthAddCommand configures the command name, flags, and action.
|
// AuthAddCommand configures the command name, flags, and action.
|
||||||
var AuthAddCommand = cli.Command{
|
var AuthAddCommand = cli.Command{
|
||||||
Name: "add",
|
Name: "add",
|
||||||
Usage: "Creates a new client access token and shows it",
|
Usage: "Creates a new client access token",
|
||||||
Flags: AuthAddFlags,
|
Flags: AuthAddFlags,
|
||||||
Action: authAddAction,
|
Action: authAddAction,
|
||||||
}
|
}
|
||||||
@@ -46,11 +45,23 @@ var AuthAddCommand = cli.Command{
|
|||||||
// authAddAction shows detailed session information.
|
// authAddAction shows detailed session information.
|
||||||
func authAddAction(ctx *cli.Context) error {
|
func authAddAction(ctx *cli.Context) error {
|
||||||
return CallWithDependencies(ctx, func(conf *config.Config) error {
|
return CallWithDependencies(ctx, func(conf *config.Config) error {
|
||||||
name := ctx.String("name")
|
// Get username from command flag.
|
||||||
|
userName := ctx.String("user")
|
||||||
|
|
||||||
if name == "" {
|
// Find user account.
|
||||||
|
user := entity.FindUserByName(userName)
|
||||||
|
|
||||||
|
if user == nil && userName != "" {
|
||||||
|
return fmt.Errorf("user %s not found", clean.LogQuote(userName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token name from command flag or ask for it.
|
||||||
|
tokenName := ctx.String("name")
|
||||||
|
|
||||||
|
if tokenName == "" {
|
||||||
prompt := promptui.Prompt{
|
prompt := promptui.Prompt{
|
||||||
Label: "Token Name",
|
Label: "Token Name",
|
||||||
|
Default: rnd.Name(),
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := prompt.Run()
|
res, err := prompt.Run()
|
||||||
@@ -59,24 +70,29 @@ func authAddAction(ctx *cli.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
name = clean.Name(res)
|
tokenName = clean.Name(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a default token name if no specific name has been provided.
|
// Get auth scope from command flag or ask for it.
|
||||||
if name == "" {
|
authScope := ctx.String("scope")
|
||||||
name = rnd.Name()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Username provided?
|
if authScope == "" {
|
||||||
userName := ctx.String("user")
|
prompt := promptui.Prompt{
|
||||||
user := entity.FindUserByName(userName)
|
Label: "Authorization Scope",
|
||||||
|
Default: "*",
|
||||||
|
}
|
||||||
|
|
||||||
if user == nil && userName != "" {
|
res, err := prompt.Run()
|
||||||
return fmt.Errorf("user %s not found", clean.LogQuote(userName))
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
authScope = clean.Scope(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create client session.
|
// Create client session.
|
||||||
sess, err := entity.CreateClientAccessToken(name, ctx.Int64("expires"), ctx.String("scope"), user)
|
sess, err := entity.CreateClientAccessToken(tokenName, ctx.Int64("expires"), authScope, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create access token: %s", err)
|
return fmt.Errorf("failed to create access token: %s", err)
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/photoprism/photoprism/pkg/authn"
|
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/authn"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Usage hints for the client management subcommands.
|
// Usage hints for the client management subcommands.
|
||||||
const (
|
const (
|
||||||
ClientNameUsage = "arbitrary name to help identify the `CLIENT` application"
|
ClientNameUsage = "arbitrary name to help identify the `CLIENT` application"
|
||||||
ClientUserName = "provide a `USERNAME` if the client belongs to a specific user account"
|
ClientUserName = "`USERNAME` of the account the client application belongs to (leave empty for none)"
|
||||||
ClientAuthMethod = "supported authentication `METHOD` for the client application"
|
|
||||||
ClientAuthScope = "authorization `SCOPE` of the client e.g. \"metrics\" or \"photos albums\" (\"*\" to allow all scopes)"
|
ClientAuthScope = "authorization `SCOPE` of the client e.g. \"metrics\" or \"photos albums\" (\"*\" to allow all scopes)"
|
||||||
|
ClientAuthMethod = "supported authentication `METHOD` for the client application"
|
||||||
ClientAuthExpires = "access token lifetime in `SECONDS`, after which a new token must be created by the client (-1 to disable)"
|
ClientAuthExpires = "access token lifetime in `SECONDS`, after which a new token must be created by the client (-1 to disable)"
|
||||||
ClientAuthTokens = "maximum `NUMBER` of access tokens the client can create (-1 to disable)"
|
ClientAuthTokens = "maximum `NUMBER` of access tokens the client can create (-1 to disable)"
|
||||||
ClientRegenerateSecret = "generate a new client secret and display it"
|
ClientRegenerateSecret = "generate a new client secret and display it"
|
||||||
@@ -43,15 +44,16 @@ var ClientAddFlags = []cli.Flag{
|
|||||||
Name: "user, u",
|
Name: "user, u",
|
||||||
Usage: ClientUserName,
|
Usage: ClientUserName,
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
|
||||||
Name: "method, m",
|
|
||||||
Usage: ClientAuthMethod,
|
|
||||||
Value: authn.MethodOAuth2.String(),
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "scope, s",
|
Name: "scope, s",
|
||||||
Usage: ClientAuthScope,
|
Usage: ClientAuthScope,
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "method, m",
|
||||||
|
Usage: ClientAuthMethod,
|
||||||
|
Value: authn.MethodOAuth2.String(),
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
cli.Int64Flag{
|
cli.Int64Flag{
|
||||||
Name: "expires, e",
|
Name: "expires, e",
|
||||||
Usage: ClientAuthExpires,
|
Usage: ClientAuthExpires,
|
||||||
@@ -68,15 +70,16 @@ var ClientModFlags = []cli.Flag{
|
|||||||
Name: "name, n",
|
Name: "name, n",
|
||||||
Usage: ClientNameUsage,
|
Usage: ClientNameUsage,
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
|
||||||
Name: "method, m",
|
|
||||||
Usage: ClientAuthMethod,
|
|
||||||
Value: authn.MethodOAuth2.String(),
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "scope, s",
|
Name: "scope, s",
|
||||||
Usage: ClientAuthScope,
|
Usage: ClientAuthScope,
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "method, m",
|
||||||
|
Usage: ClientAuthMethod,
|
||||||
|
Value: authn.MethodOAuth2.String(),
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
cli.Int64Flag{
|
cli.Int64Flag{
|
||||||
Name: "expires, e",
|
Name: "expires, e",
|
||||||
Usage: ClientAuthExpires,
|
Usage: ClientAuthExpires,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
"github.com/photoprism/photoprism/pkg/list"
|
"github.com/photoprism/photoprism/pkg/list"
|
||||||
"github.com/photoprism/photoprism/pkg/report"
|
"github.com/photoprism/photoprism/pkg/report"
|
||||||
|
"github.com/photoprism/photoprism/pkg/rnd"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClientsAddCommand configures the command name, flags, and action.
|
// ClientsAddCommand configures the command name, flags, and action.
|
||||||
@@ -39,7 +40,8 @@ func clientsAddAction(ctx *cli.Context) error {
|
|||||||
|
|
||||||
if interactive && frm.ClientName == "" {
|
if interactive && frm.ClientName == "" {
|
||||||
prompt := promptui.Prompt{
|
prompt := promptui.Prompt{
|
||||||
Label: "Client Name",
|
Label: "Client Name",
|
||||||
|
Default: rnd.Name(),
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := prompt.Run()
|
res, err := prompt.Run()
|
||||||
|
|||||||
@@ -43,11 +43,11 @@ var ClientFixtures = ClientMap{
|
|||||||
UserName: UserFixtures.Pointer("bob").UserName,
|
UserName: UserFixtures.Pointer("bob").UserName,
|
||||||
user: UserFixtures.Pointer("bob"),
|
user: UserFixtures.Pointer("bob"),
|
||||||
ClientName: "Bob",
|
ClientName: "Bob",
|
||||||
ClientType: authn.ClientWebDAV,
|
ClientType: authn.ClientPublic,
|
||||||
ClientURL: "",
|
ClientURL: "",
|
||||||
CallbackURL: "",
|
CallbackURL: "",
|
||||||
AuthMethod: authn.MethodBasic.String(),
|
AuthMethod: authn.MethodOAuth2.String(),
|
||||||
AuthScope: "webdav files photos",
|
AuthScope: "*",
|
||||||
AuthExpires: 0,
|
AuthExpires: 0,
|
||||||
AuthTokens: -1,
|
AuthTokens: -1,
|
||||||
AuthEnabled: false,
|
AuthEnabled: false,
|
||||||
|
|||||||
@@ -368,7 +368,7 @@ func TestClient_SetFormValues(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var values = form.Client{ClientName: "Annika", AuthMethod: authn.MethodBasic.String(),
|
var values = form.Client{ClientName: "Annika", AuthMethod: authn.MethodOAuth2.String(),
|
||||||
AuthScope: "metrics",
|
AuthScope: "metrics",
|
||||||
AuthExpires: -4000,
|
AuthExpires: -4000,
|
||||||
AuthTokens: -5,
|
AuthTokens: -5,
|
||||||
@@ -389,7 +389,7 @@ func TestClient_SetFormValues(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var values = form.Client{ClientName: "Friend",
|
var values = form.Client{ClientName: "Friend",
|
||||||
AuthMethod: authn.MethodBasic.String(),
|
AuthMethod: authn.MethodOAuth2.String(),
|
||||||
AuthScope: "test",
|
AuthScope: "test",
|
||||||
AuthExpires: 4000000,
|
AuthExpires: 4000000,
|
||||||
AuthTokens: 3000000000,
|
AuthTokens: 3000000000,
|
||||||
|
|||||||
@@ -40,12 +40,7 @@ func NewClientFromCli(ctx *cli.Context) Client {
|
|||||||
|
|
||||||
f.ClientName = clean.Name(ctx.String("name"))
|
f.ClientName = clean.Name(ctx.String("name"))
|
||||||
f.AuthScope = clean.Scope(ctx.String("scope"))
|
f.AuthScope = clean.Scope(ctx.String("scope"))
|
||||||
|
f.AuthMethod = authn.Method(ctx.String("method")).String()
|
||||||
if method := clean.Scope(ctx.String("method")); authn.MethodOAuth2.Equal(method) {
|
|
||||||
f.AuthMethod = authn.MethodOAuth2.String()
|
|
||||||
} else if authn.MethodBasic.Equal(method) {
|
|
||||||
f.AuthMethod = authn.MethodBasic.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
if authn.MethodOAuth2.NotEqual(f.AuthMethod) {
|
if authn.MethodOAuth2.NotEqual(f.AuthMethod) {
|
||||||
f.AuthScope = "webdav"
|
f.AuthScope = "webdav"
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ package authn
|
|||||||
// API client types.
|
// API client types.
|
||||||
const (
|
const (
|
||||||
ClientConfidential = "confidential"
|
ClientConfidential = "confidential"
|
||||||
ClientWebDAV = "webdav"
|
ClientPublic = "public"
|
||||||
ClientUnknown = ""
|
ClientUnknown = ""
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ type MethodType string
|
|||||||
// Authentication methods.
|
// Authentication methods.
|
||||||
const (
|
const (
|
||||||
MethodDefault MethodType = "default"
|
MethodDefault MethodType = "default"
|
||||||
MethodBasic MethodType = "basic"
|
|
||||||
MethodAccessToken MethodType = "access_token"
|
MethodAccessToken MethodType = "access_token"
|
||||||
MethodOAuth2 MethodType = "oauth2"
|
MethodOAuth2 MethodType = "oauth2"
|
||||||
MethodOIDC MethodType = "oidc"
|
MethodOIDC MethodType = "oidc"
|
||||||
|
Method2FA MethodType = "2fa"
|
||||||
MethodUnknown MethodType = ""
|
MethodUnknown MethodType = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,10 +30,12 @@ func (t MethodType) String() string {
|
|||||||
switch t {
|
switch t {
|
||||||
case "":
|
case "":
|
||||||
return string(MethodDefault)
|
return string(MethodDefault)
|
||||||
case "openid":
|
|
||||||
return string(MethodOIDC)
|
|
||||||
case "oauth":
|
case "oauth":
|
||||||
return string(MethodOAuth2)
|
return string(MethodOAuth2)
|
||||||
|
case "openid":
|
||||||
|
return string(MethodOIDC)
|
||||||
|
case "totp":
|
||||||
|
return string(Method2FA)
|
||||||
default:
|
default:
|
||||||
return string(t)
|
return string(t)
|
||||||
}
|
}
|
||||||
@@ -58,6 +60,8 @@ func (t MethodType) Pretty() string {
|
|||||||
return "OAuth2"
|
return "OAuth2"
|
||||||
case MethodOIDC:
|
case MethodOIDC:
|
||||||
return "OIDC"
|
return "OIDC"
|
||||||
|
case Method2FA:
|
||||||
|
return "2FA"
|
||||||
default:
|
default:
|
||||||
return txt.UpperFirst(t.String())
|
return txt.UpperFirst(t.String())
|
||||||
}
|
}
|
||||||
@@ -70,6 +74,10 @@ func Method(s string) MethodType {
|
|||||||
return MethodDefault
|
return MethodDefault
|
||||||
case "oauth2", "oauth":
|
case "oauth2", "oauth":
|
||||||
return MethodOAuth2
|
return MethodOAuth2
|
||||||
|
case "sso":
|
||||||
|
return MethodOIDC
|
||||||
|
case "two-factor", "totp":
|
||||||
|
return Method2FA
|
||||||
default:
|
default:
|
||||||
return MethodType(clean.TypeLower(s))
|
return MethodType(clean.TypeLower(s))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,35 +8,35 @@ import (
|
|||||||
|
|
||||||
func TestMethodType_String(t *testing.T) {
|
func TestMethodType_String(t *testing.T) {
|
||||||
assert.Equal(t, "default", MethodDefault.String())
|
assert.Equal(t, "default", MethodDefault.String())
|
||||||
assert.Equal(t, "basic", MethodBasic.String())
|
|
||||||
assert.Equal(t, "access_token", MethodAccessToken.String())
|
assert.Equal(t, "access_token", MethodAccessToken.String())
|
||||||
assert.Equal(t, "oauth2", MethodOAuth2.String())
|
assert.Equal(t, "oauth2", MethodOAuth2.String())
|
||||||
assert.Equal(t, "oidc", MethodOIDC.String())
|
assert.Equal(t, "oidc", MethodOIDC.String())
|
||||||
|
assert.Equal(t, "2fa", Method2FA.String())
|
||||||
assert.Equal(t, "default", MethodUnknown.String())
|
assert.Equal(t, "default", MethodUnknown.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMethodType_IsDefault(t *testing.T) {
|
func TestMethodType_IsDefault(t *testing.T) {
|
||||||
assert.Equal(t, true, MethodDefault.IsDefault())
|
assert.Equal(t, true, MethodDefault.IsDefault())
|
||||||
assert.Equal(t, false, MethodBasic.IsDefault())
|
|
||||||
assert.Equal(t, false, MethodAccessToken.IsDefault())
|
assert.Equal(t, false, MethodAccessToken.IsDefault())
|
||||||
assert.Equal(t, false, MethodOAuth2.IsDefault())
|
assert.Equal(t, false, MethodOAuth2.IsDefault())
|
||||||
assert.Equal(t, false, MethodOIDC.IsDefault())
|
assert.Equal(t, false, MethodOIDC.IsDefault())
|
||||||
|
assert.Equal(t, false, Method2FA.IsDefault())
|
||||||
assert.Equal(t, true, MethodUnknown.IsDefault())
|
assert.Equal(t, true, MethodUnknown.IsDefault())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMethodType_Pretty(t *testing.T) {
|
func TestMethodType_Pretty(t *testing.T) {
|
||||||
assert.Equal(t, "Default", MethodDefault.Pretty())
|
assert.Equal(t, "Default", MethodDefault.Pretty())
|
||||||
assert.Equal(t, "Basic", MethodBasic.Pretty())
|
|
||||||
assert.Equal(t, "Access Token", MethodAccessToken.Pretty())
|
assert.Equal(t, "Access Token", MethodAccessToken.Pretty())
|
||||||
assert.Equal(t, "OAuth2", MethodOAuth2.Pretty())
|
assert.Equal(t, "OAuth2", MethodOAuth2.Pretty())
|
||||||
assert.Equal(t, "OIDC", MethodOIDC.Pretty())
|
assert.Equal(t, "OIDC", MethodOIDC.Pretty())
|
||||||
|
assert.Equal(t, "2FA", Method2FA.Pretty())
|
||||||
assert.Equal(t, "Default", MethodUnknown.Pretty())
|
assert.Equal(t, "Default", MethodUnknown.Pretty())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMethod(t *testing.T) {
|
func TestMethod(t *testing.T) {
|
||||||
assert.Equal(t, MethodDefault, Method("default"))
|
assert.Equal(t, MethodDefault, Method("default"))
|
||||||
assert.Equal(t, MethodBasic, Method("basic"))
|
|
||||||
assert.Equal(t, MethodAccessToken, Method("access_token"))
|
assert.Equal(t, MethodAccessToken, Method("access_token"))
|
||||||
assert.Equal(t, MethodOAuth2, Method("oauth2"))
|
assert.Equal(t, MethodOAuth2, Method("oauth2"))
|
||||||
assert.Equal(t, MethodOIDC, Method("oidc"))
|
assert.Equal(t, MethodOIDC, Method("oidc"))
|
||||||
|
assert.Equal(t, Method2FA, Method("2fa"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package rnd
|
package rnd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
petname "github.com/dustinkirkland/golang-petname"
|
petname "github.com/dustinkirkland/golang-petname"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,5 +14,5 @@ func Name() string {
|
|||||||
|
|
||||||
// NameN returns a pronounceable name consisting of a random combination of adverbs, an adjective, and a pet name.
|
// NameN returns a pronounceable name consisting of a random combination of adverbs, an adjective, and a pet name.
|
||||||
func NameN(n int) string {
|
func NameN(n int) string {
|
||||||
return petname.Generate(n, "-")
|
return cases.Title(language.English, cases.Compact).String(petname.Generate(n, " "))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
package rnd
|
package rnd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestName(t *testing.T) {
|
func TestName(t *testing.T) {
|
||||||
assert.NotEmpty(t, Name())
|
name := Name()
|
||||||
|
assert.NotEmpty(t, name)
|
||||||
|
assert.Equal(t, 1, strings.Count(name, " "))
|
||||||
|
|
||||||
for n := 0; n < 10; n++ {
|
for n := 0; n < 10; n++ {
|
||||||
s := Name()
|
s := Name()
|
||||||
t.Logf("Name %d: %s", n, s)
|
t.Logf("Name %d: %s", n, s)
|
||||||
assert.NotEmpty(t, Name())
|
assert.NotEmpty(t, s)
|
||||||
|
assert.Equal(t, 1, strings.Count(s, " "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,11 +27,14 @@ func BenchmarkName(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNameN(t *testing.T) {
|
func TestNameN(t *testing.T) {
|
||||||
assert.NotEmpty(t, NameN(2))
|
name := NameN(2)
|
||||||
|
assert.NotEmpty(t, name)
|
||||||
|
assert.Equal(t, 1, strings.Count(name, " "))
|
||||||
|
|
||||||
for n := 0; n < 10; n++ {
|
for n := 0; n < 10; n++ {
|
||||||
s := NameN(n + 1)
|
s := NameN(n + 1)
|
||||||
t.Logf("NameN %d: %s", n, s)
|
t.Logf("NameN %d: %s", n, s)
|
||||||
assert.NotEmpty(t, Name())
|
assert.NotEmpty(t, s)
|
||||||
|
assert.Equal(t, n, strings.Count(s, " "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user