diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,57 @@
+# heyefi
+
+heyefi is a Linux system daemon that listens for Eye-Fi SD card and processes the requested file upload requests made by the card. This project is not endorsed by Eye-Fi Inc.
+
+# Installation
+
+heyefi is meant to be run as a system daemon. It currently needs to run as root, to make the file permissions part work with less configuration. (It currently sets the permissions of the files to the same as the upload directory. Let me know if you have ideas on how to do that [without being root](https://github.com/ryantm/heyefi/issues/4)!) It expects a configuration file to be at /etc/heyefi/heyefi.config.
+
+This program is available through Nix, and it can be easily installed on Nixos. It can be installed on any Linux distribution like this:
+
+1. Put a configuration file at /etc/heyefi/heyefi.config (format described in the "Configuration" section below)
+2. Install Nix `curl https://nixos.org/nix/install | sh`
+3. Install heyefi `nix-env -iA nixpkgs.heyefi`. The server binary should now be on your path as `heyefi`.
+4. Find the absolute path of heyefi with `readlink $(which heyefi)` (For example, on one system it was /nix/store/b4m467s0byr1v2xfd7acwpq5x4l2dp8f-heyefi-1.1.0.0/bin/heyefi)
+5. Make a systemd service (or whatever equivalent for your system) that runs the server binary as the root user
+
+# Configuration
+
+The configuration file has the following options:
+
+cards: a list of Eye-Fi card credentials. The format of the list is [["macaddress1","uploadkey1"],["macaddress2","uploadkey2"],...]
+
+upload_dir: an string that is an absolute path to where the files get put after they come off the card.
+
+Complete example config with two cards:
+
+```haskell
+cards = [["0012342de4ce","e7403a0123402ca062"],["1234562d5678","12342a062"]]
+upload_dir = "/data/unsorted"
+```
+
+# NixOS configuration
+
+There is a NixOS service definition for heyefi. It currently only supports one card's credentials.
+
+Complete example configuration:
+
+```nix
+{
+  services.heyefi.enable = true;
+  services.heyefi.cardMacaddress = "0012342de4ce";
+  services.heyefi.uploadKey = "e7403a0123402ca062";
+  services.heyefi.uploadDir = /data/unsorted;
+}
+```
+
+# Development
+
+```bash
+nix-shell
+```
+
+
+# Changelog
+
+2.0.0.0 2017-11-18 Program output should be immediately written to stdout, instead of being buffered until the program finishes.
+1.1.0.0 2017-01-02 fixed build of version 0.13.0.0 of optparse-applicative, which removes Data.Monoid exports
diff --git a/heyefi.cabal b/heyefi.cabal
--- a/heyefi.cabal
+++ b/heyefi.cabal
@@ -1,101 +1,139 @@
+-- This file has been generated from package.yaml by hpack version 0.18.1.
+--
+-- see: https://github.com/sol/hpack
+
 name:                heyefi
-version:             1.1.0.0
+version:             2.0.0.0
 synopsis:            A server for Eye-Fi SD cards.
 description:         This server listens for Eye-Fi cards that want to upload files to a computer and stores them in an upload directory. It is meant to be run as a system daemon.
+category:            Network
 homepage:            https://github.com/ryantm/heyefi
-license:             PublicDomain
-license-file:        LICENSE
+bug-reports:         https://github.com/ryantm/heyefi/issues
 author:              Ryan Mulligan
 maintainer:          ryan@ryantm.com
-category:            Network
+license:             PublicDomain
+license-file:        LICENSE
 build-type:          Simple
-cabal-version:       >=1.10
+cabal-version:       >= 1.10
 
+extra-source-files:
+    README.md
+
 source-repository head
   type: git
   location: https://github.com/ryantm/heyefi
 
 executable heyefi
-  default-language:    Haskell2010
-  hs-source-dirs:      src
-  main-is:             HEyefi/Main.hs
-  other-modules:       HEyefi.App
-                     , HEyefi.Constant
-                     , HEyefi.Config
-                     , HEyefi.Hex
-                     , HEyefi.Log
-                     , HEyefi.Soap
-                     , HEyefi.SoapResponse
-                     , HEyefi.Types
-                     , HEyefi.UploadPhoto
-                     , HEyefi.StartSession
-                     , HEyefi.Strings
-                     , HEyefi.CommandLineOptions
-  ghc-options:         -Wall
-  build-depends:       base >=4.8 && <=5
-                     , stm
-                     , unix
-                     , MissingH
-                     , bytestring
-                     , utf8-string
-                     , time
-                     , iso8601-time
-                     , warp
-                     , wai
-                     , http-types
-                     , HandsomeSoup
-                     , hxt
-                     , case-insensitive
-                     , multipart
-                     , tar
-                     , configurator
-                     , unordered-containers
-                     , text
-                     , temporary
-                     , directory
-                     , filepath
-                     , mtl
-                     , transformers
-                     , exceptions
-                     , random
-                     , optparse-applicative
+  main-is: HEyefi/Main.hs
+  hs-source-dirs:
+      src
+  default-extensions: OverloadedStrings ScopedTypeVariables GeneralizedNewtypeDeriving NoImplicitPrelude
+  ghc-options: -Wall
+  build-depends:
+      base >=4.8 && <=5
+    , stm
+    , unix
+    , MissingH
+    , bytestring
+    , utf8-string
+    , time
+    , iso8601-time
+    , warp
+    , wai
+    , http-types
+    , HandsomeSoup
+    , hxt
+    , case-insensitive
+    , multipart
+    , tar
+    , configurator
+    , unordered-containers
+    , text
+    , temporary
+    , directory
+    , filepath
+    , mtl
+    , transformers
+    , exceptions
+    , random
+    , optparse-applicative
+  other-modules:
+      HEyefi.App
+      HEyefi.CommandLineOptions
+      HEyefi.Config
+      HEyefi.Constant
+      HEyefi.Hex
+      HEyefi.Log
+      HEyefi.Prelude
+      HEyefi.Soap
+      HEyefi.SoapResponse
+      HEyefi.StartSession
+      HEyefi.Texts
+      HEyefi.Types
+      HEyefi.UploadPhoto
+  default-language: Haskell2010
 
-test-suite test-heyefi
+test-suite spec
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  hs-source-dirs:
+      test
+      src
+  default-extensions: OverloadedStrings ScopedTypeVariables GeneralizedNewtypeDeriving NoImplicitPrelude
+  ghc-options: -Wall
+  build-depends:
+      base >=4.8 && <=5
+    , stm
+    , unix
+    , MissingH
+    , bytestring
+    , utf8-string
+    , time
+    , iso8601-time
+    , warp
+    , wai
+    , http-types
+    , HandsomeSoup
+    , hxt
+    , case-insensitive
+    , multipart
+    , tar
+    , configurator
+    , unordered-containers
+    , text
+    , temporary
+    , directory
+    , filepath
+    , mtl
+    , transformers
+    , exceptions
+    , random
+    , optparse-applicative
+    , hspec
+    , hspec-wai
+    , wai-extra
+    , silently
+  other-modules:
+      HEyefi.AppSpec
+      HEyefi.ConfigSpec
+      HEyefi.LogSpec
+      HEyefi.SoapResponseSpec
+      HEyefi.SoapSpec
+      HEyefi.SpecPrelude
+      HEyefi.StartSessionSpec
+      HEyefi.UploadPhotoSpec
+      HEyefi.App
+      HEyefi.CommandLineOptions
+      HEyefi.Config
+      HEyefi.Constant
+      HEyefi.Hex
+      HEyefi.Log
+      HEyefi.Main
+      HEyefi.Prelude
+      HEyefi.Soap
+      HEyefi.SoapResponse
+      HEyefi.StartSession
+      HEyefi.Texts
+      HEyefi.Types
+      HEyefi.UploadPhoto
   default-language: Haskell2010
-  type:             exitcode-stdio-1.0
-  hs-source-dirs:   test, src
-  main-is:          Spec.hs
-  ghc-options:      -Wall
-  build-depends:    base >=4.8 && <=5
-                  , stm
-                  , unix
-                  , containers
-                  , hspec
-                  , hspec-wai
-                  , MissingH
-                  , bytestring
-                  , utf8-string
-                  , time
-                  , iso8601-time
-                  , warp
-                  , wai
-                  , wai-extra
-                  , http-types
-                  , HandsomeSoup
-                  , hxt
-                  , case-insensitive
-                  , multipart
-                  , tar
-                  , configurator
-                  , unordered-containers
-                  , text
-                  , silently
-                  , filepath
-                  , directory
-                  , temporary
-                  , directory
-                  , mtl
-                  , transformers
-                  , exceptions
-                  , random
-                  , optparse-applicative
diff --git a/src/HEyefi/App.hs b/src/HEyefi/App.hs
--- a/src/HEyefi/App.hs
+++ b/src/HEyefi/App.hs
@@ -1,17 +1,18 @@
-{-# LANGUAGE OverloadedStrings #-}
-
 module HEyefi.App where
 
+import           Control.Concurrent.STM (
+    atomically
+  , writeTVar
+  , readTVar )
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Lazy as BL
+import           Data.Maybe (isJust, fromJust, isNothing)
 import           HEyefi.Config (runWithConfig)
 import           HEyefi.Log (logDebug)
+import           HEyefi.Prelude
 import           HEyefi.Soap (handleSoapAction, soapAction)
-import           HEyefi.Strings
 import           HEyefi.Types (SharedConfig, HEyefiApplication)
 import           HEyefi.UploadPhoto (handleUpload)
-
-import qualified Data.ByteString as B
-import qualified Data.ByteString.Lazy as BL
-import           Data.Maybe (isJust, fromJust, isNothing)
 import           Network.Wai (
     Application
   , Request
@@ -19,18 +20,14 @@
   , requestBody
   , requestMethod
   , requestHeaders )
-import           Control.Concurrent.STM (
-    atomically
-  , writeTVar
-  , readTVar )
 
 app :: SharedConfig -> Application
 app sharedConfig req f = do
   config <- atomically (readTVar sharedConfig)
   body <- getWholeRequestBody req
   (result, config') <- runWithConfig config (do
-                  logDebug (show (pathInfo req))
-                  logDebug (show (requestHeaders req))
+                  logDebug (tshow (pathInfo req))
+                  logDebug (tshow (requestHeaders req))
                   dispatchRequest (BL.fromStrict body) req f)
   atomically (writeTVar sharedConfig config')
   return result
@@ -45,7 +42,7 @@
   | requestMethod req == "POST" &&
     isJust (soapAction req) =
       handleSoapAction (fromJust (soapAction req)) body req f
-dispatchRequest _ _ _ = error didNotMatchDispatch
+dispatchRequest _ _ _ = terror didNotMatchDispatch
 
 getWholeRequestBody :: Request -> IO B.ByteString
 getWholeRequestBody request = do
diff --git a/src/HEyefi/CommandLineOptions.hs b/src/HEyefi/CommandLineOptions.hs
--- a/src/HEyefi/CommandLineOptions.hs
+++ b/src/HEyefi/CommandLineOptions.hs
@@ -1,6 +1,6 @@
 module HEyefi.CommandLineOptions where
 
-import HEyefi.Strings
+import HEyefi.Prelude
 
 import Data.Monoid ((<>))
 import Data.Version (showVersion)
@@ -12,8 +12,8 @@
 opts :: ParserInfo (Maybe ())
 opts = info (helper <*> parser)
        ( fullDesc
-         <> progDesc programDescription
-         <> header programHeaderDescription )
+         <> progDesc (unpack programDescription)
+         <> header (unpack programHeaderDescription) )
 
 parser :: Parser (Maybe ())
 parser =
diff --git a/src/HEyefi/Config.hs b/src/HEyefi/Config.hs
--- a/src/HEyefi/Config.hs
+++ b/src/HEyefi/Config.hs
@@ -1,7 +1,7 @@
-{-# LANGUAGE OverloadedStrings #-}
-
 module HEyefi.Config where
 
+import           HEyefi.Log (logInfo)
+import           HEyefi.Prelude
 import           HEyefi.Types (
     Config(..)
   , CardConfig
@@ -11,8 +11,6 @@
   , uploadDirectory
   , logLevel
   , HEyefiM(..))
-import           HEyefi.Log (logInfo)
-import           HEyefi.Strings
 
 import           Control.Concurrent.STM (
     TVar
@@ -27,14 +25,15 @@
   , Handler (..)
   , SomeException (..))
 import           Control.Monad.IO.Class (liftIO)
-import           Control.Monad.State.Lazy (get, put, runStateT)
+import           Control.Monad.State.Lazy (modify, get, runStateT)
 import           Data.Configurator (load, Worth (Required), getMap)
 import           Data.Configurator.Types (Value, ConfigError (ParseError))
 import qualified Data.Configurator.Types as CT
 import           Data.HashMap.Strict ()
 import qualified Data.HashMap.Strict as HM
 import           Data.Maybe (mapMaybe)
-import           Data.Text (Text, unpack, pack)
+import           Data.Text (Text)
+import qualified Data.Text as T
 
 insertCard :: Text -> Text -> Config -> Config
 insertCard macAddress uploadKey c =
@@ -53,7 +52,7 @@
       Just _ -> writeTVar wakeSig Nothing
       Nothing -> retry))
 
-convertCardList :: Value -> Either String [(Text, Text)]
+convertCardList :: Value -> Either Text [(Text, Text)]
 convertCardList (CT.List innerList) =
   Right (mapMaybe extractTuple innerList)
   where
@@ -78,9 +77,9 @@
       Right cardList ->
         return (HM.fromList cardList)
 
-convertUploadDirectory :: Value -> Either String FilePath
+convertUploadDirectory :: Value -> Either Text FilePath
 convertUploadDirectory (CT.String uploadDir) =
-  Right (unpack uploadDir)
+  Right (T.unpack uploadDir)
 convertUploadDirectory _ =
   Left uploadDirFormatDoesNotMatch
 
@@ -101,7 +100,7 @@
 
 reloadConfig :: FilePath -> HEyefiM Config
 reloadConfig configPath = do
-  logInfo (tryingToLoadConfiguration configPath)
+  logInfo (tryingToLoadConfiguration (T.pack configPath))
   catches (
     do
       config <- liftIO (load [Required configPath])
@@ -116,10 +115,10 @@
         lastSNonce = "" } -- TODO: Careful, we might be erasing something here.
     )
     [Handler (\(ParseError p msg) -> do
-                 logInfo (errorParsingConfigurationFile p msg)
+                 logInfo (errorParsingConfigurationFile (T.pack p) (T.pack msg))
                  return emptyConfig),
      Handler (\(SomeException _) -> do
-                 logInfo (couldNotFindConfigurationFile configPath)
+                 logInfo (couldNotFindConfigurationFile (T.pack configPath))
                  return emptyConfig)]
 
 runWithConfig :: Config -> HEyefiM a -> IO (a,Config)
@@ -148,17 +147,12 @@
         liftIO (atomically (writeTVar sharedConfig config)))
     (waitForWake wakeSignal)
 
