Skip to content

Commit b01643f

Browse files
committed
Add user email change commands
1 parent 3e3e2cc commit b01643f

4 files changed

Lines changed: 191 additions & 1 deletion

File tree

SURFACE.txt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ CMD fizzy upload help
200200
CMD fizzy user
201201
CMD fizzy user avatar-remove
202202
CMD fizzy user deactivate
203+
CMD fizzy user email-change-confirm
204+
CMD fizzy user email-change-request
203205
CMD fizzy user export-create
204206
CMD fizzy user export-show
205207
CMD fizzy user help
@@ -2819,6 +2821,35 @@ FLAG fizzy user deactivate --quiet type=bool
28192821
FLAG fizzy user deactivate --styled type=bool
28202822
FLAG fizzy user deactivate --token type=string
28212823
FLAG fizzy user deactivate --verbose type=bool
2824+
FLAG fizzy user email-change-confirm --agent type=bool
2825+
FLAG fizzy user email-change-confirm --api-url type=string
2826+
FLAG fizzy user email-change-confirm --count type=bool
2827+
FLAG fizzy user email-change-confirm --help type=bool
2828+
FLAG fizzy user email-change-confirm --ids-only type=bool
2829+
FLAG fizzy user email-change-confirm --jq type=string
2830+
FLAG fizzy user email-change-confirm --json type=bool
2831+
FLAG fizzy user email-change-confirm --limit type=int
2832+
FLAG fizzy user email-change-confirm --markdown type=bool
2833+
FLAG fizzy user email-change-confirm --profile type=string
2834+
FLAG fizzy user email-change-confirm --quiet type=bool
2835+
FLAG fizzy user email-change-confirm --styled type=bool
2836+
FLAG fizzy user email-change-confirm --token type=string
2837+
FLAG fizzy user email-change-confirm --verbose type=bool
2838+
FLAG fizzy user email-change-request --agent type=bool
2839+
FLAG fizzy user email-change-request --api-url type=string
2840+
FLAG fizzy user email-change-request --count type=bool
2841+
FLAG fizzy user email-change-request --email type=string
2842+
FLAG fizzy user email-change-request --help type=bool
2843+
FLAG fizzy user email-change-request --ids-only type=bool
2844+
FLAG fizzy user email-change-request --jq type=string
2845+
FLAG fizzy user email-change-request --json type=bool
2846+
FLAG fizzy user email-change-request --limit type=int
2847+
FLAG fizzy user email-change-request --markdown type=bool
2848+
FLAG fizzy user email-change-request --profile type=string
2849+
FLAG fizzy user email-change-request --quiet type=bool
2850+
FLAG fizzy user email-change-request --styled type=bool
2851+
FLAG fizzy user email-change-request --token type=string
2852+
FLAG fizzy user email-change-request --verbose type=bool
28222853
FLAG fizzy user export-create --agent type=bool
28232854
FLAG fizzy user export-create --api-url type=string
28242855
FLAG fizzy user export-create --count type=bool
@@ -3360,6 +3391,8 @@ SUB fizzy upload help
33603391
SUB fizzy user
33613392
SUB fizzy user avatar-remove
33623393
SUB fizzy user deactivate
3394+
SUB fizzy user email-change-confirm
3395+
SUB fizzy user email-change-request
33633396
SUB fizzy user export-create
33643397
SUB fizzy user export-show
33653398
SUB fizzy user help

internal/commands/user.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,75 @@ var userExportShowCmd = &cobra.Command{
338338
},
339339
}
340340

