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) {
155155 "snippet file get": {snippetID, "a.txt"},
156156 "notifications list": nil,
157157 "notifications settings show": nil,
158 "notifications device list": nil,
158159 "repo bookmarks": nil,
159160 "search": {"app"},
160161 "mr revisions": {"alice/app", "1"},
internal/control/notifications.go +7 −4
@@ -287,12 +287,15 @@ func runNotificationsDeviceList(c *Ctx, args []string) int {
287287}
288288
289289// 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.
291294func shortToken(t string) string {
292 if len(t) <= 8 {
293 return t
295 if len(t) > 8 {
296 return t[:8] + "…"
294297 }
295 return t[:8] + "…"
298 return "(short token)"
296299}
297300
298301func runNotificationsDeviceRemove(c *Ctx, args []string) int {
internal/control/notifications_test.go +37
@@ -65,6 +65,43 @@ func TestNotificationsDeviceListTruncatesTheToken(t *testing.T) {
6565 }
6666}
6767
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.
72func 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.
91func 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
68105func TestNotificationsSettingsShowsPush(t *testing.T) {
69106 c := notifTestCtx(t, "alice")
70107 var out bytes.Buffer