-getUploadKeyForMacaddress :: String -> HEyefiM (Maybe String)
+getUploadKeyForMacaddress :: Text -> HEyefiM (Maybe Text)
 getUploadKeyForMacaddress mac = do
   c <- get
-  return (fmap unpack (HM.lookup (pack mac) (cardMap c)))
+  return (HM.lookup mac (cardMap c))
 
-putSNonce :: String -> HEyefiM ()
-putSNonce snonce = do
-  c <- get
-  put Config {
-          cardMap = cardMap c,
-          uploadDirectory = uploadDirectory c,
-          logLevel = logLevel c,
-          lastSNonce = snonce
-          }
+putSNonce :: Text -> HEyefiM ()
+putSNonce snonce =
+  modify (\ s ->
+            s { lastSNonce = snonce })
diff --git a/src/HEyefi/Constant.hs b/src/HEyefi/Constant.hs
--- a/src/HEyefi/Constant.hs
+++ b/src/HEyefi/Constant.hs
@@ -1,11 +1,13 @@
 module HEyefi.Constant where
 
+import HEyefi.Prelude
+
 port :: Int
 port = 59278
 
-configPath :: String
+configPath :: Text
 configPath = "/etc/heyefi/heyefi.config"
 
-multipartBodyBoundary :: String
+multipartBodyBoundary :: Text
 multipartBodyBoundary =
   "---------------------------02468ace13579bdfcafebabef00d"
