packages feed

status-notifier-item-0.3.2.2: test/WatcherSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module WatcherSpec (spec) where

import Control.Exception (finally)
import Data.Either (isLeft)
import Data.List (isPrefixOf)
import DBus.Client
import qualified StatusNotifier.Watcher.Client as WatcherClient
import Test.Hspec

import TestSupport

spec :: Spec
spec = around withIsolatedSessionBus $ do
  describe "StatusNotifier.Watcher.Service integration" $ do
    it "starts with no registered items" $ \() -> do
      watcher <- startWatcher
      entries <- WatcherClient.getRegisteredSNIEntries watcher
      entries `shouldBe` Right []

    it "registers an item and exposes it via RegisteredSNIEntries" $ \() -> do
      watcher <- startWatcher
      itemClient <- connectSession
      cleanup <- registerSimpleItem itemClient "org.test.ItemA" defaultPath "network-wireless"
      (do
        result <- WatcherClient.getRegisteredSNIEntries watcher
        result `shouldSatisfy` \v ->
          case v of
            Right entries -> any ((== defaultPath) . snd) entries
            Left _ -> False
        ) `finally` cleanup

    it "is idempotent when the same item registers twice" $ \() -> do
      watcher <- startWatcher
      itemClient <- connectSession
      _ <- requestName itemClient "org.test.ItemB" []
      WatcherClient.registerStatusNotifierItem itemClient "org.test.ItemB"
        `shouldReturn` Right ()
      WatcherClient.registerStatusNotifierItem itemClient "org.test.ItemB"
        `shouldReturn` Right ()
      Right entries <- WatcherClient.getRegisteredSNIEntries watcher
      let matches = filter (== ("org.test.ItemB", "/StatusNotifierItem")) entries
      length matches `shouldBe` 1

    it "returns the object path for a registered name with no explicit path" $ \() -> do
      watcher <- startWatcher
      itemClient <- connectSession
      _ <- requestName itemClient "org.test.ItemC" []
      WatcherClient.registerStatusNotifierItem itemClient "org.test.ItemC"
        `shouldReturn` Right ()
      WatcherClient.getObjectPathForItemName watcher "org.test.ItemC"
        `shouldReturn` Right "/StatusNotifierItem"

    it "accepts path-only registration and stores the sender unique name" $ \() -> do
      watcher <- startWatcher
      itemClient <- connectSession
      WatcherClient.registerStatusNotifierItem itemClient "/CustomPath"
        `shouldReturn` Right ()
      Right entries <- WatcherClient.getRegisteredSNIEntries watcher
      let customEntries = filter ((== "/CustomPath") . snd) entries
      length customEntries `shouldBe` 1
      case customEntries of
        (serviceName, _) : _ -> serviceName `shouldSatisfy` (":" `isPrefixOf`)
        _ -> expectationFailure "Expected exactly one custom entry"

    it "returns direct paths from service/path lookups without registration" $ \() -> do
      watcher <- startWatcher
      WatcherClient.getObjectPathForItemName watcher "org.test.Missing/CustomPath"
        `shouldReturn` Right "/CustomPath"

    it "returns an error for unknown services without explicit paths" $ \() -> do
      watcher <- startWatcher
      result <- WatcherClient.getObjectPathForItemName watcher "org.test.Missing"
      result `shouldSatisfy` isLeft

    it "rejects registration for unowned well-known names" $ \() -> do
      watcher <- startWatcher
      itemClient <- connectSession
      result <- WatcherClient.registerStatusNotifierItem itemClient "org.test.Unowned"
      result `shouldSatisfy` isLeft
      Right entries <- WatcherClient.getRegisteredSNIEntries watcher
      entries `shouldNotContain` [("org.test.Unowned", "/StatusNotifierItem")]

    it "deduplicates a client that registers by path and then by service name" $ \() -> do
      watcher <- startWatcher
      itemClient <- connectSession
      WatcherClient.registerStatusNotifierItem itemClient "/StatusNotifierItem"
        `shouldReturn` Right ()
      _ <- requestName itemClient "org.test.PathThenName" []
      WatcherClient.registerStatusNotifierItem itemClient "org.test.PathThenName"
        `shouldReturn` Right ()
      Right entries <- WatcherClient.getRegisteredSNIEntries watcher
      let matching = filter ((== "/StatusNotifierItem") . snd) entries
      length matching `shouldBe` 1

    it "unregisters items when their bus name disappears" $ \() -> do
      watcher <- startWatcher
      itemClient <- connectSession
      cleanup <- registerSimpleItem itemClient "org.test.ItemD" defaultPath "utilities-terminal"
      Right before <- WatcherClient.getRegisteredSNIEntries watcher
      before `shouldContain` [("org.test.ItemD", "/StatusNotifierItem")]
      cleanup
      removed <-
        waitFor 1500000 $ do
          result <- WatcherClient.getRegisteredSNIEntries watcher
          pure $ case result of
            Right entries -> ("org.test.ItemD", "/StatusNotifierItem") `notElem` entries
            Left _ -> False
      removed `shouldBe` True

    it "only unregisters the disconnected item when multiple are present" $ \() -> do
      watcher <- startWatcher
      itemClientA <- connectSession
      itemClientB <- connectSession
      cleanupA <- registerSimpleItem itemClientA "org.test.ItemE" defaultPath "media-playback-start"
      cleanupB <- registerSimpleItem itemClientB "org.test.ItemF" defaultPath "media-playback-stop"
      (do
          Right before <- WatcherClient.getRegisteredSNIEntries watcher
          before `shouldContain` [("org.test.ItemE", "/StatusNotifierItem")]
          before `shouldContain` [("org.test.ItemF", "/StatusNotifierItem")]
          cleanupA
          stable <-
            waitFor 1500000 $ do
              result <- WatcherClient.getRegisteredSNIEntries watcher
              pure $ case result of
                Right entries ->
                  ("org.test.ItemE", "/StatusNotifierItem") `notElem` entries
                    && ("org.test.ItemF", "/StatusNotifierItem") `elem` entries
                Left _ -> False
          stable `shouldBe` True
        ) `finally` cleanupB

defaultPath :: String
defaultPath = "/StatusNotifierItem"