aboutsummaryrefslogtreecommitdiffstats
path: root/internal/notify/service_test.go
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-15 18:01:49 +0200
committerDanilo M. <danix@danix.xyz>2026-09-15 18:01:49 +0200
commit7a6b1c1ada3ba6ca2ca4ed70cf51f2f5f30d7b1e (patch)
tree6b0e0ffd76f0de162093a56d1799847a8b81ade5 /internal/notify/service_test.go
parent3b8a0e0f4a5c28394ac578e8559bf57ae82d3fd2 (diff)
downloadnotifyd-7a6b1c1ada3ba6ca2ca4ed70cf51f2f5f30d7b1e.tar.gz
notifyd-7a6b1c1ada3ba6ca2ca4ed70cf51f2f5f30d7b1e.zip
fix(notify): cover expiry cleanup and harden the image-removal guard
Split the dismiss test so expiry is exercised on its own: Expire removes the image without deleting the inert entry, and nothing asserted it. removeImage now requires filepath.Dir(filepath.Clean(path)) to equal the image directory, so a path carrying .. cannot reach a sibling daemon file such as queue.json. The previous prefix test accepted it. Not exploitable until Task 4 populates Popup.Image from a client hint, which is exactly why the guard is fixed now.
Diffstat (limited to 'internal/notify/service_test.go')
-rw-r--r--internal/notify/service_test.go34
1 files changed, 34 insertions, 0 deletions
diff --git a/internal/notify/service_test.go b/internal/notify/service_test.go
index fb47b98..871a0c7 100644
--- a/internal/notify/service_test.go
+++ b/internal/notify/service_test.go
@@ -144,6 +144,40 @@ func TestCloseNotificationEmitsReasonThree(t *testing.T) {
}
}
+func TestRemoveImageRefusesPathsOutsideTheImageDir(t *testing.T) {
+ dir := t.TempDir()
+ imgDir := ImagesDir(dir)
+ if err := os.MkdirAll(imgDir, 0o700); err != nil {
+ t.Fatal(err)
+ }
+ sibling := filepath.Join(dir, "queue.json")
+ if err := os.WriteFile(sibling, []byte("x"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ owned := filepath.Join(imgDir, "1.png")
+ if err := os.WriteFile(owned, []byte("x"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ s := &Service{dir: dir}
+
+ s.removeImage(filepath.Join(imgDir, "..", "queue.json"))
+ if _, err := os.Stat(sibling); err != nil {
+ t.Fatalf("traversal removed a sibling: %v", err)
+ }
+ s.removeImage(sibling)
+ if _, err := os.Stat(sibling); err != nil {
+ t.Fatalf("unrelated path removed a sibling: %v", err)
+ }
+ s.removeImage("")
+ if _, err := os.Stat(sibling); err != nil {
+ t.Fatalf("empty path removed a sibling: %v", err)
+ }
+ s.removeImage(owned)
+ if _, err := os.Stat(owned); !os.IsNotExist(err) {
+ t.Fatalf("owned image not removed: %v", err)
+ }
+}
+
// A natural expiry must drop the timer entry, or the map grows by one timer per
// id for the whole session. No bus is needed: the store's emit is a no-op here,
// so the test exercises arm's closure directly.