341+
var userEmailChangeRequestEmail string
342+
343+
var userEmailChangeRequestCmd = &cobra.Command{
344+
Use: "email-change-request USER_ID",
345+
Short: "Request a user email address change",
346+
Long: "Requests an email address change for a user.",
347+
Args: cobra.ExactArgs(1),
348+
RunE: func(cmd *cobra.Command, args []string) error {
349+
if err := requireAuthAndAccount(); err != nil {
350+
return err
351+
}
352+
if userEmailChangeRequestEmail == "" {
353+
return newRequiredFlagError("email")
354+
}
355+
356+
userID := args[0]
357+
resp, err := getSDK().Users().RequestEmailAddressChange(cmd.Context(), userID, &generated.RequestEmailAddressChangeRequest{
358+
EmailAddress: userEmailChangeRequestEmail,
359+
})
360+
if err != nil {
361+
return convertSDKError(err)
362+
}
363+
364+
data := normalizeAny(resp.Data)
365+
if data == nil {
366+
data = map[string]any{"requested": true}
367+
}
368+
369+
breadcrumbs := []Breadcrumb{
370+
breadcrumb("user", fmt.Sprintf("fizzy user show %s", userID), "View user"),
371+
}
372+
373+
printMutation(data, "", breadcrumbs)
374+
return nil
375+
},
376+
}
377+
378+
var userEmailChangeConfirmCmd = &cobra.Command{
379+
Use: "email-change-confirm USER_ID TOKEN",
380+
Short: "Confirm a user email address change",
381+
Long: "Confirms an email address change for a user.",
382+
Args: cobra.ExactArgs(2),
383+
RunE: func(cmd *cobra.Command, args []string) error {
384+
if err := requireAuthAndAccount(); err != nil {
385+
return err
386+
}
387+
388+
userID := args[0]
389+
token := args[1]
390+
391+
resp, err := getSDK().Users().ConfirmEmailAddressChange(cmd.Context(), userID, token)
392+
if err != nil {
393+
return convertSDKError(err)
394+
}
395+
396+
data := normalizeAny(resp.Data)
397+
if data == nil {
398+
data = map[string]any{"confirmed": true}
399+
}
400+
401+
breadcrumbs := []Breadcrumb{
402+
breadcrumb("user", fmt.Sprintf("fizzy user show %s", userID), "View user"),
403+
}
404+
405+
printMutation(data, "", breadcrumbs)
406+
return nil
407+
},
408+
}
409+
341410
// Push subscription create flags
342411
var pushSubCreateUser string
343412
var pushSubCreateEndpoint string
@@ -447,6 +516,11 @@ func init() {
447516
userCmd.AddCommand(userExportCreateCmd)
448517
userCmd.AddCommand(userExportShowCmd)
449518

519+
// Email change
520+
userEmailChangeRequestCmd.Flags().StringVar(&userEmailChangeRequestEmail, "email", "", "New email address (required)")
521+
userCmd.AddCommand(userEmailChangeRequestCmd)
522+
userCmd.AddCommand(userEmailChangeConfirmCmd)
523+
450524
// Push subscriptions
451525
userPushSubscriptionCreateCmd.Flags().StringVar(&pushSubCreateUser, "user", "", "User ID (required)")
452526
userPushSubscriptionCreateCmd.Flags().StringVar(&pushSubCreateEndpoint, "endpoint", "", "Push endpoint URL (required)")

internal/commands/user_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,87 @@ func TestUserExport(t *testing.T) {
301301
})
302302
}
303303