diff --git a/src/HEyefi/Hex.hs b/src/HEyefi/Hex.hs
--- a/src/HEyefi/Hex.hs
+++ b/src/HEyefi/Hex.hs
@@ -1,13 +1,17 @@
 module HEyefi.Hex where
 
-unhex :: String -> Maybe String
-unhex [] = Just []
-unhex (a:b:xs) = do
-  first <- c a
-  second <- c b
-  rest <- unhex xs
-  return (toEnum ((first * 16) + second) : rest)
-unhex _ = Nothing
+import HEyefi.Prelude
+
+unhex :: Text -> Maybe Text
+unhex = unpack >>> go >>> fmap pack
+  where
+    go [] = Just []
+    go (a:b:xs) = do
+      first <- c a
+      second <- c b
+      rest <- go xs
+      return (toEnum ((first * 16) + second) : rest)
+    go _ = Nothing
 
 c :: Char -> Maybe Int
 c '0' = Just 0
diff --git a/src/HEyefi/Log.hs b/src/HEyefi/Log.hs
--- a/src/HEyefi/Log.hs
+++ b/src/HEyefi/Log.hs
@@ -1,27 +1,31 @@
 module HEyefi.Log where
-
-import HEyefi.Types (HEyefiM, LogLevel(..), logLevel)
+import           HEyefi.Types (HEyefiM, LogLevel(..), logLevel)
+import           HEyefi.Prelude
 
-import Control.Monad.IO.Class (liftIO)
-import Control.Monad.State.Lazy (get)
-import Data.Time.Clock (getCurrentTime)
-import Data.Time.ISO8601 (formatISO8601Millis)
+import           Control.Monad.IO.Class (liftIO)
+import           Control.Monad.State.Lazy (get)
+import           Data.Monoid ((<>))
+import           Data.Text (Text)
+import qualified Data.Text.IO as T
+import qualified Data.Text as T
+import           Data.Time.Clock (getCurrentTime)
+import           Data.Time.ISO8601 (formatISO8601Millis)
 
-log' :: LogLevel -> String -> IO ()
+log' :: LogLevel -> Text -> IO ()
 log' ll s = do
   t <- getCurrentTime
-  putStrLn (unwords [
-                 "[" ++ formatISO8601Millis t ++ "]"
-               , "[" ++ show ll ++ "]"
+  T.putStrLn (T.unwords [
+                 "[" <> T.pack (formatISO8601Millis t) <> "]"
+               , "[" <> T.pack (show ll) <> "]"
                , s])
 
-logInfoIO :: String -> IO ()
+logInfoIO :: Text -> IO ()
 logInfoIO = log' Info
 
-logInfo ::  String -> HEyefiM ()
+logInfo ::  Text -> HEyefiM ()
 logInfo = liftIO . logInfoIO
 
-logDebug :: String -> HEyefiM ()
+logDebug :: Text -> HEyefiM ()
 logDebug s = do
   config <- get
   case logLevel config of
diff --git a/src/HEyefi/Main.hs b/src/HEyefi/Main.hs
--- a/src/HEyefi/Main.hs
+++ b/src/HEyefi/Main.hs
@@ -1,25 +1,24 @@
-{-# LANGUAGE OverloadedStrings #-}
-
 module Main where
 
-import           HEyefi.App (app)
-import           HEyefi.CommandLineOptions (handleOptionsThenMaybe)
-import           HEyefi.Config (monitorConfig, newConfig, runWithConfig)
-import           HEyefi.Constant (port, configPath)
-import           HEyefi.Log (logInfoIO)
-import           HEyefi.Strings
-import           HEyefi.Types (SharedConfig)
+import HEyefi.App (app)
+import HEyefi.CommandLineOptions (handleOptionsThenMaybe)
+import HEyefi.Config (monitorConfig, newConfig, runWithConfig)
+import HEyefi.Constant (port, configPath)
+import HEyefi.Log (logInfoIO)
+import HEyefi.Prelude
+import HEyefi.Types (SharedConfig)
 
-import           Control.Concurrent (forkIO)
+import Control.Concurrent (forkIO)
 import           Control.Concurrent.STM (
     newTVar
   , atomically
   , writeTVar
   , TVar
   , readTVar )
-import           Control.Monad (forever, void)
-import           Network.Wai.Handler.Warp (run)
-import           System.Posix.Signals (installHandler, sigHUP, Handler( Catch ))
+import Control.Monad (forever, void)
+import Network.Wai.Handler.Warp (run)
+import System.IO
+import System.Posix.Signals (installHandler, sigHUP, Handler( Catch ))
 
 
 handleHup :: TVar (Maybe Int) -> IO ()
@@ -39,7 +38,7 @@
           (do
               c <- atomically (readTVar sharedConfig)
               runWithConfig c (
-                monitorConfig configPath sharedConfig wakeSig))))
+                monitorConfig (unpack configPath) sharedConfig wakeSig))))
 
 runHeyefi :: IO ()
 runHeyefi = do
@@ -48,8 +47,10 @@
 
   readAndMonitorSharedConfig wakeSig sharedConfig
 
-  logInfoIO (listeningOnPort (show port))
+  logInfoIO (listeningOnPort (tshow port))
   run port (app sharedConfig)
 
 main :: IO ()
