diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for tasty-wai
+
+## 0.1.0.0 -- 2018-12-04
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENCE b/LICENCE
new file mode 100644
--- /dev/null
+++ b/LICENCE
@@ -0,0 +1,31 @@
+Copyright (c) 2018, Commonwealth Scientific and Industrial Research Organisation
+(CSIRO) ABN 41 687 119 230.
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of QFPL nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/src/Test/Tasty/Wai.hs b/src/Test/Tasty/Wai.hs
new file mode 100644
--- /dev/null
+++ b/src/Test/Tasty/Wai.hs
@@ -0,0 +1,116 @@
+-- | Types and functions for testing 'wai' endpoints using the 'tasty' testing framework.
+--
+module Test.Tasty.Wai
+  (
+    -- * Types
+    Sess (..)
+
+    -- * Creation
+  , testWai
+
+    -- * Helpers
+  , get
+  , post
+  , put
+  , assertStatus'
+
+    -- * Request Builders
+  , buildRequest
+  , buildRequestWithBody
+
+  , module Network.Wai.Test
+  ) where
+
+import qualified Control.Exception    as E
+
+import qualified Data.ByteString      as BS
+import qualified Data.ByteString.Lazy as LBS
+import           Data.Monoid          ((<>))
+
+import           Network.HTTP.Types   (StdMethod)
+import qualified Network.HTTP.Types   as HTTP
+
+import           Test.Tasty.Providers (IsTest (..), Progress (..), TestName,
+                                       TestTree, singleTest, testFailed,
+                                       testPassed)
+import           Test.Tasty.Runners   (formatMessage)
+
+import           Network.Wai          (Application, Request, requestMethod)
+import           Network.Wai.Test
+
+-- | Data structure for carrying around the info needed to build and run a test.
+data Sess = S Application TestName (Session ())
+
+instance IsTest Sess where
+  -- No options yet
+  testOptions = mempty
+
+  run _ (S app tName sess) yieldProgress = do
+
+    -- We don't really have progress to report, so state that we're running a
+    -- test but do nothing else.
+    yieldProgress $ Progress ("Running " <> tName) 0
+
+    -- The wai-extra testing uses `throwIO` to indicate a test failure and
+    -- converts that error into a 'String'. The result of the individual
+    -- 'Session a' isn't important for the test?
+    E.try (runSession sess app) >>= either toFailure toPass
+    where
+      toFailure (WaiTestFailure s) = testFailed <$> (formatMessage s)
+      toPass     _                 = pure (testPassed mempty)
+
+-- | Create an empty 'Request' using the given HTTP Method and route.
+buildRequest
+  :: StdMethod
+  -> BS.ByteString
+  -> Request
+buildRequest mth rpath = flip setPath rpath $ defaultRequest
+  { requestMethod = HTTP.renderStdMethod mth
+  }
+
+-- | As per 'buildRequest' but requires body content.
+buildRequestWithBody
+  :: StdMethod
+  -> BS.ByteString
+  -> LBS.ByteString
+  -> SRequest
+buildRequestWithBody mth rpath =
+  SRequest (buildRequest mth rpath)
+
+-- | Run a test case against a 'Application'.
+--
+-- This module re-exports the functions from 'wai-extra' for constructing the
+-- 'Session' that is executed against a given endpoint.
+--
+-- A small test case may look like:
+--
+-- @
+-- import MyApp (app)
+--
+-- testWai app "List Topics" $ do
+--       res <- get "fudge/view"
+--       assertStatus' HTTP.status200 res
+-- @
+--
+testWai :: Application -> TestName -> Session () -> TestTree
+testWai a tn = singleTest tn . S a tn
+
+-- | Submit a 'HTTP.GET' request to the provided endpoint.
+get :: BS.ByteString -> Session SResponse
+get = request . buildRequest HTTP.GET
+
+-- | Submit a 'HTTP.POST' request to the given endpoint with the provided
+-- 'LBS.ByteString' as the body content.
+post :: BS.ByteString -> LBS.ByteString -> Session SResponse
+post r = srequest . buildRequestWithBody HTTP.POST r
+
+-- | Submit a 'HTTP.PUT' request to the given endpoint with the provided
+-- 'LBS.ByteString' as the body content.
+put :: BS.ByteString -> LBS.ByteString -> Session SResponse
+put r = srequest . buildRequestWithBody HTTP.PUT r
+
+-- | An alternative helper function for checking the status code on a response
+-- that lets you use the functions from 'Network.HTTP.Types' as opposed to bare
+-- numbers.
+assertStatus' :: HTTP.Status -> SResponse -> Session ()
+assertStatus' c = assertStatus (HTTP.statusCode c)
diff --git a/tasty-wai.cabal b/tasty-wai.cabal
new file mode 100644
--- /dev/null
+++ b/tasty-wai.cabal
@@ -0,0 +1,63 @@
+-- Initial tasty-wai.cabal generated by cabal init.  For further
+-- documentation, see http://haskell.org/cabal/users-guide/
+
+name:                tasty-wai
+version:             0.1.0.0
+synopsis:            Test 'wai' endpoints via Test.Tasty
+description:         Helper functions and runners for testing wai endpoints using the Tasty testing infrastructure.
+license:             BSD3
+license-file:        LICENCE
+author:              QFPL @ Data61
+maintainer:          sean.chalmers@data61.csiro.au
+
+copyright:           Copyright (C) 2018 Commonwealth Scientific and Industrial Research Organisation (CSIRO)
+
+category:            Testing,Web
+
+build-type:          Simple
+extra-source-files:  CHANGELOG.md
+cabal-version:       >=1.10
+
+tested-with:         GHC == 7.10.3
+                   , GHC == 8.0.2
+                   , GHC == 8.2.2
+                   , GHC == 8.4.1
+                   , GHC == 8.6.1
+
+source-repository head
+    type: git
+    location: https://github.com/qfpl/tasty-wai
+
+library
+  exposed-modules:     Test.Tasty.Wai
+
+  -- other-modules:
+  -- other-extensions:
+
+  build-depends:       base >=4.8 && <4.13
+                     , tasty >= 0.8 && < 1.3
+                     , bytestring == 0.10.*
+                     , wai == 3.2.*
+                     , wai-extra == 3.0.*
+                     , http-types >= 0.9 && < 0.13
+
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+
+
+test-suite tests
+  default-language:    Haskell2010
+
+  type:                exitcode-stdio-1.0
+
+  ghc-options:         -threaded
+
+  hs-source-dirs:      test
+  main-is:             Test.hs
+
+  build-depends:       base >=4.8 && <4.13
+                     , tasty >= 0.8 && < 1.3
+                     , bytestring == 0.10.*
+                     , wai == 3.2.*
+                     , http-types >= 0.9 && < 0.13
+                     , tasty-wai
diff --git a/test/Test.hs b/test/Test.hs
new file mode 100644
--- /dev/null
+++ b/test/Test.hs
@@ -0,0 +1,50 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import           Network.Wai        (Application)
+import qualified Network.Wai        as W
+
+import qualified Network.HTTP.Types as H
+
+import           Test.Tasty         (defaultMain, testGroup)
+import           Test.Tasty.Wai     (assertBody, assertStatus, assertStatus',
+                                     get, post, testWai)
+
+testApp :: Application
+testApp rq cb = do
+  let
+    mkresp s = W.responseLBS s []
+    resp404 = mkresp H.status404
+    resp200 = mkresp H.status200
+
+  resp <- case (W.requestMethod rq, W.pathInfo rq) of
+
+    -- Ye olde...
+    ("GET", ["hello"]) -> pure $ resp200 "world!"
+
+    -- Echo me this!
+    ("POST", ["echo"]) -> resp200 <$> W.strictRequestBody rq
+
+    -- Well, then...
+    _ -> pure $ resp404 "no route"
+
+  cb resp
+
+main :: IO ()
+main = defaultMain $ testGroup "Tasty-Wai Tests"
+
+  [ testWai testApp "Hello to World" $ do
+      res <- get "hello"
+      assertBody "world!" res
+
+  , testWai testApp "Echo to thee" $ do
+      res <- post "echo" "thus"
+      assertStatus' H.status200 res -- Use functions from Network.HTTP.Types
+      assertStatus 200 res          -- Use raw ints
+      assertBody "thus" res
+
+  , testWai testApp "Will die!" $ do
+      res <- get "not-a-thing"
+      assertStatus' H.status404 res
+      assertBody "no route" res
+  ]