304+
func TestUserEmailChange(t *testing.T) {
305+
t.Run("requests email change", func(t *testing.T) {
306+
mock := NewMockClient()
307+
mock.PostResponse = &client.APIResponse{StatusCode: 204, Data: nil}
308+
309+
result := SetTestModeWithSDK(mock)
310+
SetTestConfig("token", "account", "https://api.example.com")
311+
defer resetTest()
312+
313+
userEmailChangeRequestEmail = "new@example.com"
314+
err := userEmailChangeRequestCmd.RunE(userEmailChangeRequestCmd, []string{"user-1"})
315+
userEmailChangeRequestEmail = ""
316+
317+
assertExitCode(t, err, 0)
318+
if mock.PostCalls[0].Path != "/users/user-1/email_addresses.json" {
319+
t.Errorf("expected path '/users/user-1/email_addresses.json', got '%s'", mock.PostCalls[0].Path)
320+
}
321+
body := mock.PostCalls[0].Body.(map[string]any)
322+
if body["email_address"] != "new@example.com" {
323+
t.Errorf("expected email_address 'new@example.com', got '%v'", body["email_address"])
324+
}
325+
data, ok := result.Response.Data.(map[string]any)
326+
if !ok || data["requested"] != true {
327+
t.Fatalf("expected explicit requested=true payload, got %#v", result.Response.Data)
328+
}
329+
})
330+
331+
t.Run("confirms email change", func(t *testing.T) {
332+
mock := NewMockClient()
333+
mock.PostResponse = &client.APIResponse{StatusCode: 204, Data: nil}
334+
335+
result := SetTestModeWithSDK(mock)
336+
SetTestConfig("token", "account", "https://api.example.com")
337+
defer resetTest()
338+
339+
err := userEmailChangeConfirmCmd.RunE(userEmailChangeConfirmCmd, []string{"user-1", "token-123"})
340+
341+
assertExitCode(t, err, 0)
342+
if mock.PostCalls[0].Path != "/users/user-1/email_addresses/token-123/confirmation.json" {
343+
t.Errorf("expected confirmation path, got '%s'", mock.PostCalls[0].Path)
344+
}
345+
data, ok := result.Response.Data.(map[string]any)
346+
if !ok || data["confirmed"] != true {
347+
t.Fatalf("expected explicit confirmed=true payload, got %#v", result.Response.Data)
348+
}
349+
})
350+
351+
t.Run("requires email flag", func(t *testing.T) {
352+
mock := NewMockClient()
353+
SetTestModeWithSDK(mock)
354+
SetTestConfig("token", "account", "https://api.example.com")
355+
defer resetTest()
356+
357+
userEmailChangeRequestEmail = ""
358+
err := userEmailChangeRequestCmd.RunE(userEmailChangeRequestCmd, []string{"user-1"})
359+
assertExitCode(t, err, errors.ExitInvalidArgs)
360+
})
361+
362+
t.Run("request requires authentication", func(t *testing.T) {
363+
mock := NewMockClient()
364+
SetTestModeWithSDK(mock)
365+
SetTestConfig("", "account", "https://api.example.com")
366+
defer resetTest()
367+
368+
userEmailChangeRequestEmail = "new@example.com"
369+
err := userEmailChangeRequestCmd.RunE(userEmailChangeRequestCmd, []string{"user-1"})
370+
userEmailChangeRequestEmail = ""
371+
assertExitCode(t, err, errors.ExitAuthFailure)
372+
})
373+
374+
t.Run("confirm requires authentication", func(t *testing.T) {
375+
mock := NewMockClient()
376+
SetTestModeWithSDK(mock)
377+
SetTestConfig("", "account", "https://api.example.com")
378+
defer resetTest()
379+
380+
err := userEmailChangeConfirmCmd.RunE(userEmailChangeConfirmCmd, []string{"user-1", "token-123"})
381+
assertExitCode(t, err, errors.ExitAuthFailure)
382+
})
383+
}
384+
304385
func TestUserPushSubscriptionCreate(t *testing.T) {
305386
t.Run("creates push subscription", func(t *testing.T) {
306387
mock := NewMockClient()

skills/fizzy/SKILL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ Want to change something?
109109
| step | `step list --card NUMBER` | `step show ID --card NUMBER` | `step create` | `step update ID` | `step delete ID` | - |
110110
| reaction | `reaction list` | - | `reaction create` | - | `reaction delete ID` | - |
111111
| tag | `tag list` | - | - | - | - | - |
112-
| user | `user list` | `user show ID`, `user export-show USER_ID EXPORT_ID` | `user export-create USER_ID` | `user update ID` | - | `user deactivate ID`, `user role ID`, `user avatar-remove ID`, `user push-subscription-create`, `user push-subscription-delete ID` |
112+
| user | `user list` | `user show ID`, `user export-show USER_ID EXPORT_ID` | `user export-create USER_ID`, `user email-change-request USER_ID --email user@example.com`, `user email-change-confirm USER_ID TOKEN` | `user update ID` | - | `user deactivate ID`, `user role ID`, `user avatar-remove ID`, `user push-subscription-create`, `user push-subscription-delete ID` |
113113
| notification | `notification list` | - | - | - | - | `notification tray`, `notification read-all`, `notification settings-show`, `notification settings-update` |
114114
| pin | `pin list` | - | - | - | - | `card pin NUMBER`, `card unpin NUMBER` |
115115
| webhook | `webhook list --board ID`, `webhook deliveries --board ID WEBHOOK_ID` | `webhook show ID --board ID` | `webhook create` | `webhook update ID` | `webhook delete ID` | `webhook reactivate ID` |
@@ -732,6 +732,8 @@ fizzy user role USER_ID --role ROLE # Update user role (requires admi
732732
fizzy user avatar-remove USER_ID # Remove user avatar
733733
fizzy user export-create USER_ID # Create user data export
734734
fizzy user export-show USER_ID EXPORT_ID # Show user data export status
735+
fizzy user email-change-request USER_ID --email user@example.com
736+
fizzy user email-change-confirm USER_ID TOKEN
735737
fizzy user push-subscription-create --user ID --endpoint URL --p256dh-key KEY --auth-key KEY
736738
fizzy user push-subscription-delete SUB_ID --user ID
737739
```

0 commit comments

Comments
 (0)