diff --git a/freesound.cabal b/freesound.cabal
--- a/freesound.cabal
+++ b/freesound.cabal
@@ -1,5 +1,5 @@
 name:               freesound
-version:            0.2.0
+version:            0.3.0
 synopsis:           Access the Freesound Project database
 description:        Access the Freesound Project database. The Freesound
                     Project is a collaborative database of Creative Commons
@@ -17,7 +17,7 @@
 stability:          experimental
 homepage:           https://github.com/kaoskorobase/freesound
 bug-reports:        https://github.com/kaoskorobase/freesound/issues
-tested-with:        GHC == 6.10.1, GHC == 6.10.4, GHC == 6.12.1, GHC == 7.10.3
+tested-with:        GHC == 7.8, GHC == 7.10
 build-type:         Simple
 cabal-version:      >= 1.8
 
@@ -31,19 +31,19 @@
 library
   exposed-modules:  Sound.Freesound
                     Sound.Freesound.Bookmark
+                    Sound.Freesound.Comment
                     Sound.Freesound.List
                     Sound.Freesound.Pack
                     Sound.Freesound.Search
-                    Sound.Freesound.Search.Filter
-                    Sound.Freesound.Search.Numerical
-                    Sound.Freesound.Search.Query
                     Sound.Freesound.Sound
                     Sound.Freesound.User
   other-modules:    Sound.Freesound.API
-                    Sound.Freesound.Bookmark.Internal
                     Sound.Freesound.Pack.Type
+                    Sound.Freesound.Search.Filter
+                    Sound.Freesound.Search.Numerical
+                    Sound.Freesound.Search.Query
                     Sound.Freesound.Sound.Type
-                    Sound.Freesound.User.Type
+                    Sound.Freesound.Time
   build-depends:    base >= 3 && < 5
                   , aeson >= 0.6
                   , blaze-builder >= 0.3
@@ -73,7 +73,14 @@
   hs-source-dirs:   test
   main-is:          Spec.hs
   other-modules:    Sound.Freesound.Test
+                    Sound.Freesound.SearchSpec
+                    Sound.Freesound.SoundSpec
+                    Sound.Freesound.UserSpec
   build-depends:    base == 4.*
+                  , aeson >= 0.6
+                  , bytestring >= 0.9
+                  , containers
                   , data-default
                   , freesound
                   , hspec
+                  , hspec-core
diff --git a/src/Sound/Freesound.hs b/src/Sound/Freesound.hs
--- a/src/Sound/Freesound.hs
+++ b/src/Sound/Freesound.hs
@@ -7,32 +7,15 @@
 --
 -- > import qualified Network.HTTP.Conduit as HTTP
 
-module Sound.Freesound
-(
-  -- * Searching
-  module Sound.Freesound.Search
-, module Sound.Freesound.Search.Query
-, module Sound.Freesound.List
-  -- * Users
-, getUser
-, getUserByName
-, getBookmarkCategories
-, getSounds
-, getSounds_
-, getPacks
-  -- * Sounds
-, search
-, search_
-, getSimilar
-, getSimilar_
-  -- * Freesound API monad
-, module Sound.Freesound.API
-)
-where
+module Sound.Freesound (
+    -- * Freesound API monad
+    module Sound.Freesound.API
+    -- * Result lists
+  , module Sound.Freesound.List
+    -- * Users
+  , getUserByName
+) where
 
-import Sound.Freesound.API (Freesound, runFreesound, APIKey, apiKeyFromString)
+import Sound.Freesound.API (Freesound, runFreesound, APIKey, apiKeyFromString, get, download, downloadToFile)
 import Sound.Freesound.List
-import Sound.Freesound.Search
-import Sound.Freesound.Search.Query
-import Sound.Freesound.User
-import Sound.Freesound.Sound
+import Sound.Freesound.User (getUserByName)
diff --git a/src/Sound/Freesound/API.hs b/src/Sound/Freesound/API.hs
--- a/src/Sound/Freesound/API.hs
+++ b/src/Sound/Freesound/API.hs
@@ -7,20 +7,21 @@
 , runFreesound
 -- , request
 , URI
-, getURI
-, Resource
+, download
+, downloadToFile
+, Resource(..)
 , resourceURI
 , appendQuery
-, getResource
+, get
 ) where
 
 import qualified Blaze.ByteString.Builder as Builder
 import qualified Blaze.ByteString.Builder.Char8 as Builder
-import           Control.Monad (liftM, mzero)
 import           Control.Monad.IO.Class (MonadIO, liftIO)
 import qualified Control.Monad.Reader as R
 import           Control.Lens
-import           Data.Aeson as J
+import           Data.Aeson
+import           Data.Aeson.Types (typeMismatch)
 import qualified Data.ByteString.Char8 as BS
 import qualified Data.ByteString.Lazy.Char8 as BL
 import           Data.Text (Text)
@@ -64,23 +65,31 @@
 newtype URI = URI URI.URI deriving (Eq, Show)
 
 instance FromJSON URI where