-main = handleOptionsThenMaybe runHeyefi
+main = do
+  hSetBuffering stdout LineBuffering
+  handleOptionsThenMaybe runHeyefi
diff --git a/src/HEyefi/Prelude.hs b/src/HEyefi/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/src/HEyefi/Prelude.hs
@@ -0,0 +1,33 @@
+module HEyefi.Prelude (
+  module X,
+  Text,
+  tshow,
+  tmd5,
+  tlength,
+  terror,
+  T.pack,
+  T.unpack,
+  T.writeFile,
+  (<>)) where
+
+import           Control.Category as X
+import           Data.Hash.MD5 (md5s, Str (..))
+import           Data.Monoid ((<>))
+import           Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
+import           HEyefi.Texts as X
+import qualified Prelude as X hiding (String, writeFile, (.), id)
+import           Prelude hiding ((.))
+
+tshow :: Show a => a -> Text
+tshow = show >>> T.pack
+
+tlength :: Text -> Int
+tlength = T.length
+
+terror :: Text -> a
+terror = T.unpack >>> error
+
+tmd5 :: Text -> Text
+tmd5  = T.unpack >>> Str >>> md5s >>> T.pack
diff --git a/src/HEyefi/Soap.hs b/src/HEyefi/Soap.hs
--- a/src/HEyefi/Soap.hs
+++ b/src/HEyefi/Soap.hs
@@ -1,18 +1,5 @@
-{-# LANGUAGE OverloadedStrings #-}
-
 module HEyefi.Soap where
 
-import           HEyefi.Config (getUploadKeyForMacaddress)
-import           HEyefi.Hex (unhex)
-import           HEyefi.Log (logInfo, logDebug)
-import           HEyefi.SoapResponse (
-    markLastPhotoInRollResponse
-  , getPhotoStatusResponse )
-import           HEyefi.StartSession (startSessionResponse)
-import           HEyefi.Strings
-import           HEyefi.Types
-
-
 import           Control.Arrow ((>>>))
 import           Control.Monad (join)
 import           Control.Monad.IO.Class (liftIO)
@@ -23,14 +10,23 @@
 import           Data.ByteString.Lazy.UTF8 (toString)
 import           Data.ByteString.UTF8 (fromString)
 import qualified Data.CaseInsensitive as CI
-import           Data.Hash.MD5 (md5s, Str (..))
 import           Data.List (find)
 import           Data.Maybe (fromJust, isJust)
+import           Data.Text.Encoding (encodeUtf8)
 import           Data.Time.Clock (getCurrentTime, UTCTime)
 import           Data.Time.Format (
     formatTime
   , rfc822DateFormat
   , defaultTimeLocale )
+import           HEyefi.Config (getUploadKeyForMacaddress)
+import           HEyefi.Hex (unhex)
+import           HEyefi.Log (logInfo, logDebug)
+import           HEyefi.Prelude
+import           HEyefi.SoapResponse (
+    markLastPhotoInRollResponse
+  , getPhotoStatusResponse )
+import           HEyefi.StartSession (startSessionResponse)
+import           HEyefi.Types
 import           Network.HTTP.Types (status200, unauthorized401)
 import           Network.HTTP.Types.Header (
     hContentType
@@ -70,13 +66,13 @@
 soapAction :: Request -> Maybe SoapAction
 soapAction req = firstJust headerToSoapAction (requestHeaders req)
 
-mkResponse :: String -> HEyefiM Response
+mkResponse :: Text -> HEyefiM Response
 mkResponse responseBody = do
   t <- liftIO getCurrentTime
   return (responseLBS
           status200
-          (defaultResponseHeaders t (length responseBody))
-          (fromStrict (fromString responseBody)))
+          (defaultResponseHeaders t (tlength responseBody))
+          (fromStrict (encodeUtf8 responseBody)))
 
 mkUnauthorizedResponse :: Response
 mkUnauthorizedResponse = responseLBS unauthorized401 [] ""
@@ -92,10 +88,11 @@
   , (hContentLength, fromString (show size))]
 
 firstTag :: BL.ByteString ->
-            String ->
-            String
+            Text ->
+            Text
 firstTag body tagName =
-  head (runLA (xreadDoc >>> css tagName /> getText) (toString body))
+  pack (head (runLA (xreadDoc >>> css (unpack tagName) /> getText)
+                (toString body)))
 
 handleSoapAction :: SoapAction -> BL.ByteString -> HEyefiApplication
 handleSoapAction StartSession body _ f = do
@@ -105,14 +102,14 @@
   let cnonce = tag "cnonce"
   let transfermode = tag "transfermode"
   let transfermodetimestamp = tag "transfermodetimestamp"
-  logDebug (show macaddress)
-  logDebug (show transfermodetimestamp)
+  logDebug macaddress
+  logDebug transfermodetimestamp
   responseBody <- startSessionResponse
                    macaddress
                    cnonce
                    transfermode
                    transfermodetimestamp
-  logDebug (show responseBody)
+  logDebug responseBody
   response <- mkResponse responseBody
   liftIO (f response)
 handleSoapAction GetPhotoStatus body _ f = do
@@ -141,9 +138,9 @@
      logInfo (noUploadKeyInConfiguration macaddress)
      return False
    Just upload_key_0' -> do
-     let credentialString = macaddress ++ upload_key_0' ++ snonce
+     let credentialString = macaddress <> upload_key_0' <> snonce
      let binaryCredentialString = unhex credentialString
-     let expectedCredential = md5s (Str (fromJust binaryCredentialString))
+     let expectedCredential = tmd5 (fromJust binaryCredentialString)
      if credential /= expectedCredential then do
        logInfo (invalidCredential expectedCredential credential)
        return False
diff --git a/src/HEyefi/SoapResponse.hs b/src/HEyefi/SoapResponse.hs
--- a/src/HEyefi/SoapResponse.hs
+++ b/src/HEyefi/SoapResponse.hs
@@ -1,8 +1,10 @@
-{-# LANGUAGE OverloadedStrings #-}
-
 module HEyefi.SoapResponse where
 
-import Control.Arrow ((>>>))
+import           HEyefi.Prelude
+
+import           Control.Arrow ((>>>))
+import           Data.Text (Text)
+import qualified Data.Text as T
 import Text.XML.HXT.Core (
     runLA
   , root
@@ -27,9 +29,9 @@
     [ sattr "xmlns:SOAP-ENV" "http://schemas.xmlsoap.org/soap/envelope/" ]
     [ mkelem "SOAP-ENV:Body" [] body ]]
 
-soapResponse :: [LA n XmlTree] -> String
+soapResponse :: [LA n XmlTree] -> Text
 soapResponse body =
-  head (runLA (document >>> writeDocumentToString []) undefined)
+  T.pack (head (runLA (document >>> writeDocumentToString []) undefined))
   where
     document = root [] (soapMessage body)
 
@@ -40,7 +42,7 @@
     [ mkelem "success" [] [ txt "true" ] ]
   ]
 
-markLastPhotoInRollResponse :: String
+markLastPhotoInRollResponse :: Text
 markLastPhotoInRollResponse = soapResponse markLastPhotoInRollBody
 
 markLastPhotoInRollBody :: [LA n XmlTree]
@@ -50,7 +52,7 @@
       []
   ]
 
-getPhotoStatusResponse :: String
+getPhotoStatusResponse :: Text
 getPhotoStatusResponse = soapResponse getPhotoStatusBody
 
 getPhotoStatusBody :: [LA n XmlTree]
