Commit 4b8e4d61f0
4b8e4d61f048101c09f2e41fff3c70ec3158cbde
parent: 74936a4b62
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-20 10:01 UTC
control: mask short device tokens instead of echoing them whole
shortToken returned an 8-characters-or-fewer token unchanged: a full
echo of anything at or under the truncation cut length, since
device add enforces no minimum length. Mask the short case with a
fixed string instead. Cover the boundary and the list command's JSON
path; add the new read command to the e2e read-only fixture.
Ref #89
e2e/readonly_test.go
+1
| @@ -155,6 +155,7 @@ func TestReadOnlyCommandsWriteNothing(t *testing.T) { |
| 155 | 155 | "snippet file get": {snippetID, "a.txt"}, |
| 156 | 156 | "notifications list": nil, |
| 157 | 157 | "notifications settings show": nil, |
| 158 | "notifications device list": nil, |
| 158 | 159 | "repo bookmarks": nil, |
| 159 | 160 | "search": {"app"}, |
| 160 | 161 | "mr revisions": {"alice/app", "1"}, |
internal/control/notifications.go
+7 −4
| @@ -287,12 +287,15 @@ func runNotificationsDeviceList(c *Ctx, args []string) int { |
| 287 | 287 | } |
| 288 | 288 | |
| 289 | 289 | // shortToken renders a device token as its first eight characters. Enough |
| 290 | | // to tell two devices apart in a list, not enough to push to one. |
| 290 | // to tell two devices apart in a list, not enough to push to one. A real |
| 291 | // APNs token is 64 hex characters, so anything at or under the cut length |
| 292 | // is not a token worth showing part of — it is masked outright rather |
| 293 | // than echoed whole, which "abc…" would imply is a truncation. |
| 291 | 294 | func shortToken(t string) string { |
| 292 | | if len(t) <= 8 { |
| 293 | | return t |
| 295 | if len(t) > 8 { |
| 296 | return t[:8] + "…" |
| 294 | 297 | } |
| 295 | | return t[:8] + "…" |
| 298 | return "(short token)" |
| 296 | 299 | } |
| 297 | 300 | |
| 298 | 301 | func runNotificationsDeviceRemove(c *Ctx, args []string) int { |
internal/control/notifications_test.go
+37
| @@ -65,6 +65,43 @@ func TestNotificationsDeviceListTruncatesTheToken(t *testing.T) { |
| 65 | 65 | } |
| 66 | 66 | } |
| 67 | 67 | |
| 68 | // TestNotificationsDeviceListTruncatesTheTokenJSON is the JSON-path twin |
| 69 | // of the above: the plain and JSON output share the same rows slice, but |
| 70 | // nothing enforces that beyond reading the code, so both paths get their |
| 71 | // own test of the guarantee. |
| 72 | func TestNotificationsDeviceListTruncatesTheTokenJSON(t *testing.T) { |
| 73 | c := notifTestCtx(t, "alice") |
| 74 | long := strings.Repeat("a", 64) |
| 75 | c.Store.AddPushDevice(c.User.ID, long, "iphone") |
| 76 | var out bytes.Buffer |
| 77 | c.Stdout, c.JSON = &out, true |
| 78 | if code := runNotificationsDeviceList(c, nil); code != 0 { |
| 79 | t.Fatalf("exit %d", code) |
| 80 | } |
| 81 | if strings.Contains(out.String(), long) { |
| 82 | t.Fatal("the full token was printed") |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | // TestNotificationsDeviceListMasksAShortToken: a token at or under the |
| 87 | // truncation cut length is not returned unchanged. shortToken's short |
| 88 | // path used to return the token verbatim, a full echo of anything eight |
| 89 | // characters or fewer; runNotificationsDeviceAdd enforces no minimum |
| 90 | // length, so a short token is a value the command will store. |
| 91 | func TestNotificationsDeviceListMasksAShortToken(t *testing.T) { |
| 92 | c := notifTestCtx(t, "alice") |
| 93 | short := "abc123" |
| 94 | c.Store.AddPushDevice(c.User.ID, short, "iphone") |
| 95 | var out bytes.Buffer |
| 96 | c.Stdout = &out |
| 97 | if code := runNotificationsDeviceList(c, nil); code != 0 { |
| 98 | t.Fatalf("exit %d", code) |
| 99 | } |
| 100 | if strings.Contains(out.String(), short) { |
| 101 | t.Fatal("the short token was printed verbatim") |
| 102 | } |
| 103 | } |
| 104 | |
| 68 | 105 | func TestNotificationsSettingsShowsPush(t *testing.T) { |
| 69 | 106 | c := notifTestCtx(t, "alice") |
| 70 | 107 | var out bytes.Buffer |