-  parseJSON (String v) = maybe mzero return (parseURI' (T.unpack v))
-  parseJSON _ = mzero
+  parseJSON (String v) =
+    maybe (fail "Couldn't parse URI")
+          return
+          (parseURI' (T.unpack v))
+  parseJSON v = typeMismatch "URI" v
 
 -- | Cover up for Freesound sloppiness.
 parseURI' :: String -> Maybe URI
 parseURI' = fmap URI . URI.parseURI . URI.escapeURIString URI.isAllowedInURI
 
 -- | Download the data referred to by a URI.
-getURI :: URI -> Freesound BL.ByteString
-getURI (URI u) = do
+download :: URI -> Freesound BL.ByteString
+download (URI u) = do
   s <- R.asks session
   APIKey k <- R.asks apiKey
   let opts = HTTP.defaults & HTTP.header "Authorization" .~ ["Token " `BS.append` k]
       u' = URI.uriToString id u ""
+  -- liftIO $ print u'
   r <- liftIO $ Session.getWith opts s u'
   return $ r ^. HTTP.responseBody
 
+-- | Download the data referred to by a URI to a file.
+downloadToFile :: FilePath -> URI -> Freesound ()
+downloadToFile path uri = liftIO . BL.writeFile path =<< download uri
+
 -- | Resource URI.
 newtype Resource a = Resource URI deriving (Eq, FromJSON, Show)
 
@@ -93,19 +102,12 @@
            $ HTTP.parseQuery (BS.pack (URI.uriQuery u))
               ++ HTTP.toQuery q
 
--- | Create a query item for the API key.
--- apiKeyQuery :: Freesound HTTP.Query
--- apiKeyQuery = do
---   k <- Freesound $ R.asks apiKey
---   return $ [("api_key", Just k)]
-
--- | Download the resource referred to by a URI.
-getResource :: (FromJSON a) => Resource a -> Freesound a
-getResource (Resource u) = do
-  -- q <- apiKeyQuery
-  -- let Resource u = appendQuery q r
-  liftM (handle . J.eitherDecode) $ getURI u
-  where handle = either (\e -> error $ "Internal error (getResource): JSON decoding failed: " ++ e) id
+-- | Get the resource referred to by a URI.
+get :: (FromJSON a) => Resource a -> Freesound a
+get (Resource u) = do
+  handle . eitherDecode <$> download u
+  -- TODO: Proper error handling
+  where handle = either (\e -> error $ "JSON decoding failed: " ++ e) id
 
 -- | The base URI of the Freesound API.
 baseURI :: Builder.Builder
@@ -120,9 +122,3 @@
                . mappend baseURI
                . HTTP.encodePath path
                $ query
-
--- | Perform an HTTP request given the method, a URI and an optional body and return the response body as a Conduit source.
--- request :: Monad m => HTTP.Method -> URI -> Maybe (C.Source m BS.ByteString) -> FreesoundT m (C.ResumableSource m BS.ByteString)
--- request method (URI uri) body = do
---   f <- FreesoundT $ R.asks httpRequest
---   lift $ f method uri body
diff --git a/src/Sound/Freesound/Bookmark.hs b/src/Sound/Freesound/Bookmark.hs
--- a/src/Sound/Freesound/Bookmark.hs
+++ b/src/Sound/Freesound/Bookmark.hs
@@ -1,50 +1,30 @@
 {-# LANGUAGE CPP, OverloadedStrings #-}
 module Sound.Freesound.Bookmark (
-  Bookmark
-, sound
-, name
-, Category
-, category
-, url
-, sounds
-, getSounds
+  BookmarkCategory(..)
 ) where
 
-import           Control.Monad (mzero)
 import           Data.Aeson
 import           Data.Text (Text)
-import           Sound.Freesound.API (Freesound, Resource, URI, getResource)
+import           Sound.Freesound.API (Resource, URI)
+import           Sound.Freesound.List (List)
 import qualified Sound.Freesound.Sound.Type as Sound
 
 #if __GLASGOW_HASKELL__ < 710
 import           Control.Applicative
 #endif
 
-data Bookmark = Bookmark {
-  sound :: Sound.Summary
-, name :: Text
-} deriving (Eq, Show)
-
-instance FromJSON Bookmark where
-  parseJSON j@(Object v) =
-    Bookmark
-    <$> parseJSON j
-    <*> v .: "bookmark_name"
-  parseJSON _ = mzero
-
-data Category = Category {
-  category :: Text
-, url :: URI
-, sounds :: Resource [Bookmark]
+data BookmarkCategory = BookmarkCategory {
+    url :: URI
+  , name :: Text
+  , numSounds :: Int
+  , sounds :: Resource (List Sound.Summary)
 } deriving (Eq, Show)
 
-instance FromJSON Category where
+instance FromJSON BookmarkCategory where
   parseJSON (Object v) =
-    Category
-    <$> v .: "name"
-    <*> v .: "url"
-    <*> v .: "sounds"
-  parseJSON _ = mzero
-
-getSounds :: Category -> Freesound [Bookmark]
-getSounds = getResource . sounds
+    BookmarkCategory
+      <$> v .: "url"
+      <*> v .: "name"
+      <*> v .: "num_sounds"
+      <*> v .: "sounds"
+  parseJSON _ = fail "Couldn't parse BookmarkCategory"
diff --git a/src/Sound/Freesound/Bookmark/Internal.hs b/src/Sound/Freesound/Bookmark/Internal.hs
deleted file mode 100644
--- a/src/Sound/Freesound/Bookmark/Internal.hs
+++ /dev/null
@@ -1,24 +0,0 @@
-{-# LANGUAGE CPP, OverloadedStrings #-}
-module Sound.Freesound.Bookmark.Internal (
-  Categories(..)
-) where
-
-import           Control.Monad (mzero)
-import           Data.Aeson
-import           Sound.Freesound.Bookmark
-
-#if __GLASGOW_HASKELL__ < 710
-import           Control.Applicative
-#endif
-
-data Categories = Categories {
-  numCategories :: Int
-, categories :: [Category]
-} deriving (Eq, Show)
-
-instance FromJSON Categories where
-  parseJSON (Object v) =
-    Categories
-    <$> v .: "num_results"
-    <*> v .: "categories"
-  parseJSON _ = mzero
diff --git a/src/Sound/Freesound/Comment.hs b/src/Sound/Freesound/Comment.hs
new file mode 100644
--- /dev/null
+++ b/src/Sound/Freesound/Comment.hs
@@ -0,0 +1,26 @@
+{-# LANGUAGE CPP, OverloadedStrings #-}
+module Sound.Freesound.Comment (
+  Comment(..)
+) where
+
+import           Data.Aeson (FromJSON(..), Value(..), (.:))
+import           Data.Aeson.Types (typeMismatch)
+import           Data.Text (Text)
+import           Sound.Freesound.Time
+
+#if __GLASGOW_HASKELL__ < 710
+import           Control.Applicative
+#endif
+
+data Comment = Comment {
+  username :: Text
+  , comment :: Text
+  , created :: UTCTime
+  } deriving (Eq, Show)
+
+instance FromJSON Comment where
+  parseJSON (Object v) =
+    Comment <$> v .: "username"
+            <*> v .: "comment"
+            <*> (toUTCTime <$> (v .: "created"))
+  parseJSON v = typeMismatch "Comment" v
diff --git a/src/Sound/Freesound/List.hs b/src/Sound/Freesound/List.hs
--- a/src/Sound/Freesound/List.hs
+++ b/src/Sound/Freesound/List.hs
@@ -1,7 +1,6 @@
 {-# LANGUAGE CPP, OverloadedStrings, ScopedTypeVariables #-}
 module Sound.Freesound.List (
   List
-, Elem(..)
 , elems
 , numElems
 , previous
@@ -12,8 +11,7 @@
 
 import Control.Monad (liftM, mzero)
 import Data.Aeson
-import Data.Text (Text)
-import Sound.Freesound.API (Freesound, Resource, getResource)
+import Sound.Freesound.API (Freesound, Resource, get)
 
 #if __GLASGOW_HASKELL__ < 710
 import           Control.Applicative
@@ -26,10 +24,7 @@
 , next     :: Maybe (Resource (List a))
 } deriving (Eq, Show)
 
-class Elem a where
-  elemsFieldName :: a -> Text
-
-instance (Elem a, FromJSON a) => FromJSON (List a) where
+instance FromJSON a => FromJSON (List a) where
   parseJSON (Object v) =
     List
       <$> v .: "results"
@@ -38,8 +33,8 @@
       <*> v .:? "next"
   parseJSON _ = mzero
 
-getPrevious :: (Elem a, FromJSON a) => List a -> Freesound (Maybe (List a))
-getPrevious = maybe (return Nothing) (liftM Just . getResource) . previous
+getPrevious :: FromJSON a => List a -> Freesound (Maybe (List a))
+getPrevious = maybe (return Nothing) (liftM Just . get) . previous
 
-getNext :: (Elem a, FromJSON a) => List a -> Freesound (Maybe (List a))
-getNext = maybe (return Nothing) (liftM Just . getResource) . next
+getNext :: FromJSON a => List a -> Freesound (Maybe (List a))
+getNext = maybe (return Nothing) (liftM Just . get) . next
diff --git a/src/Sound/Freesound/Pack.hs b/src/Sound/Freesound/Pack.hs
--- a/src/Sound/Freesound/Pack.hs
+++ b/src/Sound/Freesound/Pack.hs
@@ -1,24 +1,23 @@
 module Sound.Freesound.Pack (
-  Pack(..)
-, Packs
-, Summary
-, Detail
-, username
-, getSounds
-, getSounds_
+    Pack
+  , id
+  , url
+  , description
+  , created
+  , name
+  , username
+  , numSounds
+  , sounds
+  , numDownloads
 ) where
 
-import Data.Default (def)
-import Sound.Freesound.API (Freesound, appendQuery, getResource)
-import Sound.Freesound.List (List)
-import Sound.Freesound.Pack.Type
-import Sound.Freesound.Search (Pagination)
-import Sound.Freesound.Sound (Sounds)
-
-type Packs = List Summary
-
-getSounds :: (Pack a) => Pagination -> a -> Freesound Sounds
-getSounds p = getResource . appendQuery p . sounds
+import           Prelude hiding (id)
+import           Sound.Freesound.API
+import           Sound.Freesound.List (List)
+import           Sound.Freesound.Pack.Type hiding (sounds)
+import qualified Sound.Freesound.Pack.Type as Pack
+import qualified Sound.Freesound.Sound as Sound
 
-getSounds_ :: (Pack a) => a -> Freesound Sounds
-getSounds_ = getSounds def
+-- ^ The URI for a list of sounds in the pack.
+sounds :: Pack -> Resource (List Sound.Summary)
+sounds = Resource . Pack.sounds
diff --git a/src/Sound/Freesound/Pack/Type.hs b/src/Sound/Freesound/Pack/Type.hs
--- a/src/Sound/Freesound/Pack/Type.hs
+++ b/src/Sound/Freesound/Pack/Type.hs
@@ -1,78 +1,46 @@
 {-# LANGUAGE CPP, OverloadedStrings #-}
-module Sound.Freesound.Pack.Type where
+module Sound.Freesound.Pack.Type (
+  Pack(..)
+) where
 
-import           Control.Monad (mzero)
 import           Data.Aeson (FromJSON(..), Value(..), (.:))
+import           Data.Aeson.Types (typeMismatch)
 import           Data.Text (Text)
-import           Sound.Freesound.API (Resource, URI)
-import           Sound.Freesound.List (List, Elem(..))
-import qualified Sound.Freesound.Sound as Sound
+import           Sound.Freesound.API (URI)
+import           Sound.Freesound.Time
 
 #if __GLASGOW_HASKELL__ < 710
 import           Control.Applicative
 #endif
 
-class Pack a where
-  -- | The URI for this resource.
-  ref :: a -> Resource ()
-  -- | The URL for this pack’s page on the Freesound website.
-  url :: a -> URI
-  -- | The API URI for the pack’s sound collection.
-  sounds :: a -> Resource (List Sound.Summary)
-  -- | The pack’s name.
-  name :: a -> Text
-  -- | The date when the pack was created.
-  created :: a -> Text
-  -- | The number of times the pack was downloaded.
-  numDownloads :: a -> Integer
-
-data Summary = Summary {
-  pack_ref :: Resource ()             -- ^ The URI for this resource.
-, pack_url :: URI                   -- ^ The URL for this pack’s page on the Freesound website.
-, pack_sounds :: Resource (List Sound.Summary)           -- ^ The API URI for the pack’s sound collection.
-, pack_name :: Text                 -- ^ The pack’s name.
-, pack_created :: Text              -- ^ The date when the pack was created.
-, pack_num_downloads :: Integer     -- ^ The number of times the pack was downloaded.
-} deriving (Eq, Show)
-
-instance Pack Summary where
-  ref = pack_ref
-  url = pack_url
-  sounds = pack_sounds
-  name = pack_name
-  created = pack_created
-  numDownloads = pack_num_downloads
-
-instance FromJSON Summary where
-  parseJSON (Object o) =
-    Summary
-    <$> o .: "ref"
-    <*> o .: "url"
-    <*> o .: "sounds"
-    <*> o .: "name"
-    <*> o .: "created"
-    <*> o .: "num_downloads"
-  parseJSON _ = mzero
+newtype PackId = PackId Integer deriving (Eq, Ord, Show)
 
-instance Elem Summary where
-  elemsFieldName _ = "packs"
+instance FromJSON PackId where
+  parseJSON (Number i) = return $ PackId (truncate i)
+  parseJSON v = typeMismatch "PackId" v
 
-data Detail = Detail {
-  summary :: Summary
-, username :: Text         -- ^ A JSON object with the user’s username, url, and ref.
+data Pack = Pack {
+    id :: PackId        -- ^ The unique identifier of this pack.
+  , url :: URI          -- ^ The URI for this pack on the Freesound website.
+  , description :: Text -- ^ The description the user gave to the pack (if any).
+  , created :: UTCTime  -- ^ The date when the pack was created (e.g. “2014-04-16T20:07:11.145”).
+  , name :: Text        -- ^ The name user gave to the pack.
+  , username :: Text    -- ^ Username of the creator of the pack.
+  , numSounds :: Int    -- ^ The number of sounds in the pack.
+  , sounds :: URI       -- ^ The URI for a list of sounds in the pack. Plain URI in order to break module dependency cycle.
+  , numDownloads :: Int -- ^ The number of times this pack has been downloaded.
 } deriving (Eq, Show)
 
-instance Pack Detail where
-  ref = ref . summary
-  url = url . summary
-  sounds = sounds . summary
-  name = name . summary
-  created = created . summary
-  numDownloads = numDownloads . summary
-
-instance FromJSON Detail where
-  parseJSON v@(Object o) =
-    Detail
-    <$> parseJSON v
-    <*> o .: "username"
-  parseJSON _ = mzero
+instance FromJSON Pack where
+  parseJSON (Object v) =
+    Pack
+      <$> v .: "id"
+      <*> v .: "url"
+      <*> v .: "description"
+      <*> (toUTCTime <$> (v .: "created"))
+      <*> v .: "name"
+      <*> v .: "username"
+      <*> v .: "num_sounds"
+      <*> v .: "sounds"
+      <*> v .: "num_downloads"
+  parseJSON v = typeMismatch "Pack" v
diff --git a/src/Sound/Freesound/Search.hs b/src/Sound/Freesound/Search.hs
--- a/src/Sound/Freesound/Search.hs
+++ b/src/Sound/Freesound/Search.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE CPP, OverloadedStrings #-}
 module Sound.Freesound.Search (
     SortMethod(..)
   , SortDirection(..)
@@ -9,15 +9,27 @@
   , module Sound.Freesound.Search.Filter
   , module Sound.Freesound.Search.Numerical
   , module Sound.Freesound.Search.Query
+  , search
+  , search_
 ) where
 
 import qualified Data.ByteString.Char8 as BS
-import           Data.Default (Default(..))
-import           Network.HTTP.Types.QueryLike (QueryLike(..), QueryValueLike(..))
+import           Data.Default (Default)
 import           Sound.Freesound.Search.Filter
 import           Sound.Freesound.Search.Numerical
 import           Sound.Freesound.Search.Query
+import qualified Data.ByteString as B
+import           Data.Default (def)
+import qualified Data.Text as T
+import           Network.HTTP.Types.QueryLike (QueryLike(..), QueryValueLike(..))
+import           Sound.Freesound.List (List)
+import           Sound.Freesound.Sound.Type
+import           Sound.Freesound.API (Freesound, get, resourceURI)
 
+#if __GLASGOW_HASKELL__ < 710
+import           Control.Applicative
+#endif
+
 data SortMethod    = Duration | Created | Downloads | Rating deriving (Eq, Show)
 data SortDirection = Ascending | Descending deriving (Eq, Show)
 data Sorting       = Unsorted | SortedBy SortMethod SortDirection deriving (Eq, Show)
@@ -44,7 +56,7 @@
 
 data Pagination = Pagination {
   page :: Int
-, resultsPerPage :: Int
+, pageSize :: Int
 } deriving (Eq, Show)
 
 instance Default Pagination where
@@ -53,7 +65,23 @@
 instance QueryLike Pagination where
   toQuery a = toQuery [ if page def == page a
                         then Nothing
-                        else Just (BS.pack "p", show (page a + 1))
-                      , if resultsPerPage def == resultsPerPage a
+                        else Just (BS.pack "page", show (page a + 1))
+                      , if pageSize def == pageSize a
                         then Nothing
-                        else Just (BS.pack "sounds_per_page", show (resultsPerPage a)) ]
+                        else Just (BS.pack "page_size", show (pageSize a)) ]
+
+search :: Pagination -> Sorting -> Filters -> Query -> Freesound (List Summary)
+search p s fs q =
+    get
+  $ resourceURI
+    [ "search", "text" ]
+    (toQuery p ++ toQuery [ pair "query" q
+                          , pair "filter" fs
+                          , pair "sort" s ])
+                          -- TODO: group_by_pack (changes response type)
+  where
+    pair :: QueryValueLike a => T.Text -> a -> Maybe (T.Text, B.ByteString)
+    pair k a = (,) <$> pure k <*> toQueryValue a
+
+search_ :: Query -> Freesound (List Summary)
+search_ = search def def def
diff --git a/src/Sound/Freesound/Search/Query.hs b/src/Sound/Freesound/Search/Query.hs
--- a/src/Sound/Freesound/Search/Query.hs
+++ b/src/Sound/Freesound/Search/Query.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE CPP #-}
 module Sound.Freesound.Search.Query (
     Query
   , include
@@ -5,23 +6,40 @@
   , (&)
 ) where
 
-import       Data.Char (isSpace)
-import        Data.Text (Text)
-import qualified Data.Text as Text
-import        Network.HTTP.Types.QueryLike (QueryValueLike(..))
+import           Data.Char (isSpace)
+import           Data.Default (Default(..))
+import           Data.String (IsString(..))
+import           Data.Text (Text)
+import qualified Data.Text as T
+import           Network.HTTP.Types.QueryLike (QueryValueLike(..))
 
+#if __GLASGOW_HASKELL__ < 710
+import           Data.Monoid
+#endif
+
+data Term = Include Text | Exclude Text deriving (Eq, Show)
+
 newtype Query = Query [Term] deriving (Eq, Show)
 
+instance Monoid Query where
+  mempty = Query []
+  mappend (Query xs) (Query ys) = Query (xs ++ ys)
+
+instance Default Query where
+  def = mempty
+
+instance IsString Query where
+  fromString [] = mempty
+  fromString s = include (T.pack s)
+
 instance QueryValueLike Query where
-  toQueryValue (Query ts) = toQueryValue $ Text.unwords (map f ts)
+  toQueryValue (Query ts) = toQueryValue $ T.unwords (map f ts)
     where
       quote t
-        | Text.any isSpace t = Text.cons '"' (Text.snoc t '"')
+        | T.any isSpace t = T.cons '"' (T.snoc t '"')
         | otherwise = t
-      f (Include t) = Text.cons '+' (quote t)
-      f (Exclude t) = Text.cons '-' (quote t)
-
-data Term = Include Text | Exclude Text deriving (Eq, Show)
+      f (Include t) = T.cons '+' (quote t)
+      f (Exclude t) = T.cons '-' (quote t)
 
 include :: Text -> Query
 include string = Query [Include string]
@@ -29,8 +47,5 @@
 exclude :: Text -> Query
 exclude string = Query [Exclude string]
 
-append :: Query -> Query -> Query
-append (Query xs) (Query ys) = Query (xs ++ ys)
-
 (&) :: Query -> Query -> Query
-(&) = append
+(&) = mappend
diff --git a/src/Sound/Freesound/Sound.hs b/src/Sound/Freesound/Sound.hs
--- a/src/Sound/Freesound/Sound.hs
+++ b/src/Sound/Freesound/Sound.hs
@@ -1,11 +1,11 @@
 {-# LANGUAGE CPP, OverloadedStrings #-}
 module Sound.Freesound.Sound (
-    SoundId
+    SoundId(..)
   , FileType(..)
+  , GeoTag(..)
   , Sound(..)
   , Summary
   , Detail
-  , Sounds
   , url
   , description
   , geotag
@@ -35,51 +35,18 @@
   -- , analysis
   , analysisStats
   , analysisFrames
-
-  , search
-  , search_
-  , getSimilar
-  , getSimilar_
+  , soundById
+  , soundDetail
 ) where
 
-import           Data.Default (def)
 import qualified Data.Text as T
-import           Network.HTTP.Types.QueryLike (toQuery, toQueryValue)
-import           Sound.Freesound.List (List)
-import           Sound.Freesound.Search (Filters, Pagination, Query, Sorting)
-import           Sound.Freesound.Sound.Type
-import           Sound.Freesound.API (Freesound, appendQuery, getResource, resourceURI)
-
-#if __GLASGOW_HASKELL__ < 710
-import           Control.Applicative
-#endif
-
-type Sounds = List Summary
-
-search :: Pagination -> Sorting -> Filters -> Query -> Freesound Sounds
-search p s fs q =
-    getResource
-  $ resourceURI
-    [ "search", "text" ]
-    (toQuery p ++ toQuery [ ("q"::T.Text, toQueryValue q)
-                          , ("f", toQueryValue fs)
-                          , ("s", toQueryValue s) ])
-
-search_ :: Query -> Freesound Sounds
-search_ = search def def def
-
--- | Search for sounds in a certain coordinate region.
--- geotagged :: ...
-
--- | Content based search.
--- contentSearch :: ...
-
--- Missing: distance field in the response
-getSimilar :: Pagination -> Detail -> Freesound Sounds
-getSimilar p = getResource . appendQuery p . similarSounds
+import           Sound.Freesound.API (Freesound, get, resourceURI)
+import           Sound.Freesound.Sound.Type as Sound
 
-getSimilar_ :: Detail -> Freesound Sounds
-getSimilar_ = getSimilar def
+-- | Get a sound by id.
+soundById :: SoundId -> Freesound Detail
+soundById (SoundId i) = get $ resourceURI ["sounds", T.pack (show i)] []
 
--- getAnalysisStats
--- getAnalysisFrames
+-- | Get detailed sound information from its summary.
+soundDetail :: Summary -> Freesound Detail
+soundDetail = soundById . Sound.id
diff --git a/src/Sound/Freesound/Sound/Type.hs b/src/Sound/Freesound/Sound/Type.hs
--- a/src/Sound/Freesound/Sound/Type.hs
+++ b/src/Sound/Freesound/Sound/Type.hs
@@ -2,25 +2,27 @@
 module Sound.Freesound.Sound.Type where
 
 import           Control.Monad (mzero)
-import           Data.Aeson (FromJSON(..), Value(..), (.:), (.:?))
+import           Data.Aeson (FromJSON(..), Value(..), (.:), (.:?), decodeStrict)
 import qualified Data.ByteString.Char8 as BS
 import           Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
 import           Network.HTTP.Types.QueryLike (QueryValueLike(..))
 import           Prelude hiding (id)
 import           Sound.Freesound.API (Resource, URI)
-import           Sound.Freesound.List (List, Elem(..))
+import           Sound.Freesound.Comment (Comment)
+import           Sound.Freesound.List (List)
+import           Sound.Freesound.Pack.Type (Pack)
+import           Sound.Freesound.Time
 -- import qualified Sound.Freesound.User.Type as User
 
 #if __GLASGOW_HASKELL__ < 710
 import           Control.Applicative
 #endif
 
-newtype SoundId = SoundId Integer
+newtype SoundId = SoundId { soundIdToInteger :: Integer }
                   deriving (Eq, Ord, Show)
 
-soundIdToInteger :: SoundId -> Integer
-soundIdToInteger (SoundId i) = i
-
 instance FromJSON SoundId where
   parseJSON (Number i) = return $ SoundId (truncate i)
   parseJSON _ = mzero
@@ -111,35 +113,35 @@
   parseJSON _ = fail "Couldn't parse Images"
 
 data Detail = Detail {
-    sound_id            :: SoundId      -- ^ The sound’s unique identifier.
-  , url                 :: URI          -- ^ The URI for this sound on the Freesound website.
-  , sound_name          :: Text         -- ^ The name user gave to the sound.
-  , sound_tags          :: [Text]       -- ^ An array of tags the user gave to the sound.
-  , description         :: Text         -- ^ The description the user gave to the sound.
-  , geotag              :: Maybe GeoTag -- ^ Latitude and longitude of the geotag (only for sounds that have been geotagged).
-  , created             :: Text         -- ^ The date of when the sound was uploaded.
-  , sound_license       :: License      -- ^ The license under which the sound is available to you.
-  , fileType            :: FileType     -- ^ The type of sound (wav, aif, mp3, etc.).
-  , channels            :: Int          -- ^ The number of channels.
-  , filesize            :: Integer      -- ^ The size of the file in bytes.
-  , bitrate             :: Int          -- ^ The bit rate of the sound.
-  , bitdepth            :: Int          -- ^ The bit depth of the sound.
-  , duration            :: Double       -- ^ The duration of the sound in seconds.
-  , samplerate          :: Int          -- ^ The samplerate of the sound.
-  , sound_username      :: Text         -- ^ The username of the uploader of the sound.
-  , pack                :: Maybe (Resource ()) -- ^ If the sound is part of a pack, this URI points to that pack’s API resource.
-  , download            :: URI          -- ^ The URI for retrieving the original sound.
-  , bookmark            :: URI          -- ^ The URI for bookmarking the sound.
-  , previews            :: Previews     -- ^ Dictionary containing the URIs for mp3 and ogg versions of the sound. The dictionary includes the fields preview-hq-mp3 and preview-lq-mp3 (for ~128kbps quality and ~64kbps quality mp3 respectively), and preview-hq-ogg and preview-lq-ogg (for ~192kbps quality and ~80kbps quality ogg respectively).
-  , images              :: Images       -- ^ Dictionary including the URIs for spectrogram and waveform visualizations of the sound. The dinctionary includes the fields waveform_l and waveform_m (for large and medium waveform images respectively), and spectral_l and spectral_m (for large and medium spectrogram images respectively).
-  , numDownloads        :: Integer      -- ^ The number of times the sound was downloaded.
-  , avgRating           :: Double       -- ^ The average rating of the sound.
-  , numRatings          :: Integer      -- ^ The number of times the sound was rated.
-  , rate                :: URI          -- ^ The URI for rating the sound.
-  , comments            :: Resource ()     -- ^ The URI of a paginated list of the comments of the sound.
-  , numComments         :: Integer      -- ^ The number of comments.
-  , comment             :: URI          -- ^ The URI to comment the sound.
-  , similarSounds       :: Resource (List Summary)     -- ^ URI pointing to the similarity resource (to get a list of similar sounds).
+    sound_id            :: SoundId                  -- ^ The sound’s unique identifier.
+  , url                 :: URI                      -- ^ The URI for this sound on the Freesound website.
+  , sound_name          :: Text                     -- ^ The name user gave to the sound.
+  , sound_tags          :: [Text]                   -- ^ An array of tags the user gave to the sound.
+  , description         :: Text                     -- ^ The description the user gave to the sound.
+  , geotag              :: Maybe GeoTag             -- ^ Latitude and longitude of the geotag (only for sounds that have been geotagged).
+  , created             :: UTCTime                  -- ^ The date of when the sound was uploaded.
+  , sound_license       :: License                  -- ^ The license under which the sound is available to you.
+  , fileType            :: FileType                 -- ^ The type of sound (wav, aif, mp3, etc.).
+  , channels            :: Int                      -- ^ The number of channels.
+  , filesize            :: Integer                  -- ^ The size of the file in bytes.
+  , bitrate             :: Int                      -- ^ The bit rate of the sound.
+  , bitdepth            :: Int                      -- ^ The bit depth of the sound.
+  , duration            :: Double                   -- ^ The duration of the sound in seconds.
+  , samplerate          :: Int                      -- ^ The samplerate of the sound.
+  , sound_username      :: Text                     -- ^ The username of the uploader of the sound.
+  , pack                :: Maybe (Resource Pack)    -- ^ If the sound is part of a pack, this URI points to that pack’s API resource.
+  , download            :: URI                      -- ^ The URI for retrieving the original sound.
+  , bookmark            :: URI                      -- ^ The URI for bookmarking the sound.
+  , previews            :: Previews                 -- ^ Dictionary containing the URIs for mp3 and ogg versions of the sound. The dictionary includes the fields preview-hq-mp3 and preview-lq-mp3 (for ~128kbps quality and ~64kbps quality mp3 respectively), and preview-hq-ogg and preview-lq-ogg (for ~192kbps quality and ~80kbps quality ogg respectively).
+  , images              :: Images                   -- ^ Dictionary including the URIs for spectrogram and waveform visualizations of the sound. The dinctionary includes the fields waveform_l and waveform_m (for large and medium waveform images respectively), and spectral_l and spectral_m (for large and medium spectrogram images respectively).
+  , numDownloads        :: Integer                  -- ^ The number of times the sound was downloaded.
+  , avgRating           :: Double                   -- ^ The average rating of the sound.
+  , numRatings          :: Integer                  -- ^ The number of times the sound was rated.
+  , rate                :: URI                      -- ^ The URI for rating the sound.
+  , comments            :: Resource (List Comment)  -- ^ The URI of a paginated list of the comments of the sound.
+  , numComments         :: Integer                  -- ^ The number of comments.
+  , comment             :: URI                      -- ^ The URI to comment the sound.
+  , similarSounds       :: Resource (List Summary)  -- ^ URI pointing to the similarity resource (to get a list of similar sounds).
   -- , analysis            :: Maybe
   , analysisStats       :: Resource ()     -- ^ URI pointing to the complete analysis results of the sound.
   , analysisFrames      :: Resource ()    -- ^ The URI for retrieving a JSON file with analysis information for each frame of the sound.
@@ -154,7 +156,7 @@
       <*> v .: "tags"
       <*> v .: "description"
       <*> v .:? "geotag"
-      <*> v .: "created"
+      <*> (toUTCTime <$> (v .: "created"))
       <*> v .: "license"
       <*> v .: "type"
       <*> v .: "channels"
@@ -213,9 +215,6 @@
   username = summary_username
   license = summary_license
 
-instance Elem Summary where
-  elemsFieldName _ = "sounds"
-
 -- | Coordinate
 data GeoTag = GeoTag {
   latitude :: Double
@@ -223,5 +222,9 @@
 } deriving (Eq, Show)
 
 instance FromJSON GeoTag where
-  parseJSON (String _) = pure (GeoTag 0 0)
+  parseJSON (String s) = do
+    let coords = T.concat ["[", T.intercalate "," (T.splitOn " " s), "]"]
+    case decodeStrict (T.encodeUtf8 coords) of
+      Just [lat, lon] -> return $ GeoTag lat lon
+      _ -> fail "Couldn't parse GeoTag"
   parseJSON _ = fail "Couldn't parse GeoTag"
diff --git a/src/Sound/Freesound/Time.hs b/src/Sound/Freesound/Time.hs
new file mode 100644
--- /dev/null
+++ b/src/Sound/Freesound/Time.hs
@@ -0,0 +1,15 @@
+module Sound.Freesound.Time (
+    Time
+  , toUTCTime
+  , UTCTime
+) where
+
+import           Data.Aeson
+import qualified Data.Text as T
+import           Data.Time (UTCTime)
+
+newtype Time = Time { toUTCTime :: UTCTime } deriving (Eq, Show)
+
+instance FromJSON Time where
+  parseJSON (String s) = Time `fmap` parseJSON (String (T.snoc s 'Z'))
+  parseJSON _ = fail "Couldn't parse time"
diff --git a/src/Sound/Freesound/User.hs b/src/Sound/Freesound/User.hs
--- a/src/Sound/Freesound/User.hs
+++ b/src/Sound/Freesound/User.hs
@@ -1,58 +1,72 @@
+{-# LANGUAGE CPP, OverloadedStrings #-}
 module Sound.Freesound.User (
   User(..)
-, Summary
-, Detail
-, sounds
-, packs
-, firstName
-, lastName
-, about
-, homePage
-, signature
-, dateJoined
-, bookmarkCategories
-, getUser
+, Avatar(..)
 , getUserByName
-, getBookmarkCategories
-, getSounds
-, getSounds_
-, getPacks
 ) where
 
-import           Control.Monad (liftM)
-import           Data.Default (def)
+import           Control.Monad (join)
+import           Data.Aeson (FromJSON(..), Value(..), (.:), (.:?))
 import           Data.Text (Text)
-import qualified Data.Text as T
-import           Sound.Freesound.API (Freesound, appendQuery, getResource, resourceURI)
-import qualified Sound.Freesound.Bookmark as Bookmark
-import qualified Sound.Freesound.Bookmark.Internal as Bookmark
-import           Sound.Freesound.Pack (Packs)
-import           Sound.Freesound.Search (Pagination)
-import           Sound.Freesound.Sound (Sounds)
-import           Sound.Freesound.User.Type
-
--- | Get detailed information about a user.
-getUser :: (User a) => a -> Freesound Detail
-getUser = getResource . ref
+import           Sound.Freesound.API (Freesound, Resource, URI, get, resourceURI)
+import           Sound.Freesound.Bookmark (BookmarkCategory)
+import           Sound.Freesound.List (List)
+import           Sound.Freesound.Pack (Pack)
+import qualified Sound.Freesound.Sound as Sound
+import           Sound.Freesound.Time
 
--- | Get information about a user by name.
-getUserByName :: Text -> Freesound Detail
-getUserByName t = getResource $ resourceURI [ T.pack "people", t ] []
+#if __GLASGOW_HASKELL__ < 710
+import           Control.Applicative
+#endif
 
--- | Retrieve a list of a user's bookmark categories.
-getBookmarkCategories :: Detail -> Freesound [Bookmark.Category]
-getBookmarkCategories = liftM Bookmark.categories . getResource . bookmarkCategories
+data Avatar = Avatar {
+    small :: URI
+  , medium :: URI
+  , large :: URI
+  } deriving (Eq, Show)
 
--- | Retrieve a user's sounds.
--- This is broken: the response doesn't contain the User.
-getSounds :: Pagination -> Detail -> Freesound Sounds
-getSounds p = getResource . appendQuery p . sounds
+instance FromJSON Avatar where
+  parseJSON (Object v) =
+    Avatar <$> v .: "small"
+           <*> v .: "medium"
+           <*> v .: "large"
+  parseJSON _ = fail "Couldn't parse Avatar"
 
--- | Retrieve a user's sounds.
+-- | User of the Freesound database.
+data User = User {
+    url :: URI                              -- ^ The URI for this users' profile on the Freesound website.
+  , username :: Text                        -- ^ The username.
+  , about :: Maybe Text                     -- ^ The 'about' text of users' profile (if indicated).
+  , homepage :: Maybe URI                   -- ^ The URI of users' homepage outside Freesound (if indicated).
+  , avatar :: Maybe Avatar                  -- ^ The user's avatar image (if indicated).
+  , dateJoined :: UTCTime                   -- ^ The date when the user joined Freesound.
+  , numSounds :: Int                        -- ^ The number of sounds uploaded by the user.
+  , sounds :: Resource (List Sound.Summary) -- ^ The API URI for this user’s sound collection.
+  , numPacks :: Int                         -- ^ The number of packs by the user.
+  , packs :: Resource (List Pack)           -- ^ The API URI for this user’s pack collection.
+  , numPosts :: Int                         -- ^ The number of forum posts by the user.
+  , numComments :: Int                      -- ^ The number of comments that user made in other users' sounds.
+  , bookmarkCategories :: Resource (List BookmarkCategory)  -- ^ The URI for a list of bookmark categories by the user.
+} deriving (Eq, Show)
 
-getSounds_ :: Detail -> Freesound Sounds
-getSounds_ = getSounds def
+instance FromJSON User where
+  parseJSON (Object v) =
+    User
+    <$> v .: "url"
+    <*> v .: "username"
+    <*> (join <$> (v .:? "about"))
+    <*> (join <$> (v .:? "homepage"))
+    <*> (join <$> (v .:? "avatar"))
+    <*> (toUTCTime <$> (v .: "date_joined"))
+    <*> v .: "num_sounds"
+    <*> v .: "sounds"
+    <*> v .: "num_packs"
+    <*> v .: "packs"
+    <*> v .: "num_posts"
+    <*> v .: "num_comments"
+    <*> v .: "bookmark_categories"
+  parseJSON _ = fail "Couldn't parse User"
 
--- | Retrieve a user's packs.
-getPacks :: Detail -> Freesound Packs
-getPacks = getResource . packs
+-- | Get information about a user by name.
+getUserByName :: Text -> Freesound User
+getUserByName u = get $ resourceURI [ "users", u ] []
diff --git a/src/Sound/Freesound/User/Type.hs b/src/Sound/Freesound/User/Type.hs
deleted file mode 100644
--- a/src/Sound/Freesound/User/Type.hs
+++ /dev/null
@@ -1,85 +0,0 @@
-{-# LANGUAGE CPP, OverloadedStrings #-}
-module Sound.Freesound.User.Type where
-
-import           Control.Monad (mzero)
-import           Data.Aeson (FromJSON(..), Value(..), (.:))
-import           Data.Text (Text)
-import qualified Data.Text as T
-import           Sound.Freesound.API (Resource, URI)
-import qualified Sound.Freesound.Bookmark.Internal as Bookmark
-import           Sound.Freesound.List (List)
-import qualified Sound.Freesound.Pack.Type as Pack
-import qualified Sound.Freesound.Sound.Type as Sound
-
-#if __GLASGOW_HASKELL__ < 710
-import           Control.Applicative
-#endif
-
--- | User of the Freesound database.
-class User a where
-  -- | The user’s username.
-  name :: a -> Text
-  -- | The URI for this resource.
-  ref :: a -> Resource Detail
-  -- | The profile page for the user on the Freesound website.
-  url :: a -> URI
-
--- | User of the Freesound database.
-data Summary = Summary {
-  user_name :: Text     -- ^ The user’s username.
-, user_ref  :: Resource Detail -- ^ The URI for this resource.
-, user_url :: URI       -- ^ The profile page for the user on the Freesound website.
-} deriving (Eq, Show)
-
-instance User Summary where
-  name = user_name
-  ref  = user_ref
-  url  = user_url
-
-instance FromJSON Summary where
-  parseJSON (Object v) =
-    Summary
-    <$> v .: "username"
-    <*> v .: "ref"
-    <*> v .: "url"
-  parseJSON _ = mzero
-
-data Detail = Detail {
-  summary :: Summary                -- ^ Summary.
-, sounds :: Resource (List Sound.Summary)                -- ^ The API URI for this user’s sound collection.
-, packs :: Resource (List Pack.Summary)                -- ^ The API URI for this user’s pack collection.
-, firstName :: Maybe Text           -- ^ The user’s first name, possibly empty.
-, lastName :: Maybe Text            -- ^ The user’s last name, possibly empty.
-, about :: Maybe Text               -- ^ A small text the user wrote about himself.
--- FIXME: homePage :: Maybe Data
-, homePage :: Maybe Text            -- ^ The user’s homepage, possibly empty.
-, signature :: Maybe Text           -- ^ The user’s signature, possibly empty.
-, dateJoined :: Text                -- ^ The date the user joined Freesound.
-, bookmarkCategories :: Resource Bookmark.Categories
-} deriving (Eq, Show)
-
-instance User Detail where
-  name = name . summary
-  ref  = ref . summary
-  url  = url . summary
-
-textToMaybe :: Maybe Text -> Maybe Text
-textToMaybe Nothing = Nothing
-textToMaybe (Just s)
-  | T.null s  = Nothing
-  | otherwise = Just s
-
-instance FromJSON Detail where
-  parseJSON j@(Object v) =
-    Detail
-    <$> parseJSON j
-    <*> v .: "sounds"
-    <*> v .: "packs"
-    <*> (textToMaybe <$> (v .: "first_name"))
-    <*> (textToMaybe <$> (v .: "last_name"))
-    <*> (textToMaybe <$> (v .: "about"))
-    <*> (textToMaybe <$> (v .: "home_page"))
-    <*> (textToMaybe <$> (v .: "signature"))
-    <*> v .: "date_joined"
-    <*> v .: "bookmark_categories"
-  parseJSON _ = mzero
diff --git a/test/Sound/Freesound/SearchSpec.hs b/test/Sound/Freesound/SearchSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Sound/Freesound/SearchSpec.hs
@@ -0,0 +1,23 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Sound.Freesound.SearchSpec (spec) where
+
+import           Data.Default (def)
+import           Sound.Freesound
+import           Sound.Freesound.Search
+import qualified Sound.Freesound.Sound as Sound
+import           Sound.Freesound.Test
+import           Test.Hspec
+
+spec :: Spec
+spec = do
+  describe "search" $ do
+    it "searches" $ do
+      let p s =    Sound.username s == "k0s"
+                && Sound.name s == "Spiffy Spank"
+      fs (anySatisfy p =<< search_ (include "spank")) `shouldReturn` True
+    it "filters by user name" $ do
+      let p s = Sound.username s == "k0s"
+      fs (allSatisfy p =<< search def def (username "k0s") (include "spank")) `shouldReturn` True
+    it "paginates" $ do
+      l <- fs $ search (Pagination 0 1) def def ""
+      length (elems l) `shouldBe` 1
diff --git a/test/Sound/Freesound/SoundSpec.hs b/test/Sound/Freesound/SoundSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Sound/Freesound/SoundSpec.hs
@@ -0,0 +1,71 @@
+{-# LANGUAGE CPP, OverloadedStrings #-}
+module Sound.Freesound.SoundSpec (spec) where
+
+import qualified Data.ByteString.Lazy as BL
+import           Data.List (find)
+import           Data.Maybe (fromJust)
+import           Sound.Freesound
+import qualified Sound.Freesound.Comment as Comment
+import qualified Sound.Freesound.Pack as Pack
+import           Sound.Freesound.Search
+import qualified Sound.Freesound.Sound as Sound
+import           Sound.Freesound.Test
+import           Test.Hspec
+
+#if __GLASGOW_HASKELL__ < 710
+import           Control.Applicative
+#endif
+
+spiffySpankId :: Sound.SoundId
+spiffySpankId = Sound.SoundId 167068
+
+spiffySpank :: Freesound Sound.Detail
+spiffySpank = Sound.soundById spiffySpankId
+
+nokia_2600_warmup :: Freesound Sound.Detail
+nokia_2600_warmup = Sound.soundById (Sound.SoundId 330864)
+
+spec :: Spec
+spec = do
+  describe "Spiffy Spank" $ do
+    it "can be retrieved by id" $ do
+      s <- fs spiffySpank
+      Sound.name s `shouldBe` "Spiffy Spank"
+    it "has the correct creation date" $ do
+      s <- fs spiffySpank
+      Sound.created s `shouldBe` read "2012-10-10 15:24:33.328 UTC"
+    it "has the correct file type" $ do
+      s <- fs spiffySpank
+      Sound.fileType s `shouldBe` Sound.WAV
+    it "can be recovered from its summary" $ do
+      Just info <- find (\s -> Sound.id s == spiffySpankId)
+                    <$> (fs $ getAll =<< search_ (include "Spiffy Spank"))
+      (s, s') <- fs $ (,) <$> Sound.soundDetail info <*> spiffySpank
+      s `shouldBe` s'
+    it "has no geotag" $ do
+      Sound.geotag <$> fs spiffySpank `shouldReturn` Nothing
+    it "is not part of a pack" $ do
+      Sound.pack <$> fs spiffySpank `shouldReturn` Nothing
+  describe "nokia_2600_warmup" $ do
+    it "has the correct geotag" $ do
+      Sound.geotag <$> fs nokia_2600_warmup
+        `shouldReturn` Just (Sound.GeoTag 41.4151829276 2.16533660889)
+    it "is part of pack \"Nokia 2600\"" $ do
+      r <- Sound.pack <$> fs nokia_2600_warmup
+      r `shouldNotBe` Nothing
+      p <- fs . get . fromJust $ r
+      Pack.name p `shouldBe` "Nokia 2600"
+    unlessCI $ do
+      it "can be downloaded" $ do
+        b <- fs $ download . Sound.download =<< nokia_2600_warmup
+        b' <- BL.readFile "test/data/330864__k0s__nokia-2600-warmup.wav"
+        b `shouldBe` b'
+  describe "Nokia_2600_Silence.wav" $ do
+    it "should have a lovely comment" $ do
+      s <- fs . Sound.soundById $ Sound.SoundId 231519
+      l <- fs . get . Sound.comments $ s
+      let c = Comment.Comment {
+            Comment.username = "toiletrolltube"
+          , Comment.comment = "I love this sort of stuff, very useful indeed thanks."
+          , Comment.created = read "2014-03-29 03:15:55.948 UTC" }
+      fs (anySatisfy (==c) l) `shouldReturn` True
diff --git a/test/Sound/Freesound/Test.hs b/test/Sound/Freesound/Test.hs
--- a/test/Sound/Freesound/Test.hs
+++ b/test/Sound/Freesound/Test.hs
@@ -1,9 +1,52 @@
 module Sound.Freesound.Test (
-  fs
+    fs
+  , anySatisfy
+  , allSatisfy
+  , getAll
+  , unlessCI
 ) where
 
+import Data.Aeson (FromJSON)
 import Sound.Freesound
-import System.Environment (getEnv)
+import qualified Sound.Freesound.List as L
+import System.Environment (getEnv, lookupEnv)
+import Test.Hspec.Core.Spec
 
 fs :: Freesound a -> IO a
 fs a = flip runFreesound a =<< apiKeyFromString `fmap` getEnv "FREESOUND_API_KEY"
+
+anySatisfy :: (FromJSON a) => (a -> Bool) -> List a -> Freesound Bool
+anySatisfy p l = do
+  if any p (elems l)
+    then return True
+    else do
+      n <- getNext l
+      case n of
+        Nothing -> return False
+        Just l' -> anySatisfy p l'
+
+allSatisfy :: (FromJSON a) => (a -> Bool) -> List a -> Freesound Bool
+allSatisfy p l = do
+  if all p (elems l)
+    then do
+      n <- getNext l
+      case n of
+        Nothing -> return True
+        Just l' -> allSatisfy p l'
+    else return False
+
+getAll :: (FromJSON a) => List a -> Freesound [a]
+getAll l =
+  case L.next l of
+    Nothing -> return $ L.elems l
+    Just n -> do
+      xs <- getAll =<< get n
+      return $ L.elems l ++ xs
+
+-- | Emit spec only when _not_ running on the CI server.
+unlessCI :: SpecWith () -> SpecWith ()
+unlessCI action = do
+  e <- runIO $ lookupEnv "CI"
+  case e of
+    Nothing -> action
+    _ -> return ()
diff --git a/test/Sound/Freesound/UserSpec.hs b/test/Sound/Freesound/UserSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Sound/Freesound/UserSpec.hs
@@ -0,0 +1,66 @@
+{-# LANGUAGE CPP, OverloadedStrings #-}
+module Sound.Freesound.UserSpec (spec) where
+
+import           Data.Default (def)
+import           Data.List (find)
+import           Data.Maybe (fromJust)
+import qualified Data.Set as Set
+import           Sound.Freesound
+import qualified Sound.Freesound.Bookmark as Bookmark
+import qualified Sound.Freesound.Pack as Pack
+import           Sound.Freesound.Search
+import qualified Sound.Freesound.Sound as Sound
+import qualified Sound.Freesound.User as User
+import           Sound.Freesound.Test
+import           Test.Hspec
+
+#if __GLASGOW_HASKELL__ < 710
+import           Control.Applicative
+#endif
+
+k0s :: Freesound User.User
+k0s = User.getUserByName "k0s"
+
+spec :: Spec
+spec = do
+  describe "k0s" $ do
+    it "can be retrieved by name" $ do
+      u <- fs k0s
+      User.username u `shouldBe` "k0s"
+    it "has the correct date" $ do
+      u <- fs k0s
+      User.dateJoined u `shouldBe` read "2007-02-02 17:32:34 UTC"
+    it "has the correct number of sounds" $ do
+      u <- fs k0s
+      sounds <- fs $ get $ User.sounds u
+      numElems sounds `shouldBe` User.numSounds u
+    it "has the correct set of sounds" $ do
+      sounds <- fs $ getAll =<< get . User.sounds =<< k0s
+      sounds' <- fs $ getAll =<< search def def (username "k0s") def
+      Set.fromList (map Sound.id sounds) `shouldBe` Set.fromList (map Sound.id sounds')
+    it "has a pack called \"Nokia 2600\" containing 2 sounds" $ do
+      u <- fs k0s
+      l <- fs . get . User.packs $ u
+      numElems l `shouldSatisfy` (>=1)
+      r <- fs $ find (\p -> Pack.name p == "Nokia 2600") <$> getAll l
+      r `shouldNotBe` Nothing
+      let p = fromJust r
+      Pack.numSounds p `shouldBe` 2
+      Pack.username p `shouldBe` User.username u
+      sounds <- fs . get . Pack.sounds $ p
+      numElems sounds `shouldBe` 2
+      length (elems sounds) `shouldBe` 2
+    it "has haskell-freesound bookmark category" $ do
+      l <- fs $ get . User.bookmarkCategories =<< k0s
+      -- More than one category
+      numElems l `shouldSatisfy` (>=1)
+      -- Test bookmark category
+      b <- fs $ find (\b -> Bookmark.name b == "haskell-freesound-tests") <$> getAll l
+      b `shouldNotBe` Nothing
+      -- Number of sounds in category
+      Bookmark.numSounds (fromJust b) `shouldBe` 1
+      sounds <- fs . get . Bookmark.sounds . fromJust $ b
+      numElems sounds `shouldBe` 1
+      -- Correct sound in category
+      elems sounds `shouldSatisfy` (not.null)
+      Sound.id (head (elems sounds)) `shouldBe` Sound.SoundId 250719