diff --git a/src/HEyefi/StartSession.hs b/src/HEyefi/StartSession.hs
--- a/src/HEyefi/StartSession.hs
+++ b/src/HEyefi/StartSession.hs
@@ -1,19 +1,18 @@
-{-# LANGUAGE OverloadedStrings #-}
-
 module HEyefi.StartSession where
 
-import HEyefi.Config (getUploadKeyForMacaddress, putSNonce)
-import HEyefi.Hex (unhex)
-import HEyefi.Log (logInfo)
-import HEyefi.Types (HEyefiM(..))
+import           HEyefi.Config (getUploadKeyForMacaddress, putSNonce)
+import           HEyefi.Hex (unhex)
+import           HEyefi.Log (logInfo)
+import           HEyefi.Prelude
+import           HEyefi.Types (HEyefiM(..))
 
-import Control.Arrow ((>>>))
-import Control.Monad (replicateM)
-import Control.Monad.IO.Class (liftIO)
-import Data.Char (intToDigit)
-import Data.Hash.MD5 (md5s, Str (..))
-import Data.Maybe (fromJust)
-import System.Random (randomRIO)
+import           Control.Arrow ((>>>))
+import           Control.Monad (replicateM)
+import           Control.Monad.IO.Class (liftIO)
+import           Data.Char (intToDigit)
+import           Data.Maybe (fromJust)
+
+import           System.Random (randomRIO)
 import Text.XML.HXT.Core ( runLA
                          , mkelem
                          , spi
@@ -23,29 +22,30 @@
                          , root
                          , writeDocumentToString)
 
-newServerNonce :: IO String
-newServerNonce =
-  replicateM 32 (do
-                    i <- randomRIO (0, 15)
-                    return (intToDigit i))
+newServerNonce :: IO Text
+newServerNonce = do
+  string <- replicateM 32 (do
+                            i <- randomRIO (0, 15)
+                            return (intToDigit i))
+  return (pack string)
 
-startSessionResponse :: String ->
-                        String ->
-                        String ->
-                        String ->
-                        HEyefiM String
+startSessionResponse :: Text ->
+                        Text ->
+                        Text ->
+                        Text ->
+                        HEyefiM Text
 startSessionResponse macaddress cnonce transfermode transfermodetimestamp = do
   upload_key_0 <- getUploadKeyForMacaddress macaddress
   case upload_key_0 of
    Nothing -> do
-     logInfo ("No upload key found in configuration for macaddress: " ++ macaddress)
+     logInfo (noUploadKeyInConfiguration macaddress)
      return ""
    Just upload_key_0' -> do
      snonce <- liftIO newServerNonce
      putSNonce snonce
-     let credentialString = macaddress ++ cnonce ++ upload_key_0'
-     let binaryCredentialString = unhex credentialString
-     let credential = md5s (Str (fromJust binaryCredentialString))
+     let credentialText = macaddress <> cnonce <> upload_key_0'
+     let binaryCredentialString = unhex credentialText
+     let credential = tmd5 (fromJust binaryCredentialString)
      let document =
            root []
            [ spi t_xml "version=\"1.0\" encoding=\"UTF-8\""
@@ -54,14 +54,16 @@
              [ mkelem "SOAP-ENV:Body" []
                [ mkelem "StartSessionResponse"
                  [ sattr "xmlns" "http://localhost/api/soap/eyefilm" ]
-                 [ mkelem "credential" [] [ txt credential ]
-                 , mkelem "snonce" [] [ txt snonce ]
-                 , mkelem "transfermode" [] [ txt transfermode ]
-                 , mkelem "transfermodetimestamp" [] [ txt transfermodetimestamp ]
+                 [ mkelem "credential" [] [ txtT credential ]
+                 , mkelem "snonce" [] [ txtT snonce ]
+                 , mkelem "transfermode" [] [ txtT transfermode ]
+                 , mkelem "transfermodetimestamp" [] [ txtT transfermodetimestamp ]
                  , mkelem "upsyncallowed" [] [ txt "true" ]
                  ]
                ]
              ]
            ]
 
-     return (head (runLA (document >>> writeDocumentToString []) undefined))
+     return (pack (head (runLA (document >>> writeDocumentToString []) undefined)))
+     where
+       txtT = txt . unpack
diff --git a/src/HEyefi/Strings.hs b/src/HEyefi/Strings.hs
deleted file mode 100644
--- a/src/HEyefi/Strings.hs
+++ /dev/null
@@ -1,72 +0,0 @@
-module HEyefi.Strings where
-
-cardsFormatDoesNotMatch :: String
-cardsFormatDoesNotMatch =
-  "Format of cards does not match [[MacAddress, Key],[MacAddress, Key],...]."
-
-missingCardsDefinition :: String
-missingCardsDefinition = "Configuration is missing a definition for `cards`."
-
-uploadDirFormatDoesNotMatch :: String
-uploadDirFormatDoesNotMatch =
-  "Format of upload_dir does not match \"/path/to/upload/dir\""
-
-missingUploadDirDefinition :: String
-missingUploadDirDefinition =
-  "Configuration is missing a definition for `upload_dir`."
-
-
-noUploadKeyInConfiguration :: String -> String
-noUploadKeyInConfiguration =
-  (++) "No upload key found in configuration for macaddress: "
-
-invalidCredential :: String -> String -> String
-invalidCredential expected actual =
-  "Invalid credential in GetPhotoStatus request. Expected: "
-   ++ expected ++ " Actual: " ++ actual
-
-listeningOnPort :: String -> String
-listeningOnPort = (++) "Listening on port "
-
-gotStartSessionRequest :: String
-gotStartSessionRequest = "Got StartSession request"
-
-gotGetPhotoStatusRequest :: String
-gotGetPhotoStatusRequest = "Got GetPhotoStatus request"
-
-gotMarkLastPhotoInRollRequest :: String
-gotMarkLastPhotoInRollRequest = "Got MarkLastPhotoInRoll request"
-
-loadedConfiguration :: String
-loadedConfiguration = "Loaded configuration"
-
-tryingToLoadConfiguration :: String -> String
-tryingToLoadConfiguration = (++) "Trying to load configuration at "
-
-errorParsingConfigurationFile :: String -> String -> String
-errorParsingConfigurationFile p msg =
-  "Error parsing configuration file at " ++
-  p ++
-  " with message: " ++
-  msg
-
-couldNotFindConfigurationFile :: String -> String
-couldNotFindConfigurationFile = (++) "Could not find configuration file at "
-
-didNotMatchDispatch :: String
-didNotMatchDispatch = "did not match dispatch"
-
-notADefinedSoapAction :: String -> String
-notADefinedSoapAction sa = sa ++ " is not a defined SoapAction yet"
-
-programDescription :: String
-programDescription = "A server daemon for Eye-Fi SD cards."
-
-programHeaderDescription :: String
-programHeaderDescription = "heyefi - a server for Eye-Fi SD cards"
-
-gotUploadRequest :: String
-gotUploadRequest = "Got Upload request"
-
-uploadedTo :: String -> String
-uploadedTo = (++) "Uploaded to "
diff --git a/src/HEyefi/Texts.hs b/src/HEyefi/Texts.hs
new file mode 100644
--- /dev/null
+++ b/src/HEyefi/Texts.hs
@@ -0,0 +1,75 @@
+module HEyefi.Texts where
+
+import Data.Text (Text)
+import Data.Monoid ((<>))
+
+cardsFormatDoesNotMatch :: Text
+cardsFormatDoesNotMatch =
+  "Format of cards does not match [[MacAddress, Key],[MacAddress, Key],...]."
+
+missingCardsDefinition :: Text
+missingCardsDefinition = "Configuration is missing a definition for `cards`."
+
+uploadDirFormatDoesNotMatch :: Text
+uploadDirFormatDoesNotMatch =
+  "Format of upload_dir does not match \"/path/to/upload/dir\""
+
+missingUploadDirDefinition :: Text
+missingUploadDirDefinition =
+  "Configuration is missing a definition for `upload_dir`."
+
+
+noUploadKeyInConfiguration :: Text -> Text
+noUploadKeyInConfiguration =
+  (<>) "No upload key found in configuration for macaddress: "
+
+invalidCredential :: Text -> Text -> Text
+invalidCredential expected actual =
+  "Invalid credential in GetPhotoStatus request. Expected: "
+   <> expected <> " Actual: " <> actual
+
+listeningOnPort :: Text -> Text
+listeningOnPort = (<>) "Listening on port "
+
+gotStartSessionRequest :: Text
+gotStartSessionRequest = "Got StartSession request"
+
+gotGetPhotoStatusRequest :: Text
+gotGetPhotoStatusRequest = "Got GetPhotoStatus request"
+
+gotMarkLastPhotoInRollRequest :: Text
+gotMarkLastPhotoInRollRequest = "Got MarkLastPhotoInRoll request"
+
+loadedConfiguration :: Text
+loadedConfiguration = "Loaded configuration"
+
+tryingToLoadConfiguration :: Text -> Text
+tryingToLoadConfiguration = (<>) "Trying to load configuration at "
+
+errorParsingConfigurationFile :: Text -> Text -> Text
+errorParsingConfigurationFile p msg =
+  "Error parsing configuration file at " <>
+  p <>
+  " with message: " <>
+  msg
+
+couldNotFindConfigurationFile :: Text -> Text
+couldNotFindConfigurationFile = (<>) "Could not find configuration file at "
+
+didNotMatchDispatch :: Text
+didNotMatchDispatch = "did not match dispatch"
+
+notADefinedSoapAction :: Text -> Text
+notADefinedSoapAction sa = sa <> " is not a defined SoapAction yet"
+
+programDescription :: Text
+programDescription = "A server daemon for Eye-Fi SD cards."
+
+programHeaderDescription :: Text
+programHeaderDescription = "heyefi - a server for Eye-Fi SD cards"
+
+gotUploadRequest :: Text
+gotUploadRequest = "Got Upload request"
+
+uploadedTo :: Text -> Text
+uploadedTo = (<>) "Uploaded to "
diff --git a/src/HEyefi/Types.hs b/src/HEyefi/Types.hs
--- a/src/HEyefi/Types.hs
+++ b/src/HEyefi/Types.hs
@@ -1,7 +1,7 @@
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-
 module HEyefi.Types where
 
+import HEyefi.Prelude
+
 import           Control.Concurrent.STM (TVar)
 import           Control.Monad.Catch (MonadMask, MonadCatch, MonadThrow)
 import           Control.Monad.IO.Class (MonadIO)
@@ -41,7 +41,7 @@
   cardMap :: CardConfig,
   uploadDirectory :: FilePath,
   logLevel :: LogLevel,
-  lastSNonce :: String
+  lastSNonce :: Text
 }
 
 type SharedConfig = TVar Config
diff --git a/src/HEyefi/UploadPhoto.hs b/src/HEyefi/UploadPhoto.hs
--- a/src/HEyefi/UploadPhoto.hs
+++ b/src/HEyefi/UploadPhoto.hs
@@ -1,18 +1,15 @@
-{-# LANGUAGE OverloadedStrings #-}
-
 module HEyefi.UploadPhoto where
 
+import           Codec.Archive.Tar (extract)
+import           Control.Monad.IO.Class (liftIO)
+import           Control.Monad.State.Lazy (get)
+import qualified Data.ByteString.Lazy as BL
 import           HEyefi.Constant (multipartBodyBoundary)
 import           HEyefi.Log (logDebug, logInfo)
+import           HEyefi.Prelude
 import           HEyefi.Soap (mkResponse)
 import           HEyefi.SoapResponse (soapResponse, uploadPhotoResponse)
-import           HEyefi.Strings
 import           HEyefi.Types (uploadDirectory, HEyefiM, HEyefiApplication)
-
-import           Codec.Archive.Tar (extract)
-import           Control.Monad.IO.Class (liftIO)
-import           Control.Monad.State.Lazy (get)
-import qualified Data.ByteString.Lazy as BL
 import           Network.Multipart (
     parseMultipartBody
   , MultiPart (..)
@@ -29,6 +26,7 @@
   , FileStatus )
 
 
+
 copyMatchingOwnership :: FileStatus -> FilePath -> FilePath -> IO FilePath
 copyMatchingOwnership fs from to = do
   copyFile from to
@@ -65,26 +63,28 @@
 handleUpload :: BL.ByteString -> HEyefiApplication
 handleUpload body _ f = do
   logDebug gotUploadRequest
-  let MultiPart bodyParts = parseMultipartBody multipartBodyBoundary body
-  logDebug (show (length bodyParts))
+  let MultiPart bodyParts = parseMultipartBody (unpack multipartBodyBoundary) body
+  logDebug (tshow (length bodyParts))
   lBP bodyParts
-  let (BodyPart _ soapEnvelope) = head bodyParts
-  let (BodyPart _ file) = bodyParts !! 1
-  let (BodyPart _ digest) = bodyParts !! 2
 
+  let [  BodyPart _ soapEnvelope
+       , BodyPart _ file
+       , BodyPart _ digest
+       ] = bodyParts
+
   outputPath <- writeTarFile file
-  logInfo (uploadedTo outputPath)
+  logInfo (uploadedTo (pack outputPath))
 
-  logDebug (show soapEnvelope)
-  logDebug (show digest)
+  logDebug (tshow soapEnvelope)
+  logDebug (tshow digest)
   let responseBody = soapResponse uploadPhotoResponse
-  logDebug (show responseBody)
+  logDebug responseBody
   r <- mkResponse responseBody
   liftIO (f r)
 
   where
     lBP [] = return ()
     lBP (BodyPart headers _ : xs) = do
-      logDebug (show headers)
+      logDebug (tshow headers)
       lBP xs
       return ()
diff --git a/test/HEyefi/AppSpec.hs b/test/HEyefi/AppSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/HEyefi/AppSpec.hs
@@ -0,0 +1,84 @@
+module HEyefi.AppSpec where
+
+import Control.Concurrent.STM
+import Data.ByteString.Lazy (toStrict)
+import Data.CaseInsensitive as CI
+import Data.Text (Text, isInfixOf)
+import Data.Text.Encoding (decodeUtf8)
+import HEyefi.App
+import HEyefi.Config
+import HEyefi.SpecPrelude
+import Network.HTTP.Types.Method
+import Network.Wai (Application)
+import Network.Wai.Test (SResponse (simpleBody))
+import Test.Hspec.Wai
+import Test.Hspec.Wai.Internal
+
+
+
+spec :: Spec
+spec = do
+  describe "StartSession" (
+    it "should return expected body" (
+       do
+         a <- app'
+         action <- runWaiSession sampleStartSessionRequest a
+         responseBodyContains action sampleStartSessionResponse1 `shouldBe` True
+         responseBodyContains action sampleStartSessionResponse2
+           `shouldBe` True))
+  with app' (
+    do
+      describe "MarkLastPhotoInRoll" (
+        it "should respond with status 200" (
+           sampleMarkLastPhotoRequest
+           `shouldRespondWith`
+           sampleMarkLastPhotoInRollResponse {matchStatus = 200}))
+      describe "StartSession" (
+        it "should respond with status 200" (
+           sampleStartSessionRequest
+           `shouldRespondWith` 200))
+      describe "GetPhotoStatus" (
+        it "should respond with status 200" (
+           sampleStartSessionRequest
+           `shouldRespondWith` 200)))
+
+responseBodyContains :: SResponse -> Text -> Bool
+responseBodyContains r t =
+  t
+  `isInfixOf`
+  (decodeUtf8 . toStrict) (simpleBody r)
+
+app' :: IO Application
+app' = do
+  sharedConfig <- atomically (newTVar (insertCard "0018562de4ce" "36d61e4e7403a0586702c9159892a062" emptyConfig))
+  return (app sharedConfig)
+
+sampleMarkLastPhotoRequest :: WaiSession SResponse
+sampleMarkLastPhotoRequest =
+  request
+  methodPost "/"
+  [(CI.mk "SoapAction",  "\"urn:MarkLastPhotoInRoll\"")]
+  "<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:ns1=\"EyeFi/SOAP/EyeFilm\"><SOAP-ENV:Body><ns1:MarkLastPhotoInRoll><macaddress>001856417729</macaddress><mergedelta>0</mergedelta></ns1:MarkLastPhotoInRoll></SOAP-ENV:Body></SOAP-ENV:Envelope>"
+
+sampleMarkLastPhotoInRollResponse :: ResponseMatcher
+sampleMarkLastPhotoInRollResponse = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Body><MarkLastPhotoInRollResponse xmlns=\"http://localhost/api/soap/eyefilm\"/></SOAP-ENV:Body></SOAP-ENV:Envelope>"
+
+sampleStartSessionRequest :: WaiSession SResponse
+sampleStartSessionRequest =
+  request
+  methodPost "/"
+  [(CI.mk "SoapAction",  "\"urn:StartSession\"")]
+  "<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:ns1=\"EyeFi/SOAP/EyeFilm\"><SOAP-ENV:Body><ns1:StartSession><macaddress>0018562de4ce</macaddress><cnonce>6eb0444343c1953e47fb28181bb4e47f</cnonce><transfermode>34</transfermode><transfermodetimestamp>1356903384</transfermodetimestamp></ns1:StartSession></SOAP-ENV:Body></SOAP-ENV:Envelope>"
+
+sampleStartSessionResponse1 :: Text
+sampleStartSessionResponse1 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Body><StartSessionResponse xmlns=\"http://localhost/api/soap/eyefilm\"><credential>f9d03ddcce53582ff10075577e522373</credential><snonce>"
+
+sampleStartSessionResponse2 :: Text
+sampleStartSessionResponse2 = "</snonce><transfermode>34</transfermode><transfermodetimestamp>1356903384</transfermodetimestamp><upsyncallowed>true</upsyncallowed></StartSessionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>"
+
+sampleGetPhotoRequest :: WaiSession SResponse
+sampleGetPhotoRequest =
+  request
+  methodPost "/"
+  [(CI.mk "SoapAction",  "\"urn:GetPhotoRequest\"")]
+  "<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:ns1=\"EyeFi/SOAP/EyeFilm\"><SOAP-ENV:Body><ns1:GetPhotoStatus><credential>7daa9ecf3a9f135f5bb30541ed84fcfb</credential><macaddress>0018562de4ce</macaddress><filename>IMG_2195.JPG.tar</filename><filesize>125952</filesize><filesignature>736ffb7fa20f1708fd300c58c0aabb61</filesignature><flags>4</flags></ns1:GetPhotoStatus></SOAP-ENV:Body></SOAP-ENV:Envelope>"
diff --git a/test/HEyefi/ConfigSpec.hs b/test/HEyefi/ConfigSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/HEyefi/ConfigSpec.hs
@@ -0,0 +1,83 @@
+module HEyefi.ConfigSpec where
+
+import           Test.Hspec
+
+import           HEyefi.Config
+import           HEyefi.Prelude
+import           HEyefi.Types (cardMap, uploadDirectory, Config)
+
+import           Control.Exception (catch, SomeException, throwIO)
+import qualified Data.HashMap.Strict as HM
+import           System.Directory (getTemporaryDirectory, removeFile)
+import           System.FilePath ((</>))
+import           System.IO.Error (isDoesNotExistError)
+import           System.IO.Silently (capture)
+
+removeIfExists :: FilePath -> IO ()
+removeIfExists fileName = removeFile fileName `catch` handleExists
+  where handleExists e
+          | isDoesNotExistError e = return ()
+          | otherwise = throwIO e
+
+getNonexistentTemporaryFile :: IO FilePath
+getNonexistentTemporaryFile = do
+  tempdir <- catch getTemporaryDirectory (\(_::SomeException) -> return ".")
+  let file = tempdir </> "heyefi.config"
+  removeIfExists file
+  return file
+
+makeAndReloadFile_ :: Text -> IO Text
+makeAndReloadFile_ s = fmap fst (makeAndReloadFile s)
+
+makeAndReloadFile :: Text -> IO (Text, Config)
+makeAndReloadFile contents = do
+  file <- getNonexistentTemporaryFile
+  writeFile file contents
+  (a,b) <- capture (do
+                       r <- runWithEmptyConfig (reloadConfig file)
+                       return (fst r))
+  return (pack a, b)
+
+validConfig :: Text
+validConfig = "upload_dir = \"/data/photos\"\ncards = [[\"0012342de4ce\",\"e7403a0123402ca062\"],[\"1234562d5678\",\"12342a062\"]]"
+
+spec :: Spec
+spec =
+  describe "reloadConfig" (do
+    it "should report an error for a non-existent configuration file"
+     (do
+         file <- getNonexistentTemporaryFile
+         (output, config) <- capture (
+           do
+             r <- runWithEmptyConfig (reloadConfig file)
+             return (fst r))
+         cardMap config `shouldBe` HM.empty
+         output `shouldContain` "Could not find configuration file at " ++ file)
+    it "should report an error for an unparsable configuration file"
+     (do
+         (output, config) <- makeAndReloadFile "a = (\n"
+         cardMap config `shouldBe` HM.empty
+         unpack output `shouldContain` "Error parsing configuration file at "
+         unpack output `shouldContain` "with message: endOfInput")
+    it "should complain about missing cards configuration"
+     (do
+         (output, config) <- makeAndReloadFile "upload_dir = \"/data/annex/doxie/unsorted\""
+         cardMap config `shouldBe` HM.empty
+         unpack output `shouldContain` "missing a definition for `cards`.")
+    it "should complain about cards not having the correct format"
+     (do
+         (_, config) <- makeAndReloadFile "upload_dir = \"/data/annex/doxie/unsorted\"\ncards=[[\"1\",\"2\",\"3\"]]"
+         cardMap config `shouldBe` HM.empty
+         output2 <- makeAndReloadFile_ "upload_dir = \"/data/annex/doxie/unsorted\"\ncards=\"1\""
+         unpack output2 `shouldContain` "Format of cards does not match")
+    it "should complain about missing upload_dir configuration"
+     (do
+         (output, config) <-  makeAndReloadFile "cards = [[\"0012342de4ce\",\"e7403a0123402ca062\"],[\"1234562d5678\",\"12342a062\"]]"
+         uploadDirectory config `shouldBe` ""
+         unpack output `shouldContain`"missing a definition for `upload_dir`.")
+    it "should parse cards for a valid configuration"
+     (do
+         (_, config) <- makeAndReloadFile validConfig
+         uploadDirectory config `shouldBe` "/data/photos"
+         HM.lookup "0012342de4ce" (cardMap config) `shouldBe` Just "e7403a0123402ca062"
+         HM.lookup "1234562d5678" (cardMap config) `shouldBe` Just "12342a062"))
diff --git a/test/HEyefi/LogSpec.hs b/test/HEyefi/LogSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/HEyefi/LogSpec.hs
@@ -0,0 +1,36 @@
+module HEyefi.LogSpec where
+
+import Data.Time.Clock (getCurrentTime)
+import Data.Time.Format (formatTime, defaultTimeLocale)
+import HEyefi.Config
+import HEyefi.Log
+import HEyefi.Prelude
+import HEyefi.SpecPrelude
+import HEyefi.Types
+
+currentTime :: IO Text
+currentTime = do
+  t <- getCurrentTime
+  return (pack (formatTime defaultTimeLocale "%FT%T" t))
+
+spec :: Spec
+spec = do
+  describe "logDebug" (do
+    it "should output nothing when debugging is not enabled" (
+        do
+          result <- capture_ (runWithEmptyConfig (logDebug "hi"))
+          result `shouldBe` "")
+    it "should output somethign when debugging enabled" (
+        do
+          let config = emptyConfig { logLevel = Debug }
+          result <- capture_ (runWithConfig config (logDebug "hi"))
+          result `tshouldContain` "Debug"
+          result `tshouldContain` "hi"))
+  describe "logInfo"
+    (it "should output something" (
+        do
+          result <- capture_ (runWithEmptyConfig (logInfo "hi"))
+          time <- currentTime
+          result `tshouldContain` time
+          result `tshouldContain` "Info"
+          result `tshouldContain` "hi"))
diff --git a/test/HEyefi/SoapResponseSpec.hs b/test/HEyefi/SoapResponseSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/HEyefi/SoapResponseSpec.hs
@@ -0,0 +1,10 @@
+module HEyefi.SoapResponseSpec where
+
+import HEyefi.SoapResponse
+import HEyefi.SpecPrelude
+
+spec :: Spec
+spec =
+  describe "getPhotoStatusResponse"
+    (it "should have GetPhotoStatusResponse element"
+     (getPhotoStatusResponse `tshouldContain` "GetPhotoStatusResponse"))
diff --git a/test/HEyefi/SoapSpec.hs b/test/HEyefi/SoapSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/HEyefi/SoapSpec.hs
@@ -0,0 +1,37 @@
+module HEyefi.SoapSpec where
+
+import HEyefi.SpecPrelude
+
+import HEyefi.Soap
+import HEyefi.Types
+
+
+intToMaybe :: Int -> Maybe Int
+intToMaybe 5 = Just 5
+intToMaybe 4 = Just 4
+intToMaybe _ = Nothing
+
+spec :: Spec
+spec = do
+  describe "firstJust" (
+    do
+      it "should handle an empty list"
+        (firstJust intToMaybe [] `shouldBe` Nothing)
+      it "should handle one not matched element"
+        (firstJust intToMaybe [6] `shouldBe` Nothing)
+      it "should handle one matched element"
+        (firstJust intToMaybe [4] `shouldBe` Just 4)
+      it "should get the first matched element"
+        (firstJust intToMaybe [1,5,4] `shouldBe` Just 5))
+  describe "headerToSoapAction" (
+    do
+      it "should return StartSession for one"
+        (headerToSoapAction (soapActionHeaderName, "\"urn:StartSession\"")
+            `shouldBe` Just StartSession)
+      it "should return GetPhotoStatus for one"
+        (headerToSoapAction (soapActionHeaderName, "\"urn:GetPhotoStatus\"")
+            `shouldBe` Just GetPhotoStatus)
+      it "should return MarkLastPhotoInRoll for one"
+        (headerToSoapAction (soapActionHeaderName,
+                             "\"urn:MarkLastPhotoInRoll\"")
+            `shouldBe` Just MarkLastPhotoInRoll))
diff --git a/test/HEyefi/SpecPrelude.hs b/test/HEyefi/SpecPrelude.hs
new file mode 100644
--- /dev/null
+++ b/test/HEyefi/SpecPrelude.hs
@@ -0,0 +1,17 @@
+module HEyefi.SpecPrelude (
+  module X,
+  tshouldContain,
+  capture_) where
+
+import HEyefi.Prelude as X
+import Test.Hspec as X
+import qualified Test.Hspec as HS
+import qualified System.IO.Silently as S
+
+tshouldContain :: Text -> Text -> HS.Expectation
+tshouldContain actual expected =
+  unpack actual `shouldContain` unpack expected
+
+
+capture_ :: IO a -> IO Text
+capture_ a = fmap pack (S.capture_ a)
diff --git a/test/HEyefi/StartSessionSpec.hs b/test/HEyefi/StartSessionSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/HEyefi/StartSessionSpec.hs
@@ -0,0 +1,21 @@
+module HEyefi.StartSessionSpec where
+
+import HEyefi.Config (emptyConfig, insertCard, runWithConfig)
+import HEyefi.SpecPrelude
+import HEyefi.StartSession
+
+spec :: Spec
+spec =
+  describe "startSessionResponse"
+    (it "should respond the same as eyefiserver2"
+     (do
+         let c = insertCard "0018562de4ce" "36d61e4e7403a0586702c9159892a062" emptyConfig
+         (d , _) <- runWithConfig c (
+           startSessionResponse
+           "0018562de4ce"
+           "3623cd00fe5aef4c7ccdc66ddbe2f151"
+           "34"
+           "1356903384")
+
+         d `tshouldContain` "<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Body><StartSessionResponse xmlns=\"http://localhost/api/soap/eyefilm\"><credential>7e8f22c00390b6f1ef7bafe7613cbdd2</credential><snonce>"
+         d `tshouldContain` "</snonce><transfermode>34</transfermode><transfermodetimestamp>1356903384</transfermodetimestamp><upsyncallowed>true</upsyncallowed></StartSessionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>"))
diff --git a/test/HEyefi/UploadPhotoSpec.hs b/test/HEyefi/UploadPhotoSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/HEyefi/UploadPhotoSpec.hs
@@ -0,0 +1,52 @@
+module HEyefi.UploadPhotoSpec where
+
+import           Control.Monad
+import qualified Data.ByteString.Lazy as BS
+import           System.Directory
+import           System.Posix.User
+import           Test.Hspec
+import           System.Posix.Files (
+    setOwnerAndGroup
+  , fileOwner
+  , fileGroup
+  , getFileStatus
+  )
+
+
+import           HEyefi.Config
+import           HEyefi.Types
+import           HEyefi.UploadPhoto
+import           HEyefi.SpecPrelude
+
+
+uploadDir :: FilePath
+uploadDir = "test_upload_dir"
+
+uploadConfig :: Config
+uploadConfig = emptyConfig { uploadDirectory = uploadDir }
+
+removeUploadDir :: IO ()
+removeUploadDir = do
+  pathExists <- doesPathExist uploadDir
+  when pathExists
+    (removeDirectoryRecursive uploadDir)
+
+spec :: Spec
+spec =
+  describe "writeTarFile"
+    (it "should write the file to the upload_dir" (
+        do
+          bs <- BS.readFile "test_upload_file.tar"
+          removeUploadDir
+          createDirectory uploadDir
+          ge <- getGroupEntryForName "wheel"
+          ue <- getUserEntryForName "ryantm"
+          setOwnerAndGroup uploadDir (userID ue) (groupID ge)
+          _ <- runWithConfig uploadConfig (writeTarFile bs)
+          contents <- getDirectoryContents uploadDir
+          length contents `shouldBe` 3
+          let f = "test_upload_dir/test_upload_file.txt"
+          fs <- getFileStatus f
+          fileGroup fs `shouldBe` groupID ge
+          fileOwner fs `shouldBe` userID ue
+          removeUploadDir))
