fix(app): keep token preset cli working on servers without issues; return issue show write errors; correct issue help limits
- commit
- 4f0da009b2c39ff30d4433ae2d428bdc3485b7ec
- author
- brice · · 2026-10-19 06:06 UTC
- committer
- brice · · 2026-10-19 06:07 UTC
- parent
- a4f8ebd
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +68 -15
M internal/app/cli_issue.go +17 -8
| @@ -110,13 +110,16 @@ func newIssueCmd(env *cliEnv) *cobra.Command { | |||
| 110 | 110 | ||
| 111 | 111 | // requireIssues refuses to go on with a server that does not list issues: it | |
| 112 | 112 | // has no IssueService. A server whose features cannot be read is refused too. | |
| 113 | + | // errNoIssues marks a server that lacks the issues feature. | |
| 114 | + | var errNoIssues = errors.New("issues are not available") | |
| 115 | + | ||
| 113 | 116 | func requireIssues(ctx context.Context, c *client.Client) error { | |
| 114 | 117 | res, err := c.Server().GetVersion(ctx, connect.NewRequest(&ermite.GetVersionRequest{})) | |
| 115 | 118 | if err != nil { | |
| 116 | 119 | return c.Describe(err) | |
| 117 | 120 | } | |
| 118 | 121 | if !slices.Contains(res.Msg.GetFeatures(), client.FeatureIssues) { | |
| 119 | - | return fmt.Errorf("the server does not support %s, so it has no issues (upgrade the server, see `ermite version`)", client.FeatureIssues) | |
| 122 | + | return fmt.Errorf("%w: the server does not support %s, so it has no issues (upgrade the server, see `ermite version`)", errNoIssues, client.FeatureIssues) | |
| 120 | 123 | } | |
| 121 | 124 | return nil | |
| 122 | 125 | } | |
| @@ -194,9 +197,9 @@ func newIssueCreateCmd(env *cliEnv) *cobra.Command { | |||
| 194 | 197 | cmd := &cobra.Command{ | |
| 195 | 198 | Use: "create <repo>", | |
| 196 | 199 | Short: "Open an issue", | |
| 197 | - | Long: "Open an issue. The title is one line of at most 120 characters, the body plain text of at most\n" + | |
| 198 | - | "8000; neither may hold control characters (a body may hold line breaks) or bidirectional override\n" + | |
| 199 | - | "characters. --body - and --body-file - read the body from standard input; --body-file reads it\n" + | |
| 200 | + | Long: "Open an issue. The title is one line of at most 120 characters, the body Markdown of at most\n" + | |
| 201 | + | "10,000 characters; disallowed control characters are stripped (a body may hold line breaks).\n" + | |
| 202 | + | "--body - and --body-file - read the body from standard input; --body-file reads it\n" + | |
| 200 | 203 | "from a file otherwise. Needs the issue:write scope." + jsonIssueHelp, | |
| 201 | 204 | Args: cobra.ExactArgs(1), | |
| 202 | 205 | ValidArgsFunction: completeRepoNames(env), | |
| @@ -343,10 +346,16 @@ func printIssueDetail(w io.Writer, d *ermite.GetIssueResponse) error { | |||
| 343 | 346 | if err != nil { | |
| 344 | 347 | return err | |
| 345 | 348 | } | |
| 346 | - | _, _ = fmt.Fprintf(w, "\n%s\n", indentText(body)) | |
| 347 | - | _, _ = fmt.Fprintf(w, "\ncomments (%d)\n", len(d.GetComments())) | |
| 349 | + | if _, err := fmt.Fprintf(w, "\n%s\n", indentText(body)); err != nil { | |
| 350 | + | return err | |
| 351 | + | } | |
| 352 | + | if _, err := fmt.Fprintf(w, "\ncomments (%d)\n", len(d.GetComments())); err != nil { | |
| 353 | + | return err | |
| 354 | + | } | |
| 348 | 355 | for _, c := range d.GetComments() { | |
| 349 | - | _, _ = fmt.Fprintf(w, " #%s %s, %s\n%s\n", c.GetId(), clean(c.GetAuthor()), shortTime(c.GetCreatedAt(), "-"), indentText(c.GetText())) | |
| 356 | + | if _, err := fmt.Fprintf(w, " #%s %s, %s\n%s\n", c.GetId(), clean(c.GetAuthor()), shortTime(c.GetCreatedAt(), "-"), indentText(c.GetText())); err != nil { | |
| 357 | + | return err | |
| 358 | + | } | |
| 350 | 359 | } | |
| 351 | 360 | return nil | |
| 352 | 361 | } | |
| @@ -356,7 +365,7 @@ func newIssueCommentCmd(env *cliEnv) *cobra.Command { | |||
| 356 | 365 | cmd := &cobra.Command{ | |
| 357 | 366 | Use: "comment <repo> <number>", | |
| 358 | 367 | Short: "Comment on an issue", | |
| 359 | - | Long: "Add a plain-text comment (at most 8000 characters, no control characters except line breaks).\n" + | |
| 368 | + | Long: "Add a Markdown comment (at most 10,000 characters; disallowed control characters are stripped).\n" + | |
| 360 | 369 | "-m - reads it from standard input. Needs the issue:write scope." + jsonIssueCommentHelp, | |
| 361 | 370 | Args: cobra.ExactArgs(2), | |
| 362 | 371 | ValidArgsFunction: completeIssueRepoThenNumber(env), | |
| @@ -110,13 +110,16 @@ func newIssueCmd(env *cliEnv) *cobra.Command { | |||||
| 110 | 110 | ||||
| 111 | // requireIssues refuses to go on with a server that does not list issues: it | 111 | // requireIssues refuses to go on with a server that does not list issues: it | ||
| 112 | // has no IssueService. A server whose features cannot be read is refused too. | 112 | // has no IssueService. A server whose features cannot be read is refused too. | ||
| 113 | + | // errNoIssues marks a server that lacks the issues feature. | |||
| 114 | + | var errNoIssues = errors.New("issues are not available") | |||
| 115 | + | ||||
| 113 | func requireIssues(ctx context.Context, c *client.Client) error { | 116 | func requireIssues(ctx context.Context, c *client.Client) error { | ||
| 114 | res, err := c.Server().GetVersion(ctx, connect.NewRequest(&ermite.GetVersionRequest{})) | 117 | res, err := c.Server().GetVersion(ctx, connect.NewRequest(&ermite.GetVersionRequest{})) | ||
| 115 | if err != nil { | 118 | if err != nil { | ||
| 116 | return c.Describe(err) | 119 | return c.Describe(err) | ||
| 117 | } | 120 | } | ||
| 118 | if !slices.Contains(res.Msg.GetFeatures(), client.FeatureIssues) { | 121 | if !slices.Contains(res.Msg.GetFeatures(), client.FeatureIssues) { | ||
| 119 | - | return fmt.Errorf("the server does not support %s, so it has no issues (upgrade the server, see `ermite version`)", client.FeatureIssues) | 122 | + | return fmt.Errorf("%w: the server does not support %s, so it has no issues (upgrade the server, see `ermite version`)", errNoIssues, client.FeatureIssues) |
| 120 | } | 123 | } | ||
| 121 | return nil | 124 | return nil | ||
| 122 | } | 125 | } | ||
| @@ -194,9 +197,9 @@ func newIssueCreateCmd(env *cliEnv) *cobra.Command { | |||||
| 194 | cmd := &cobra.Command{ | 197 | cmd := &cobra.Command{ | ||
| 195 | Use: "create <repo>", | 198 | Use: "create <repo>", | ||
| 196 | Short: "Open an issue", | 199 | Short: "Open an issue", | ||
| 197 | - | Long: "Open an issue. The title is one line of at most 120 characters, the body plain text of at most\n" + | 200 | + | Long: "Open an issue. The title is one line of at most 120 characters, the body Markdown of at most\n" + |
| 198 | - | "8000; neither may hold control characters (a body may hold line breaks) or bidirectional override\n" + | 201 | + | "10,000 characters; disallowed control characters are stripped (a body may hold line breaks).\n" + |
| 199 | - | "characters. --body - and --body-file - read the body from standard input; --body-file reads it\n" + | 202 | + | "--body - and --body-file - read the body from standard input; --body-file reads it\n" + |
| 200 | "from a file otherwise. Needs the issue:write scope." + jsonIssueHelp, | 203 | "from a file otherwise. Needs the issue:write scope." + jsonIssueHelp, | ||
| 201 | Args: cobra.ExactArgs(1), | 204 | Args: cobra.ExactArgs(1), | ||
| 202 | ValidArgsFunction: completeRepoNames(env), | 205 | ValidArgsFunction: completeRepoNames(env), | ||
| @@ -343,10 +346,16 @@ func printIssueDetail(w io.Writer, d *ermite.GetIssueResponse) error { | |||||
| 343 | if err != nil { | 346 | if err != nil { | ||
| 344 | return err | 347 | return err | ||
| 345 | } | 348 | } | ||
| 346 | - | _, _ = fmt.Fprintf(w, "\n%s\n", indentText(body)) | 349 | + | if _, err := fmt.Fprintf(w, "\n%s\n", indentText(body)); err != nil { |
| 347 | - | _, _ = fmt.Fprintf(w, "\ncomments (%d)\n", len(d.GetComments())) | 350 | + | return err |
| 351 | + | } | |||
| 352 | + | if _, err := fmt.Fprintf(w, "\ncomments (%d)\n", len(d.GetComments())); err != nil { | |||
| 353 | + | return err | |||
| 354 | + | } | |||
| 348 | for _, c := range d.GetComments() { | 355 | for _, c := range d.GetComments() { | ||
| 349 | - | _, _ = fmt.Fprintf(w, " #%s %s, %s\n%s\n", c.GetId(), clean(c.GetAuthor()), shortTime(c.GetCreatedAt(), "-"), indentText(c.GetText())) | 356 | + | if _, err := fmt.Fprintf(w, " #%s %s, %s\n%s\n", c.GetId(), clean(c.GetAuthor()), shortTime(c.GetCreatedAt(), "-"), indentText(c.GetText())); err != nil { |
| 357 | + | return err | |||
| 358 | + | } | |||
| 350 | } | 359 | } | ||
| 351 | return nil | 360 | return nil | ||
| 352 | } | 361 | } | ||
| @@ -356,7 +365,7 @@ func newIssueCommentCmd(env *cliEnv) *cobra.Command { | |||||
| 356 | cmd := &cobra.Command{ | 365 | cmd := &cobra.Command{ | ||
| 357 | Use: "comment <repo> <number>", | 366 | Use: "comment <repo> <number>", | ||
| 358 | Short: "Comment on an issue", | 367 | Short: "Comment on an issue", | ||
| 359 | - | Long: "Add a plain-text comment (at most 8000 characters, no control characters except line breaks).\n" + | 368 | + | Long: "Add a Markdown comment (at most 10,000 characters; disallowed control characters are stripped).\n" + |
| 360 | "-m - reads it from standard input. Needs the issue:write scope." + jsonIssueCommentHelp, | 369 | "-m - reads it from standard input. Needs the issue:write scope." + jsonIssueCommentHelp, | ||
| 361 | Args: cobra.ExactArgs(2), | 370 | Args: cobra.ExactArgs(2), | ||
| 362 | ValidArgsFunction: completeIssueRepoThenNumber(env), | 371 | ValidArgsFunction: completeIssueRepoThenNumber(env), | ||
M internal/app/cli_issue_test.go +40 -0
| @@ -3,6 +3,7 @@ | |||
| 3 | 3 | package app | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | + | "errors" | |
| 6 | 7 | "os" | |
| 7 | 8 | "path/filepath" | |
| 8 | 9 | "testing" | |
| @@ -222,3 +223,42 @@ func TestTokenCreateIssueWriteNeedsTheFeature(t *testing.T) { | |||
| 222 | 223 | runCLI(t, envFor(f.serve(t)), "", "token", "create", "x", "--preset", "agent", "--scope", "issue:write").ok(t) | |
| 223 | 224 | assert.Contains(t, lastCreateToken(t, f).GetScopes(), ermite.Scope_SCOPE_ISSUE_WRITE) | |
| 224 | 225 | } | |
| 226 | + | ||
| 227 | + | func TestTokenCreatePresetCLIOnAnOldServerDropsIssueWrite(t *testing.T) { | |
| 228 | + | f := &fakeAPI{getVersion: func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) { | |
| 229 | + | return &ermite.GetVersionResponse{Version: "v0.4.0"}, nil | |
| 230 | + | }, createToken: func(r *ermite.CreateTokenRequest) (*ermite.CreateTokenResponse, error) { | |
| 231 | + | return &ermite.CreateTokenResponse{Token: &ermite.Token{Id: "t", Label: r.GetLabel(), Scopes: r.GetScopes()}, Secret: "ermite_S"}, nil | |
| 232 | + | }} | |
| 233 | + | res := runCLI(t, envFor(f.serve(t)), "", "token", "create", "x", "--preset", "cli").ok(t) | |
| 234 | + | got := lastCreateToken(t, f).GetScopes() | |
| 235 | + | assert.NotContains(t, got, ermite.Scope_SCOPE_ISSUE_WRITE) | |
| 236 | + | assert.NotEmpty(t, got) | |
| 237 | + | assert.Contains(t, res.stderr, "no issue:write") | |
| 238 | + | ||
| 239 | + | res = runCLI(t, envFor(f.serve(t)), "", "token", "create", "x", "--preset", "cli", "--scope", "issue:write") | |
| 240 | + | require.Error(t, res.err) | |
| 241 | + | assert.Contains(t, res.err.Error(), "does not support issues") | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | type failingWriter struct{ after int } | |
| 245 | + | ||
| 246 | + | func (w *failingWriter) Write(p []byte) (int, error) { | |
| 247 | + | if w.after <= 0 { | |
| 248 | + | return 0, errors.New("write failed") | |
| 249 | + | } | |
| 250 | + | w.after-- | |
| 251 | + | return len(p), nil | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | func TestPrintIssueDetailReturnsWriteErrors(t *testing.T) { | |
| 255 | + | d := &ermite.GetIssueResponse{Issue: &ermite.Issue{Repo: "alpha", Number: 1, Title: "t"}, Comments: []*ermite.IssueComment{{Id: "1", Text: "c"}}} | |
| 256 | + | for after := range 60 { | |
| 257 | + | err := printIssueDetail(&failingWriter{after: after}, d) | |
| 258 | + | if err == nil { | |
| 259 | + | return // all writes succeeded: every earlier failure was reported | |
| 260 | + | } | |
| 261 | + | assert.ErrorContains(t, err, "write failed", after) | |
| 262 | + | } | |
| 263 | + | t.Fatal("never completed") | |
| 264 | + | } | |
| @@ -3,6 +3,7 @@ | |||||
| 3 | package app | 3 | package app | ||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | + | "errors" | |||
| 6 | "os" | 7 | "os" | ||
| 7 | "path/filepath" | 8 | "path/filepath" | ||
| 8 | "testing" | 9 | "testing" | ||
| @@ -222,3 +223,42 @@ func TestTokenCreateIssueWriteNeedsTheFeature(t *testing.T) { | |||||
| 222 | runCLI(t, envFor(f.serve(t)), "", "token", "create", "x", "--preset", "agent", "--scope", "issue:write").ok(t) | 223 | runCLI(t, envFor(f.serve(t)), "", "token", "create", "x", "--preset", "agent", "--scope", "issue:write").ok(t) | ||
| 223 | assert.Contains(t, lastCreateToken(t, f).GetScopes(), ermite.Scope_SCOPE_ISSUE_WRITE) | 224 | assert.Contains(t, lastCreateToken(t, f).GetScopes(), ermite.Scope_SCOPE_ISSUE_WRITE) | ||
| 224 | } | 225 | } | ||
| 226 | + | ||||
| 227 | + | func TestTokenCreatePresetCLIOnAnOldServerDropsIssueWrite(t *testing.T) { | |||
| 228 | + | f := &fakeAPI{getVersion: func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) { | |||
| 229 | + | return &ermite.GetVersionResponse{Version: "v0.4.0"}, nil | |||
| 230 | + | }, createToken: func(r *ermite.CreateTokenRequest) (*ermite.CreateTokenResponse, error) { | |||
| 231 | + | return &ermite.CreateTokenResponse{Token: &ermite.Token{Id: "t", Label: r.GetLabel(), Scopes: r.GetScopes()}, Secret: "ermite_S"}, nil | |||
| 232 | + | }} | |||
| 233 | + | res := runCLI(t, envFor(f.serve(t)), "", "token", "create", "x", "--preset", "cli").ok(t) | |||
| 234 | + | got := lastCreateToken(t, f).GetScopes() | |||
| 235 | + | assert.NotContains(t, got, ermite.Scope_SCOPE_ISSUE_WRITE) | |||
| 236 | + | assert.NotEmpty(t, got) | |||
| 237 | + | assert.Contains(t, res.stderr, "no issue:write") | |||
| 238 | + | ||||
| 239 | + | res = runCLI(t, envFor(f.serve(t)), "", "token", "create", "x", "--preset", "cli", "--scope", "issue:write") | |||
| 240 | + | require.Error(t, res.err) | |||
| 241 | + | assert.Contains(t, res.err.Error(), "does not support issues") | |||
| 242 | + | } | |||
| 243 | + | ||||
| 244 | + | type failingWriter struct{ after int } | |||
| 245 | + | ||||
| 246 | + | func (w *failingWriter) Write(p []byte) (int, error) { | |||
| 247 | + | if w.after <= 0 { | |||
| 248 | + | return 0, errors.New("write failed") | |||
| 249 | + | } | |||
| 250 | + | w.after-- | |||
| 251 | + | return len(p), nil | |||
| 252 | + | } | |||
| 253 | + | ||||
| 254 | + | func TestPrintIssueDetailReturnsWriteErrors(t *testing.T) { | |||
| 255 | + | d := &ermite.GetIssueResponse{Issue: &ermite.Issue{Repo: "alpha", Number: 1, Title: "t"}, Comments: []*ermite.IssueComment{{Id: "1", Text: "c"}}} | |||
| 256 | + | for after := range 60 { | |||
| 257 | + | err := printIssueDetail(&failingWriter{after: after}, d) | |||
| 258 | + | if err == nil { | |||
| 259 | + | return // all writes succeeded: every earlier failure was reported | |||
| 260 | + | } | |||
| 261 | + | assert.ErrorContains(t, err, "write failed", after) | |||
| 262 | + | } | |||
| 263 | + | t.Fatal("never completed") | |||
| 264 | + | } | |||
M internal/app/cli_token.go +9 -4
| @@ -133,11 +133,16 @@ func newTokenCreateCmd(env *cliEnv) *cobra.Command { | |||
| 133 | 133 | return err | |
| 134 | 134 | } | |
| 135 | 135 | } | |
| 136 | - | // Only an explicit --scope issue:write is checked: a preset such as cli | |
| 137 | - | // grows with the contract and must keep working on an older server. | |
| 138 | - | if explicitScope(scopes, ermite.Scope_SCOPE_ISSUE_WRITE) { | |
| 136 | + | // A preset such as cli grows with the contract and must keep working on an | |
| 137 | + | // older server: issue:write from a preset alone is dropped there, while an | |
| 138 | + | // explicit --scope issue:write is refused. | |
| 139 | + | if slices.Contains(req.Scopes, ermite.Scope_SCOPE_ISSUE_WRITE) { | |
| 139 | 140 | if err := requireIssues(env.ctx(cmd), c); err != nil { | |
| 140 | - | return fmt.Errorf("%w (needed for the issue:write scope; nothing was created)", err) | |
| 141 | + | if !errors.Is(err, errNoIssues) || explicitScope(scopes, ermite.Scope_SCOPE_ISSUE_WRITE) { | |
| 142 | + | return fmt.Errorf("%w (needed for the issue:write scope; nothing was created)", err) | |
| 143 | + | } | |
| 144 | + | req.Scopes = slices.DeleteFunc(req.Scopes, func(s ermite.Scope) bool { return s == ermite.Scope_SCOPE_ISSUE_WRITE }) | |
| 145 | + | _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "note: the server has no issues feature, so the token has no issue:write scope") | |
| 141 | 146 | } | |
| 142 | 147 | } | |
| 143 | 148 | res, err := c.Tokens().CreateToken(env.ctx(cmd), connect.NewRequest(req)) | |
| @@ -133,11 +133,16 @@ func newTokenCreateCmd(env *cliEnv) *cobra.Command { | |||||
| 133 | return err | 133 | return err | ||
| 134 | } | 134 | } | ||
| 135 | } | 135 | } | ||
| 136 | - | // Only an explicit --scope issue:write is checked: a preset such as cli | 136 | + | // A preset such as cli grows with the contract and must keep working on an |
| 137 | - | // grows with the contract and must keep working on an older server. | 137 | + | // older server: issue:write from a preset alone is dropped there, while an |
| 138 | - | if explicitScope(scopes, ermite.Scope_SCOPE_ISSUE_WRITE) { | 138 | + | // explicit --scope issue:write is refused. |
| 139 | + | if slices.Contains(req.Scopes, ermite.Scope_SCOPE_ISSUE_WRITE) { | |||
| 139 | if err := requireIssues(env.ctx(cmd), c); err != nil { | 140 | if err := requireIssues(env.ctx(cmd), c); err != nil { | ||
| 140 | - | return fmt.Errorf("%w (needed for the issue:write scope; nothing was created)", err) | 141 | + | if !errors.Is(err, errNoIssues) || explicitScope(scopes, ermite.Scope_SCOPE_ISSUE_WRITE) { |
| 142 | + | return fmt.Errorf("%w (needed for the issue:write scope; nothing was created)", err) | |||
| 143 | + | } | |||
| 144 | + | req.Scopes = slices.DeleteFunc(req.Scopes, func(s ermite.Scope) bool { return s == ermite.Scope_SCOPE_ISSUE_WRITE }) | |||
| 145 | + | _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "note: the server has no issues feature, so the token has no issue:write scope") | |||
| 141 | } | 146 | } | ||
| 142 | } | 147 | } | ||
| 143 | res, err := c.Tokens().CreateToken(env.ctx(cmd), connect.NewRequest(req)) | 148 | res, err := c.Tokens().CreateToken(env.ctx(cmd), connect.NewRequest(req)) | ||
M internal/app/cli_token_test.go +2 -3
| @@ -174,12 +174,11 @@ func TestTokenCreatePresetExpansionOnTheWire(t *testing.T) { | |||
| 174 | 174 | "cli": client.AllScopes(), | |
| 175 | 175 | } { | |
| 176 | 176 | t.Run(preset, func(t *testing.T) { | |
| 177 | - | f := &fakeAPI{createToken: func(r *ermite.CreateTokenRequest) (*ermite.CreateTokenResponse, error) { | |
| 177 | + | f := &fakeAPI{getVersion: withIssues, createToken: func(r *ermite.CreateTokenRequest) (*ermite.CreateTokenResponse, error) { | |
| 178 | 178 | return &ermite.CreateTokenResponse{Token: &ermite.Token{Id: "t", Scopes: r.GetScopes()}, Secret: "s"}, nil | |
| 179 | 179 | }} | |
| 180 | 180 | runCLI(t, envFor(f.serve(t)), "", "token", "create", "x", "--preset", preset).ok(t) | |
| 181 | - | _, reqs := f.snapshot() | |
| 182 | - | got := reqs[0].(*ermite.CreateTokenRequest) | |
| 181 | + | got := lastCreateToken(t, f) | |
| 183 | 182 | assert.ElementsMatch(t, want, got.GetScopes()) | |
| 184 | 183 | assert.Empty(t, got.GetRepos()) | |
| 185 | 184 | assert.Nil(t, got.GetExpiresAt()) | |
| @@ -174,12 +174,11 @@ func TestTokenCreatePresetExpansionOnTheWire(t *testing.T) { | |||||
| 174 | "cli": client.AllScopes(), | 174 | "cli": client.AllScopes(), | ||
| 175 | } { | 175 | } { | ||
| 176 | t.Run(preset, func(t *testing.T) { | 176 | t.Run(preset, func(t *testing.T) { | ||
| 177 | - | f := &fakeAPI{createToken: func(r *ermite.CreateTokenRequest) (*ermite.CreateTokenResponse, error) { | 177 | + | f := &fakeAPI{getVersion: withIssues, createToken: func(r *ermite.CreateTokenRequest) (*ermite.CreateTokenResponse, error) { |
| 178 | return &ermite.CreateTokenResponse{Token: &ermite.Token{Id: "t", Scopes: r.GetScopes()}, Secret: "s"}, nil | 178 | return &ermite.CreateTokenResponse{Token: &ermite.Token{Id: "t", Scopes: r.GetScopes()}, Secret: "s"}, nil | ||
| 179 | }} | 179 | }} | ||
| 180 | runCLI(t, envFor(f.serve(t)), "", "token", "create", "x", "--preset", preset).ok(t) | 180 | runCLI(t, envFor(f.serve(t)), "", "token", "create", "x", "--preset", preset).ok(t) | ||
| 181 | - | _, reqs := f.snapshot() | 181 | + | got := lastCreateToken(t, f) |
| 182 | - | got := reqs[0].(*ermite.CreateTokenRequest) | |||
| 183 | assert.ElementsMatch(t, want, got.GetScopes()) | 182 | assert.ElementsMatch(t, want, got.GetScopes()) | ||
| 184 | assert.Empty(t, got.GetRepos()) | 183 | assert.Empty(t, got.GetRepos()) | ||
| 185 | assert.Nil(t, got.GetExpiresAt()) | 184 | assert.Nil(t, got.GetExpiresAt()) | ||