diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+## 0.1.0.0 (2021-05-30)
+
+- Basic Gerrit API data types.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,20 @@
+# gerrit
+
+[![Hackage](https://img.shields.io/hackage/v/gerrit.svg?logo=haskell)](https://hackage.haskell.org/package/gerrit)
+[![Apache-2.0 license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
+
+A client library for [gerrit](https://www.gerritcodereview.com/).
+
+## Features
+
+- JSON decoder for API endpoints and events stream.
+- HTTP client helper functions.
+
+## Contribute
+
+Run tests:
+
+```
+$ cabal build
+$ cabal test
+```
diff --git a/gerrit.cabal b/gerrit.cabal
new file mode 100644
--- /dev/null
+++ b/gerrit.cabal
@@ -0,0 +1,72 @@
+cabal-version:       2.4
+name:                gerrit
+version:             0.1.0.0
+synopsis:            A gerrit client library
+description:
+    Gerrit is a client library to interface with https://www.gerritcodereview.com/
+    .
+    Use this library to query and post to gerrit REST API.
+    .
+homepage:            https://github.com/softwarefactory-project/gerrit-haskell#readme
+bug-reports:         https://github.com/softwarefactory-project/gerrit-haskell/issues
+license:             Apache-2.0
+license-file:        LICENSE
+author:              Tristan de Cacqueray
+maintainer:          tdecacqu@redhat.com
+copyright:           2020 Red Hat
+category:            Development
+build-type:          Simple
+extra-doc-files:     CHANGELOG.md
+                     README.md
+extra-source-files:  test/data/*.json
+tested-with:         GHC == 8.10.4
+
+source-repository head
+  type:                git
+  location:            https://github.com/softwarefactory-project/gerrit-haskell.git
+
+common common-options
+  build-depends:       base                   >= 4.11.0.0 && < 5
+                     , aeson                  >= 1.0.0.0  && < 1.6
+                     , bytestring             < 0.11
+  ghc-options:         -Wall
+                       -Wcompat
+                       -Widentities
+                       -Wincomplete-uni-patterns
+                       -Wincomplete-record-updates
+                       -Wredundant-constraints
+  if impl(ghc >= 8.2)
+    ghc-options:       -fhide-source-paths
+  if impl(ghc >= 8.4)
+    ghc-options:       -Wmissing-export-lists
+                       -Wpartial-fields
+  if impl(ghc >= 8.10)
+    ghc-options:       -Wunused-packages
+
+  default-language:    Haskell2010
+
+library
+  import:              common-options
+  build-depends:       containers             >= 0.5.8.0  && < 0.7
+                     , http-client            >= 0.5.0    && < 0.8
+                     , http-client-tls        >= 0.2.0    && < 0.4
+                     , text                   >= 0.11.1.0 && < 1.3
+
+  hs-source-dirs:      src
+  exposed-modules:     Gerrit
+                     , Gerrit.Data
+                     , Gerrit.Event
+  other-modules:       Gerrit.Client
+
+test-suite gerrit-test
+  import:              common-options
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Spec.hs
+  build-depends:       directory
+                     , gerrit
+                     , tasty                      >= 0.11.2   && < 1.5
+                     , tasty-hunit                >= 0.10     && < 0.11
+  ghc-options:         -threaded
+                       -rtsopts
+                       -with-rtsopts=-N
diff --git a/src/Gerrit.hs b/src/Gerrit.hs
new file mode 100644
--- /dev/null
+++ b/src/Gerrit.hs
@@ -0,0 +1,96 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | This module contains the gerrit client library
+module Gerrit
+  ( -- * Client
+    GerritClient,
+    withClient,
+
+    -- * Api
+    getVersion,
+    getChange,
+    queryChanges,
+    postReview,
+
+    -- * Main data types
+    GerritVersion (..),
+    GerritQuery (..),
+    GerritChange (..),
+    GerritChangeStatus (..),
+    ReviewResult (..),
+
+    -- * Convenient functions
+    changeUrl,
+    hasLabel,
+  )
+where
+
+import Control.Exception (try)
+import qualified Data.Map as M
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import qualified Data.Text as T
+import Gerrit.Client
+import Gerrit.Data
+import Network.HTTP.Client (HttpException)
+
+-- | Return the url of a 'GerritChange'
+changeUrl :: GerritClient -> GerritChange -> T.Text
+changeUrl client change = baseUrl client <> T.pack (show (number change))
+
+-- | Get the server version
+getVersion :: GerritClient -> IO GerritVersion
+getVersion = gerritGet "config/server/version"
+
+-- | Search for changes
+queryChanges :: Int -> [GerritQuery] -> GerritClient -> IO [GerritChange]
+queryChanges count queries = gerritGet ("changes/?" <> queryString)
+  where
+    queryString = T.intercalate "&" [changeString, countString, option]
+    changeString = "q=" <> T.intercalate "+" (map queryText queries)
+    countString = "n=" <> T.pack (show count)
+    option = "o=CURRENT_REVISION&o=DETAILED_LABELS"
+
+getChange :: Int -> GerritClient -> IO (Maybe GerritChange)
+getChange changeNumber client = do
+  res <-
+    try
+      ( gerritGet ("changes/" <> T.pack (show changeNumber) <> "?o=CURRENT_REVISION&o=DETAILED_LABELS") client
+      ) ::
+      IO (Either HttpException GerritChange)
+  pure $ case res of
+    Right change -> Just change
+    Left _err -> Nothing
+
+-- | Post a review
+postReview ::
+  -- | The change to review
+  GerritChange ->
+  -- | A message
+  Text ->
+  -- | A label
+  Text ->
+  -- | A vote
+  Int ->
+  -- | The client
+  GerritClient ->
+  -- | Returns the ReviewResult
+  IO ReviewResult
+postReview change message label value' = gerritPost urlPath review
+  where
+    urlPath = "changes/" <> changeId <> "/revisions/" <> revHash <> "/review"
+    changeId = Gerrit.Data.id change
+    revHash = fromMaybe "" (Gerrit.Data.current_revision change)
+    review =
+      ReviewInput
+        { riMessage = Just message,
+          riLabels = Just (M.fromList [(label, value')])
+        }
+
+-- | Check if a gerrit change as a label
+hasLabel :: T.Text -> Int -> GerritChange -> Bool
+hasLabel label labelValue change = case M.lookup label (labels change) of
+  Just gerritLabel ->
+    (> 0) $
+      length $ filter (\vote -> fromMaybe 0 (value vote) == labelValue) (Gerrit.Data.all gerritLabel)
+  _ -> False
diff --git a/src/Gerrit/Client.hs b/src/Gerrit/Client.hs
new file mode 100644
--- /dev/null
+++ b/src/Gerrit/Client.hs
@@ -0,0 +1,79 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+
+-- | This module contains the internal gerrit REST client
+module Gerrit.Client
+  ( GerritClient (baseUrl),
+    withClient,
+    gerritGet,
+    gerritPost,
+  )
+where
+
+import Data.Aeson (FromJSON, ToJSON, eitherDecode, encode)
+import qualified Data.ByteString.Lazy as BSL
+import Data.Maybe (fromMaybe)
+import Data.Text (Text, unpack)
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import Network.HTTP.Client
+import Network.HTTP.Client.TLS (tlsManagerSettings)
+import System.Environment (lookupEnv)
+
+-- | The GerritClient record, use 'withClient' to create
+data GerritClient = GerritClient
+  { baseUrl :: Text,
+    manager :: Manager,
+    auth :: Maybe (Text, Text)
+  }
+
+-- | Create the 'GerritClient'
+withClient ::
+  -- | The gerrit api url
+  Text ->
+  -- | A username (password is read from GERRIT_PASSWORD environment)
+  Maybe Text ->
+  -- | The callback
+  (GerritClient -> IO ()) ->
+  -- | withClient performs the IO
+  IO ()
+withClient url username callBack =
+  do
+    manager <- newManager tlsManagerSettings
+    auth <- case username of
+      Just user -> do
+        pass <- lookupEnv "GERRIT_PASSWORD"
+        pure $ Just (user, T.pack $ fromMaybe "" pass)
+      _ -> pure Nothing
+    callBack (GerritClient {..})
+  where
+    baseUrl = T.dropWhileEnd (== '/') url <> "/"
+
+gerritDecode :: (FromJSON a, Applicative f) => Response BSL.ByteString -> f a
+gerritDecode response = case eitherDecode $ BSL.drop 5 $ responseBody response of
+  Left err -> error $ "Decoding of " <> show (responseBody response) <> " failed with: " <> err
+  Right a -> pure a
+
+gerritPost :: (ToJSON a, FromJSON b) => Text -> a -> GerritClient -> IO b
+gerritPost path postData GerritClient {..} =
+  do
+    initRequest <- case auth of
+      Just (user, pass) ->
+        applyBasicAuth (T.encodeUtf8 user) (T.encodeUtf8 pass)
+          <$> parseUrlThrow (unpack $ baseUrl <> "a/" <> path)
+      Nothing -> parseUrlThrow (unpack $ baseUrl <> path)
+    let request =
+          initRequest
+            { method = "POST",
+              requestHeaders = requestHeaders initRequest <> [("Content-Type", "application/json; charset=UTF-8")],
+              requestBody = RequestBodyLBS $ encode postData
+            }
+    response <- httpLbs request manager
+    gerritDecode response
+
+gerritGet :: (FromJSON a) => Text -> GerritClient -> IO a
+gerritGet path GerritClient {..} =
+  do
+    request <- parseUrlThrow (unpack $ baseUrl <> path)
+    response <- httpLbs request manager
+    gerritDecode response
diff --git a/src/Gerrit/Data.hs b/src/Gerrit/Data.hs
new file mode 100644
--- /dev/null
+++ b/src/Gerrit/Data.hs
@@ -0,0 +1,168 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | This module contains the gerrit data type
+module Gerrit.Data
+  ( -- * Main data types
+    GerritVersion (..),
+    GerritQuery (..),
+    GerritChangeStatus (..),
+    GerritChange (..),
+    GerritRevision (..),
+    GerritLabel (..),
+    GerritAccount (..),
+    GerritLabelVote (..),
+    GerritDetailedLabelVote (..),
+    GerritDetailedLabel (..),
+    ReviewResult (..),
+    ReviewInput (..),
+
+    -- * Convenient functions
+    queryText,
+  )
+where
+
+import Data.Aeson
+import Data.Char (isUpper, toLower)
+import qualified Data.Map as M
+import Data.Text (Text)
+import qualified Data.Text as T
+import GHC.Generics (Generic)
+
+newtype GerritVersion = GerritVersion Text
+  deriving (Show, Generic)
+  deriving anyclass (FromJSON)
+
+-- https://gerrit-review.googlesource.com/Documentation/json.html
+data GerritRevisionKind = REWORK | TRIVIAL_REBASE | MERGE_FIRST_PARENT_UPDATE | NO_CODE_CHANGE | NO_CHANGE
+  deriving (Eq, Show, Generic, FromJSON)
+
+data GerritChangeStatus = NEW | MERGED | ABANDONED | DRAFT
+  deriving (Eq, Show, Generic, FromJSON)
+
+data GerritLabelVote = REJECTED | APPROVED | DISLIKED | RECOMMENDED
+  deriving (Eq, Show, Ord, Generic)
+
+-- We use a custom parseJSON to decode Label Vote as lowercase
+instance FromJSON GerritLabelVote where
+  parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = map toLower}
+
+instance FromJSONKey GerritLabelVote where
+  fromJSONKey = genericFromJSONKey defaultJSONKeyOptions {keyModifier = map toLower}
+
+-- | Modify record attribute to match json schema
+-- Remove the prefix and use snakecase
+customParseJSON :: String -> Options
+customParseJSON prefix = defaultOptions {fieldLabelModifier = recordToJson}
+  where
+    recordToJson = updateCase . drop (length prefix)
+    updateCase [] = []
+    updateCase (x : xs) = toLower x : updateCase' xs
+    updateCase' [] = []
+    updateCase' (x : xs)
+      | isUpper x = '_' : toLower x : updateCase' xs
+      | otherwise = x : updateCase' xs
+
+-- https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html
+data ReviewResult = ReviewResult
+  { rrLabels :: Maybe (M.Map Text Int),
+    rrReady :: Maybe Int
+  }
+  deriving (Eq, Show, Ord, Generic)
+
+instance FromJSON ReviewResult where
+  parseJSON = genericParseJSON $ customParseJSON "rr"
+
+instance ToJSON ReviewResult where
+  toJSON = genericToJSON $ customParseJSON "rr"
+
+data ReviewInput = ReviewInput
+  { riMessage :: Maybe Text,
+    riLabels :: Maybe (M.Map Text Int)
+  }
+  deriving (Eq, Show, Ord, Generic)
+
+instance FromJSON ReviewInput where
+  parseJSON = genericParseJSON $ customParseJSON "ri"
+
+instance ToJSON ReviewInput where
+  toJSON = genericToJSON $ (customParseJSON "ri") {omitNothingFields = True}
+
+-- https://gerrit-review.googlesource.com/Documentation/user-search.html
+data GerritQuery
+  = Status GerritChangeStatus
+  | Owner Text
+  | CommitMessage Text
+  | Project Text
+  | ChangeId Text
+
+-- | Convert a GerritQuery object to the search terms
+queryText :: GerritQuery -> Text
+queryText (Status stat) = "status:" <> T.toLower (T.pack $ show stat)
+queryText (Owner owner) = "owner:" <> owner
+queryText (CommitMessage message) = "message:" <> message
+queryText (Project project') = "project:" <> project'
+queryText (ChangeId changeId) = "change:" <> changeId
+
+data GerritRevision = GerritRevision
+  { ref :: Text,
+    kind :: GerritRevisionKind
+  }
+  deriving (Show, Generic, FromJSON)
+
+newtype GerritAccount = GerritAccount
+  { unused_account_id :: Int
+  }
+  deriving (Show, Generic)
+
+-- We use a cusom parseJSON to decode `_account_id` as `account_id`
+instance FromJSON GerritAccount where
+  parseJSON = genericParseJSON aesonOptions
+
+newtype GerritLabel
+  = GerritLabel (M.Map GerritLabelVote GerritAccount)
+  deriving (Show, Generic)
+  deriving anyclass (FromJSON)
+
+data GerritDetailedLabelVote = GerritDetailedLabelVote
+  { value :: Maybe Int,
+    account_id :: Int
+  }
+  deriving (Show, Generic)
+
+-- We use a cusom parseJSON to decode record field properly
+instance FromJSON GerritDetailedLabelVote where
+  parseJSON = genericParseJSON aesonOptions
+
+data GerritDetailedLabel = GerritDetailedLabel
+  { all :: [GerritDetailedLabelVote],
+    default_value :: Int
+  }
+  deriving (Show, Generic, FromJSON)
+
+data GerritChange = GerritChange
+  { id :: Text,
+    project :: Text,
+    branch :: Text,
+    subject :: Text,
+    status :: GerritChangeStatus,
+    mergeable :: Maybe Bool,
+    revisions :: M.Map Text (Maybe GerritRevision),
+    current_revision :: Maybe Text,
+    number :: Int,
+    labels :: M.Map Text GerritDetailedLabel
+  }
+  deriving (Show, Generic)
+
+-- We use a cusom parseJSON to decode `_number` as `number`
+instance FromJSON GerritChange where
+  parseJSON = genericParseJSON aesonOptions
+
+aesonOptions :: Options
+aesonOptions = defaultOptions {fieldLabelModifier = recordToJson}
+  where
+    recordToJson "number" = "_number"
+    recordToJson "account_id" = "_account_id"
+    recordToJson n = n
diff --git a/src/Gerrit/Event.hs b/src/Gerrit/Event.hs
new file mode 100644
--- /dev/null
+++ b/src/Gerrit/Event.hs
@@ -0,0 +1,448 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+
+-- | Gerrit stream event data type
+module Gerrit.Event
+  ( -- * Main event data types
+    EventType (..),
+    Event (..),
+
+    -- * Common data types
+    User (..),
+    PatchSet (..),
+    Change (..),
+    Ref (..),
+
+    -- * Individual event data types
+    AssigneeChanged (..),
+    ChangeAbandoned (..),
+    ChangeDeleted (..),
+    ChangeMerged (..),
+    ChangeRestored (..),
+    CommentAdded (..),
+    HashtagsChanged (..),
+    ProjectCreated (..),
+    PatchsetCreated (..),
+    RefUpdated (..),
+    ReviewerAdded (..),
+    ReviewerDeleted (..),
+    TopicChanged (..),
+    WorkInProgressStateChanged (..),
+    PrivateStateChanged (..),
+    VoteDeleted (..),
+
+    -- * Convenient functions
+    getChange,
+    getUser,
+    getEventType,
+    eventName,
+  )
+where
+
+import Control.Monad (mzero)
+import Data.Aeson
+  ( FromJSON (..),
+    Options (fieldLabelModifier),
+    Value (Object, String),
+    defaultOptions,
+    genericParseJSON,
+    (.:),
+  )
+import Data.Char (toLower)
+import Data.Text (Text)
+import qualified Data.Text as Text
+import GHC.Generics (Generic)
+
+aesonOption :: Text -> Options
+aesonOption prefix = defaultOptions {fieldLabelModifier = recordToJson}
+  where
+    recordToJson = updateCase . drop (Text.length prefix)
+    updateCase [] = []
+    updateCase (x : xs) = toLower x : xs
+
+-------------------------------------------------------------------------------
+-- Shared types
+-------------------------------------------------------------------------------
+data User = User
+  { userName :: Maybe Text,
+    userEmail :: Maybe Text,
+    userUsername :: Maybe Text
+  }
+  deriving (Show, Eq, Generic)
+
+instance FromJSON User where
+  parseJSON = genericParseJSON $ aesonOption "user"
+
+data PatchSet = PatchSet
+  { patchSetNumber :: Int,
+    patchSetRef :: Text,
+    patchSetUploader :: User,
+    patchsetAuthor :: User
+  }
+  deriving (Show, Eq, Generic)
+
+instance FromJSON PatchSet where
+  parseJSON = genericParseJSON $ aesonOption "patchSet"
+
+data Change = Change
+  { changeProject :: Text,
+    changeBranch :: Text,
+    changeSubject :: Text,
+    changeUrl :: Text,
+    changeOwner :: User
+  }
+  deriving (Show, Eq, Generic)
+
+instance FromJSON Change where
+  parseJSON = genericParseJSON $ aesonOption "change"
+
+data Ref = Ref
+  { refOldRev :: Text,
+    refNewRev :: Text,
+    refRefName :: Text,
+    refProject :: Text
+  }
+  deriving (Show, Eq, Generic)
+
+instance FromJSON Ref where
+  parseJSON = genericParseJSON $ aesonOption "ref"
+
+-------------------------------------------------------------------------------
+-- Individual Event data types
+-------------------------------------------------------------------------------
+data AssigneeChanged = AssigneeChanged
+  { assigneeChangedChange :: Change,
+    assigneeChangedChanger :: User
+  }
+  deriving (Show, Eq, Generic)
+
+data ChangeAbandoned = ChangeAbandoned
+  { changeAbandonedProject :: Text,
+    changeAbandonedAbandoner :: User,
+    changeAbandonedChange :: Change,
+    changeAbandonedPatchSet :: PatchSet
+  }
+  deriving (Show, Eq, Generic)
+
+data ChangeDeleted = ChangeDeleted
+  { changeDeletedChange :: Change,
+    changeDeletedDeleter :: User
+  }
+  deriving (Show, Eq, Generic)
+
+data ChangeMerged = ChangeMerged
+  { changeMergedProject :: Text,
+    changeMergedSubmitter :: User,
+    changeMergedChange :: Change,
+    changeMergedPatchSet :: PatchSet
+  }
+  deriving (Show, Eq, Generic)
+
+data ChangeRestored = ChangeRestored
+  { changeRestoredChange :: Change,
+    changeRestoredPatchSet :: PatchSet,
+    changeRestoredRestorer :: User,
+    changeRestoredReason :: Text
+  }
+  deriving (Show, Eq, Generic)
+
+data CommentAdded = CommentAdded
+  { commentAddedChange :: Change,
+    commentAddedPatchSet :: PatchSet,
+    commentAddedAuthor :: User
+  }
+  deriving (Show, Eq, Generic)
+
+data HashtagsChanged = HashtagsChanged
+  { hashtagsChangedChange :: Change,
+    hashtagsChangedEditor :: User
+  }
+  deriving (Show, Eq, Generic)
+
+data ProjectCreated = ProjectCreated
+  { projectCreatedProjectName :: Text,
+    projectCreatedHeadName :: Text
+  }
+  deriving (Show, Eq, Generic)
+
+data PatchsetCreated = PatchsetCreated
+  { patchsetCreatedUploader :: User,
+    patchsetCreatedChange :: Change,
+    patchsetCreatedPatchSet :: PatchSet
+  }
+  deriving (Show, Eq, Generic)
+
+data RefUpdated = RefUpdated
+  { refUpdatedSubmitter :: User,
+    refUpdatedRefUpdate :: Ref
+  }
+  deriving (Show, Eq, Generic)
+
+data ReviewerAdded = ReviewerAdded
+  { reviewerAddedChange :: Change,
+    reviewerAddedPatchSet :: PatchSet,
+    reviewerAddedAdder :: User
+  }
+  deriving (Show, Eq, Generic)
+
+data ReviewerDeleted = ReviewerDeleted
+  { reviewerDeletedChange :: Change,
+    reviewerDeletedPatchSet :: PatchSet,
+    reviewerDeletedRemover :: User
+  }
+  deriving (Show, Eq, Generic)
+
+data TopicChanged = TopicChanged
+  { topicChangedChange :: Change,
+    topicChangedChanger :: User,
+    topicChangedOldTopic :: Text
+  }
+  deriving (Show, Eq, Generic)
+
+data WorkInProgressStateChanged = WorkInProgressStateChanged
+  { workInProgressStateChangedChange :: Change,
+    workInProgressStateChangedPatchSet :: PatchSet,
+    workInProgressStateChangedChanger :: User
+  }
+  deriving (Show, Eq, Generic)
+
+data PrivateStateChanged = PrivateStateChanged
+  { privateStateChangedChange :: Change,
+    privateStateChangedPatchSet :: PatchSet,
+    privateStateChangedChanger :: User
+  }
+  deriving (Show, Eq, Generic)
+
+data VoteDeleted = VoteDeleted
+  { voteDeletedChange :: Change,
+    voteDeletedPatchSet :: PatchSet,
+    voteDeletedRemover :: User
+  }
+  deriving (Show, Eq, Generic)
+
+-------------------------------------------------------------------------------
+-- Events FromJSON instances
+-------------------------------------------------------------------------------
+instance FromJSON AssigneeChanged where
+  parseJSON = genericParseJSON $ aesonOption "assigneeChanged"
+
+instance FromJSON ChangeAbandoned where
+  parseJSON = genericParseJSON $ aesonOption "changeAbandoned"
+
+instance FromJSON ChangeDeleted where
+  parseJSON = genericParseJSON $ aesonOption "changeDeleted"
+
+instance FromJSON ChangeMerged where
+  parseJSON = genericParseJSON $ aesonOption "changeMerged"
+
+instance FromJSON ChangeRestored where
+  parseJSON = genericParseJSON $ aesonOption "changeRestored"
+
+instance FromJSON CommentAdded where
+  parseJSON = genericParseJSON $ aesonOption "commentAdded"
+
+instance FromJSON HashtagsChanged where
+  parseJSON = genericParseJSON $ aesonOption "hashtagsChanged"
+
+instance FromJSON ProjectCreated where
+  parseJSON = genericParseJSON $ aesonOption "projectCreated"
+
+instance FromJSON PatchsetCreated where
+  parseJSON = genericParseJSON $ aesonOption "patchsetCreated"
+
+instance FromJSON RefUpdated where
+  parseJSON = genericParseJSON $ aesonOption "refUpdated"
+
+instance FromJSON ReviewerAdded where
+  parseJSON = genericParseJSON $ aesonOption "reviewerAdded"
+
+instance FromJSON ReviewerDeleted where
+  parseJSON = genericParseJSON $ aesonOption "reviewerDeleted"
+
+instance FromJSON TopicChanged where
+  parseJSON = genericParseJSON $ aesonOption "topicChanged"
+
+instance FromJSON WorkInProgressStateChanged where
+  parseJSON = genericParseJSON $ aesonOption "workInProgressStateChanged"
+
+instance FromJSON PrivateStateChanged where
+  parseJSON = genericParseJSON $ aesonOption "privateStateChanged"
+
+instance FromJSON VoteDeleted where
+  parseJSON = genericParseJSON $ aesonOption "voteDeleted"
+
+-- | The event "type" data type
+data EventType
+  = AssigneeChangedEvent
+  | ChangeAbandonedEvent
+  | ChangeDeletedEvent
+  | ChangeMergedEvent
+  | ChangeRestoredEvent
+  | CommentAddedEvent
+  | DroppedOutputEvent
+  | HashtagsChangedEvent
+  | ProjectCreatedEvent
+  | PatchsetCreatedEvent
+  | RefUpdatedEvent
+  | ReviewerAddedEvent
+  | ReviewerDeletedEvent
+  | TopicChangedEvent
+  | WorkInProgressStateChangedEvent
+  | PrivateStateChangedEvent
+  | VoteDeletedEvent
+  deriving (Show, Eq)
+
+instance FromJSON EventType where
+  parseJSON (String v) = case v of
+    "assignee-changed" -> pure AssigneeChangedEvent
+    "change-abandoned" -> pure ChangeAbandonedEvent
+    "change-deleted" -> pure ChangeDeletedEvent
+    "change-merged" -> pure ChangeMergedEvent
+    "change-restored" -> pure ChangeRestoredEvent
+    "comment-added" -> pure CommentAddedEvent
+    "dropped-output" -> pure DroppedOutputEvent
+    "hashtags-changed" -> pure HashtagsChangedEvent
+    "project-created" -> pure ProjectCreatedEvent
+    "patchset-created" -> pure PatchsetCreatedEvent
+    "ref-updated" -> pure RefUpdatedEvent
+    "reviewer-added" -> pure ReviewerAddedEvent
+    "reviewer-deleted" -> pure ReviewerDeletedEvent
+    "topic-changed" -> pure TopicChangedEvent
+    "wip-state-changed" -> pure WorkInProgressStateChangedEvent
+    "private-state-changed" -> pure PrivateStateChangedEvent
+    "vote-deleted" -> pure VoteDeletedEvent
+    _ -> mzero
+  parseJSON _ = mzero
+
+-- | The main event union data type
+data Event
+  = EventAssigneeChanged AssigneeChanged
+  | EventChangeAbandoned ChangeAbandoned
+  | EventChangeDeleted ChangeDeleted
+  | EventChangeMerged ChangeMerged
+  | EventChangeRestored ChangeRestored
+  | EventCommentAdded CommentAdded
+  | EventDroppedOutput
+  | EventHashtagsChanged HashtagsChanged
+  | EventProjectCreated ProjectCreated
+  | EventPatchsetCreated PatchsetCreated
+  | EventRefUpdated RefUpdated
+  | EventReviewerAdded ReviewerAdded
+  | EventReviewerDeleted ReviewerDeleted
+  | EventTopicChanged TopicChanged
+  | EventWorkInProgressStateChanged WorkInProgressStateChanged
+  | EventPrivateStateChanged PrivateStateChanged
+  | EventVoteDeleted VoteDeleted
+  deriving (Show, Eq)
+
+instance FromJSON Event where
+  parseJSON o@(Object v) = do
+    eType <- v .: "type"
+    case eType of
+      AssigneeChangedEvent -> EventAssigneeChanged <$> parseJSON o
+      ChangeAbandonedEvent -> EventChangeAbandoned <$> parseJSON o
+      ChangeDeletedEvent -> EventChangeDeleted <$> parseJSON o
+      ChangeMergedEvent -> EventChangeMerged <$> parseJSON o
+      ChangeRestoredEvent -> EventChangeRestored <$> parseJSON o
+      CommentAddedEvent -> EventCommentAdded <$> parseJSON o
+      DroppedOutputEvent -> pure EventDroppedOutput
+      HashtagsChangedEvent -> EventHashtagsChanged <$> parseJSON o
+      ProjectCreatedEvent -> EventProjectCreated <$> parseJSON o
+      PatchsetCreatedEvent -> EventPatchsetCreated <$> parseJSON o
+      RefUpdatedEvent -> EventRefUpdated <$> parseJSON o
+      ReviewerAddedEvent -> EventReviewerAdded <$> parseJSON o
+      ReviewerDeletedEvent -> EventReviewerDeleted <$> parseJSON o
+      TopicChangedEvent -> EventTopicChanged <$> parseJSON o
+      WorkInProgressStateChangedEvent -> EventWorkInProgressStateChanged <$> parseJSON o
+      PrivateStateChangedEvent -> EventPrivateStateChanged <$> parseJSON o
+      VoteDeletedEvent -> EventVoteDeleted <$> parseJSON o
+  parseJSON _ = mzero
+
+-------------------------------------------------------------------------------
+-- Utilities function
+-------------------------------------------------------------------------------
+
+-- | Get the associated change
+getChange :: Event -> Maybe Change
+getChange event = case event of
+  EventAssigneeChanged AssigneeChanged {..} -> Just assigneeChangedChange
+  EventChangeAbandoned ChangeAbandoned {..} -> Just changeAbandonedChange
+  EventChangeDeleted ChangeDeleted {..} -> Just changeDeletedChange
+  EventChangeMerged ChangeMerged {..} -> Just changeMergedChange
+  EventChangeRestored ChangeRestored {..} -> Just changeRestoredChange
+  EventCommentAdded CommentAdded {..} -> Just commentAddedChange
+  EventDroppedOutput -> Nothing
+  EventHashtagsChanged HashtagsChanged {..} -> Just hashtagsChangedChange
+  EventProjectCreated _ -> Nothing
+  EventPatchsetCreated PatchsetCreated {..} -> Just patchsetCreatedChange
+  EventRefUpdated _ -> Nothing
+  EventReviewerAdded ReviewerAdded {..} -> Just reviewerAddedChange
+  EventReviewerDeleted ReviewerDeleted {..} -> Just reviewerDeletedChange
+  EventTopicChanged TopicChanged {..} -> Just topicChangedChange
+  EventWorkInProgressStateChanged WorkInProgressStateChanged {..} -> Just workInProgressStateChangedChange
+  EventPrivateStateChanged PrivateStateChanged {..} -> Just privateStateChangedChange
+  EventVoteDeleted VoteDeleted {..} -> Just voteDeletedChange
+
+-- | Get the associated author
+getUser :: Event -> Maybe User
+getUser event = case event of
+  EventAssigneeChanged AssigneeChanged {..} -> Just assigneeChangedChanger
+  EventChangeAbandoned ChangeAbandoned {..} -> Just changeAbandonedAbandoner
+  EventChangeDeleted ChangeDeleted {..} -> Just changeDeletedDeleter
+  EventChangeMerged ChangeMerged {..} -> Just changeMergedSubmitter
+  EventChangeRestored ChangeRestored {..} -> Just changeRestoredRestorer
+  EventCommentAdded CommentAdded {..} -> Just commentAddedAuthor
+  EventDroppedOutput -> Nothing
+  EventHashtagsChanged HashtagsChanged {..} -> Just hashtagsChangedEditor
+  EventProjectCreated _ -> Nothing
+  EventPatchsetCreated PatchsetCreated {..} -> Just patchsetCreatedUploader
+  EventRefUpdated _ -> Nothing
+  EventReviewerAdded ReviewerAdded {..} -> Just reviewerAddedAdder
+  EventReviewerDeleted ReviewerDeleted {..} -> Just reviewerDeletedRemover
+  EventTopicChanged TopicChanged {..} -> Just topicChangedChanger
+  EventWorkInProgressStateChanged WorkInProgressStateChanged {..} -> Just workInProgressStateChangedChanger
+  EventPrivateStateChanged PrivateStateChanged {..} -> Just privateStateChangedChanger
+  EventVoteDeleted VoteDeleted {..} -> Just voteDeletedRemover
+
+-- | Get the 'EventType' back from an 'Event'
+getEventType :: Event -> EventType
+getEventType event = case event of
+  EventAssigneeChanged _ -> AssigneeChangedEvent
+  EventChangeAbandoned _ -> ChangeAbandonedEvent
+  EventChangeDeleted _ -> ChangeDeletedEvent
+  EventChangeMerged _ -> ChangeMergedEvent
+  EventChangeRestored _ -> ChangeRestoredEvent
+  EventCommentAdded _ -> CommentAddedEvent
+  EventDroppedOutput -> DroppedOutputEvent
+  EventHashtagsChanged _ -> HashtagsChangedEvent
+  EventProjectCreated _ -> ProjectCreatedEvent
+  EventPatchsetCreated _ -> PatchsetCreatedEvent
+  EventRefUpdated _ -> RefUpdatedEvent
+  EventReviewerAdded _ -> ReviewerAddedEvent
+  EventReviewerDeleted _ -> ReviewerDeletedEvent
+  EventTopicChanged _ -> TopicChangedEvent
+  EventWorkInProgressStateChanged _ -> WorkInProgressStateChangedEvent
+  EventPrivateStateChanged _ -> PrivateStateChangedEvent
+  EventVoteDeleted _ -> VoteDeletedEvent
+
+-- | Get the text representation suitable for event subscription.
+eventName :: EventType -> Text
+eventName eventType = case eventType of
+  AssigneeChangedEvent -> "assignee-changed"
+  ChangeAbandonedEvent -> "change-abandoned"
+  ChangeDeletedEvent -> "change-deleted"
+  ChangeMergedEvent -> "change-merged"
+  ChangeRestoredEvent -> "change-restored"
+  CommentAddedEvent -> "comment-added"
+  DroppedOutputEvent -> "dropped-output"
+  HashtagsChangedEvent -> "hashtags-changed"
+  ProjectCreatedEvent -> "project-created"
+  PatchsetCreatedEvent -> "patchset-created"
+  RefUpdatedEvent -> "ref-updated"
+  ReviewerAddedEvent -> "reviewer-added"
+  ReviewerDeletedEvent -> "reviewer-deleted"
+  TopicChangedEvent -> "topic-changed"
+  WorkInProgressStateChangedEvent -> "wip-state-changed"
+  PrivateStateChangedEvent -> "private-state-changed"
+  VoteDeletedEvent -> "vote-deleted"
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,111 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main (main) where
+
+import Data.Aeson (decode, encode)
+import Data.ByteString.Lazy (ByteString)
+import qualified Data.ByteString.Lazy as BSL
+import Data.Maybe (fromJust)
+import Gerrit
+import Gerrit.Data as Gerrit
+import qualified Gerrit.Event as Event
+import System.Directory (listDirectory)
+import Test.Tasty
+import Test.Tasty.HUnit
+
+main :: IO ()
+main =
+  do
+    files <- listDirectory "./test/data"
+    dataFiles <- traverse (BSL.readFile . ("./test/data/" <>)) files
+    withClient "http://example.com/r" Nothing $ \client ->
+      defaultMain (tests (zip files dataFiles) client)
+
+tests :: [(FilePath, ByteString)] -> GerritClient -> TestTree
+tests dataFiles client = testGroup "Tests" $ [unitTests] <> encodingTests dataFiles client
+
+unitTests :: TestTree
+unitTests =
+  testGroup
+    "unitTests"
+    [ testCase "queryString status" $
+        assertEqual "Query string is valid" "status:new" (queryText (Status NEW))
+    ]
+
+encodingTests :: [(FilePath, ByteString)] -> GerritClient -> [TestTree]
+encodingTests dataFiles client =
+  [ testGroup
+      "FromJSON"
+      [ testCase "Test GerritChange.json" $
+          assertBool "GerritChange is decoded" $
+            isChange $ decode $ getRaw "GerritChange.json",
+        testCase "Test EventPatchsetCreated.json" $
+          assertBool "EventPatchsetCreated is decoded" $
+            isEventPatchsetCreated $ decode $ getRaw "EventPatchsetCreated.json",
+        testCase "Test EventChangeMerged.json" $
+          assertBool "EventChangeMerged is decoded" $
+            isChangeMergedEvent $ decode $ getRaw "EventChangeMerged.json",
+        testCase "Test EventCommentAdded.json" $
+          assertBool "EventCommentAdded is decoded" $
+            isCommentEvent $ decode $ getRaw "EventCommentAdded.json",
+        testCase "Test EventChangeAbandoned.json" $
+          assertBool "EventChangeAbandoned is decoded" $
+            isAbandonedEvent $ decode $ getRaw "EventChangeAbandoned.json",
+        testCase "Test EventProjectCreated.json" $
+          assertBool "EventProjectCreated is decoded" $
+            isEventProjectCreated $ decode $ getRaw "EventProjectCreated.json",
+        testCase "Test EventRefUpdated.json" $
+          assertBool "EventRefUpdated is decoded" $
+            isEventRefUpdated $ decode $ getRaw "EventRefUpdated.json",
+        testCase "Test ReviewResult.json" $
+          assertBool "ReviewResult is decoded" $
+            isReviewResult $ decode $ getRaw "ReviewResult.json",
+        testCase "Test changeUrl works" $
+          assertEqual "Change url is correct" "http://example.com/r/18839" testGetUrl,
+        testCase "Test hasLabel works" $
+          assertBool "Label not found" testIsApproved
+      ],
+    testGroup
+      "toJSON"
+      [ testCase "Test ReviewInput.json" $
+          assertEqual "Review is encoded" (getRaw "ReviewInput.json") (prettyEncode reviewInput)
+      ]
+  ]
+  where
+    prettyEncode obj = encode obj <> "\n"
+    reviewInput = Gerrit.ReviewInput (Just "Thanks!") Nothing
+    getRaw :: FilePath -> ByteString
+    getRaw fp = fromJust $ lookup fp dataFiles
+    isReviewResult :: Maybe ReviewResult -> Bool
+    isReviewResult = \case
+      Just _ -> True
+      Nothing -> False
+    isChangeMergedEvent = \case
+      Just (Event.EventChangeMerged _) -> True
+      _ -> False
+    isEventPatchsetCreated = \case
+      Just (Event.EventPatchsetCreated _) -> True
+      _ -> False
+    isCommentEvent = \case
+      Just (Event.EventCommentAdded _) -> True
+      _ -> False
+    isAbandonedEvent = \case
+      Just (Event.EventChangeAbandoned _) -> True
+      _ -> False
+    isEventProjectCreated = \case
+      Just (Event.EventProjectCreated _) -> True
+      _ -> False
+    isEventRefUpdated = \case
+      Just (Event.EventRefUpdated _) -> True
+      _ -> False
+    isChange :: Maybe GerritChange -> Bool
+    isChange = \case
+      Just _ -> True
+      Nothing -> False
+    testIsApproved = case decode (getRaw "GerritChange.json") of
+      Just change -> hasLabel "Code-Review" 1 change
+      Nothing -> False
+    testGetUrl = case decode (getRaw "GerritChange.json") of
+      Just change -> changeUrl client change
+      Nothing -> "decode-failed"
diff --git a/test/data/EventChangeAbandoned.json b/test/data/EventChangeAbandoned.json
new file mode 100644
--- /dev/null
+++ b/test/data/EventChangeAbandoned.json
@@ -0,0 +1,49 @@
+{
+  "abandoner": {
+    "name": "Tristan de Cacqueray",
+    "email": "tdecacqu@redhat.com",
+    "username": "TristanCacqueray"
+  },
+  "reason": "Duplicated changeid",
+  "patchSet": {
+    "number": 1,
+    "revision": "39ff07f7277ad4e85a94df1e130ec3e0dc55e9c7",
+    "parents": ["b20838415c86314d73ff5bb5532b1a9a43d38043"],
+    "ref": "refs/changes/16/21916/1",
+    "uploader": {
+      "name": "Tristan de Cacqueray",
+      "email": "tdecacqu@redhat.com",
+      "username": "TristanCacqueray"
+    },
+    "createdOn": 1622284534,
+    "author": {
+      "name": "Tristan de Cacqueray",
+      "email": "tdecacqu@redhat.com",
+      "username": "TristanCacqueray"
+    },
+    "kind": "REWORK",
+    "sizeInsertions": 158,
+    "sizeDeletions": 23
+  },
+  "change": {
+    "project": "software-factory/gerritbot-matrix",
+    "branch": "master",
+    "id": "Ie795336f3b1814a7e3d65a33c391fe3f64472fac",
+    "number": 21916,
+    "subject": "Group related event to emit a single message",
+    "owner": {
+      "name": "Tristan de Cacqueray",
+      "email": "tdecacqu@redhat.com",
+      "username": "TristanCacqueray"
+    },
+    "url": "https://softwarefactory-project.io/r/c/software-factory/gerritbot-matrix/+/21916",
+    "commitMessage": "Group related event to emit a single message\n\nFixes https://github.com/softwarefactory-project/gerritbot-matrix/issues/1\nChange-Id: If4b38b79a421b9e257bdf82077ab4c9801a3a447\n\nChange-Id: Ie795336f3b1814a7e3d65a33c391fe3f64472fac\n",
+    "createdOn": 1622284534,
+    "status": "ABANDONED"
+  },
+  "project": "software-factory/gerritbot-matrix",
+  "refName": "refs/heads/master",
+  "changeKey": { "id": "Ie795336f3b1814a7e3d65a33c391fe3f64472fac" },
+  "type": "change-abandoned",
+  "eventCreatedOn": 1622285180
+}
diff --git a/test/data/EventChangeMerged.json b/test/data/EventChangeMerged.json
new file mode 100644
--- /dev/null
+++ b/test/data/EventChangeMerged.json
@@ -0,0 +1,47 @@
+{
+  "submitter": { "name": "Zuul", "username": "zuul" },
+  "newRev": "d7d5dc4a739aafcb0a3edfb758e15f878ad3292f",
+  "patchSet": {
+    "number": 2,
+    "revision": "4cc63b82c3c0114d29b50e2593af21504aa10f57",
+    "parents": ["e61b385d95d4a8969bc09ab9163b550da8f715a3"],
+    "ref": "refs/changes/05/21905/2",
+    "uploader": {
+      "name": "Tristan de Cacqueray",
+      "email": "tdecacqu@redhat.com",
+      "username": "TristanCacqueray"
+    },
+    "createdOn": 1622229383,
+    "author": {
+      "name": "Tristan de Cacqueray",
+      "email": "tdecacqu@redhat.com",
+      "username": "TristanCacqueray"
+    },
+    "kind": "REWORK",
+    "sizeInsertions": 201,
+    "sizeDeletions": 1
+  },
+  "change": {
+    "project": "software-factory/gerrit-haskell",
+    "branch": "master",
+    "id": "I7c926701b9e2b57b934917672bb1397851119ae0",
+    "number": 21905,
+    "subject": "Add Gerrit.Event data type",
+    "owner": {
+      "name": "Tristan de Cacqueray",
+      "email": "tdecacqu@redhat.com",
+      "username": "TristanCacqueray"
+    },
+    "url": "https://softwarefactory-project.io/r/c/software-factory/gerrit-haskell/+/21905",
+    "commitMessage": "Add Gerrit.Event data type\n\nChange-Id: I7c926701b9e2b57b934917672bb1397851119ae0\n",
+    "createdOn": 1622228651,
+    "status": "MERGED"
+  },
+  "project": "software-factory/gerrit-haskell",
+  "refName": "refs/heads/master",
+  "changeKey": {
+    "id": "I7c926701b9e2b57b934917672bb1397851119ae0"
+  },
+  "type": "change-merged",
+  "eventCreatedOn": 1622233316
+}
diff --git a/test/data/EventCommentAdded.json b/test/data/EventCommentAdded.json
new file mode 100644
--- /dev/null
+++ b/test/data/EventCommentAdded.json
@@ -0,0 +1,70 @@
+{
+  "author": {
+    "name": "Zuul CI",
+    "email": "zuul@softwarefactory-project.io",
+    "username": "zuul"
+  },
+  "approvals": [
+    {
+      "type": "Verified",
+      "description": "Verified",
+      "value": "0"
+    },
+    {
+      "type": "Code-Review",
+      "description": "Code-Review",
+      "value": "0"
+    },
+    {
+      "type": "Workflow",
+      "description": "Workflow",
+      "value": "0"
+    }
+  ],
+  "comment": "Patch Set 3:\n\nStarting check jobs.",
+  "patchSet": {
+    "number": 3,
+    "revision": "938356461cd9fcb470e195fd1ef1907c06db730a",
+    "parents": [
+      "e61b385d95d4a8969bc09ab9163b550da8f715a3"
+    ],
+    "ref": "refs/changes/05/21905/3",
+    "uploader": {
+      "name": "Tristan de Cacqueray",
+      "email": "tdecacqu@redhat.com",
+      "username": "TristanCacqueray"
+    },
+    "createdOn": 1622229429,
+    "author": {
+      "name": "Tristan de Cacqueray",
+      "email": "tdecacqu@redhat.com",
+      "username": "TristanCacqueray"
+    },
+    "kind": "REWORK",
+    "sizeInsertions": 203,
+    "sizeDeletions": 1
+  },
+  "change": {
+    "project": "software-factory/gerrit-haskell",
+    "branch": "master",
+    "id": "I7c926701b9e2b57b934917672bb1397851119ae0",
+    "number": 21905,
+    "subject": "Add Gerrit.Event data type",
+    "owner": {
+      "name": "Tristan de Cacqueray",
+      "email": "tdecacqu@redhat.com",
+      "username": "TristanCacqueray"
+    },
+    "url": "https://softwarefactory-project.io/r/c/software-factory/gerrit-haskell/+/21905",
+    "commitMessage": "Add Gerrit.Event data type\n\nChange-Id: I7c926701b9e2b57b934917672bb1397851119ae0\n",
+    "createdOn": 1622228651,
+    "status": "NEW"
+  },
+  "project": "software-factory/gerrit-haskell",
+  "refName": "refs/heads/master",
+  "changeKey": {
+    "id": "I7c926701b9e2b57b934917672bb1397851119ae0"
+  },
+  "type": "comment-added",
+  "eventCreatedOn": 1622229444
+}
diff --git a/test/data/EventPatchsetCreated.json b/test/data/EventPatchsetCreated.json
new file mode 100644
--- /dev/null
+++ b/test/data/EventPatchsetCreated.json
@@ -0,0 +1,50 @@
+{
+  "uploader": {
+    "name": "Tristan de Cacqueray",
+    "email": "tdecacqu@redhat.com",
+    "username": "TristanCacqueray"
+  },
+  "patchSet": {
+    "number": 2,
+    "revision": "4cc63b82c3c0114d29b50e2593af21504aa10f57",
+    "parents": ["e61b385d95d4a8969bc09ab9163b550da8f715a3"],
+    "ref": "refs/changes/05/21905/2",
+    "uploader": {
+      "name": "Tristan de Cacqueray",
+      "email": "tdecacqu@redhat.com",
+      "username": "TristanCacqueray"
+    },
+    "createdOn": 1622229383,
+    "author": {
+      "name": "Tristan de Cacqueray",
+      "email": "tdecacqu@redhat.com",
+      "username": "TristanCacqueray"
+    },
+    "kind": "REWORK",
+    "sizeInsertions": 201,
+    "sizeDeletions": 1
+  },
+  "change": {
+    "project": "software-factory/gerrit-haskell",
+    "branch": "master",
+    "id": "I7c926701b9e2b57b934917672bb1397851119ae0",
+    "number": 21905,
+    "subject": "Add Gerrit.Event data type",
+    "owner": {
+      "name": "Tristan de Cacqueray",
+      "email": "tdecacqu@redhat.com",
+      "username": "TristanCacqueray"
+    },
+    "url": "https://softwarefactory-project.io/r/c/software-factory/gerrit-haskell/+/21905",
+    "commitMessage": "Add Gerrit.Event data type\n\nChange-Id: I7c926701b9e2b57b934917672bb1397851119ae0\n",
+    "createdOn": 1622228651,
+    "status": "NEW"
+  },
+  "project": "software-factory/gerrit-haskell",
+  "refName": "refs/heads/master",
+  "changeKey": {
+    "id": "I7c926701b9e2b57b934917672bb1397851119ae0"
+  },
+  "type": "patchset-created",
+  "eventCreatedOn": 1622229383
+}
diff --git a/test/data/EventProjectCreated.json b/test/data/EventProjectCreated.json
new file mode 100644
--- /dev/null
+++ b/test/data/EventProjectCreated.json
@@ -0,0 +1,6 @@
+{
+  "projectName": "software-factory/gerritbot-matrix",
+  "headName": "refs/heads/master",
+  "type": "project-created",
+  "eventCreatedOn": 1622230530
+}
diff --git a/test/data/EventRefUpdated.json b/test/data/EventRefUpdated.json
new file mode 100644
--- /dev/null
+++ b/test/data/EventRefUpdated.json
@@ -0,0 +1,15 @@
+{
+  "submitter": {
+    "name": "Tristan de Cacqueray",
+    "email": "tdecacqu@redhat.com",
+    "username": "TristanCacqueray"
+  },
+  "refUpdate": {
+    "oldRev": "0000000000000000000000000000000000000000",
+    "newRev": "aaf4e7f7437e010ba19ed259be6391a9a658dbaa",
+    "refName": "refs/changes/05/21905/1",
+    "project": "software-factory/gerrit-haskell"
+  },
+  "type": "ref-updated",
+  "eventCreatedOn": 1622228651
+}
diff --git a/test/data/GerritChange.json b/test/data/GerritChange.json
new file mode 100644
--- /dev/null
+++ b/test/data/GerritChange.json
@@ -0,0 +1,84 @@
+{
+  "id": "software-factory%2Fgerrit-haskell~master~Ic6add806c49bccaf6e15d4ef1c62522d56eb9384",
+  "project": "software-factory/gerrit-haskell",
+  "branch": "master",
+  "hashtags": [],
+  "change_id": "Ic6add806c49bccaf6e15d4ef1c62522d56eb9384",
+  "subject": "Initial summon",
+  "status": "NEW",
+  "created": "2020-07-15 19:39:55.000000000",
+  "updated": "2020-07-15 23:01:14.000000000",
+  "submit_type": "MERGE_IF_NECESSARY",
+  "mergeable": true,
+  "insertions": 295,
+  "deletions": 0,
+  "unresolved_comment_count": 0,
+  "_number": 18839,
+  "owner": {
+    "_account_id": 7
+  },
+  "labels": {
+    "Code-Review": {
+      "all": [
+        {
+          "value": 1,
+          "permitted_voting_range": {
+            "min": -1,
+            "max": 1
+          },
+          "_account_id": 458
+        }
+      ],
+      "values": {
+        "-2": "Do not submit",
+        "-1": "I would prefer that you didn't submit this",
+        " 0": "No score",
+        "+1": "Looks good to me, but someone else must approve",
+        "+2": "Looks good to me (core reviewer)"
+      },
+      "default_value": 0
+    },
+    "Verified": {
+      "all": [
+        {
+          "value": 1,
+          "date": "2020-07-17 13:02:15.000000000",
+          "permitted_voting_range": {
+            "min": -2,
+            "max": 2
+          },
+          "_account_id": 458
+        },
+        {
+          "_account_id": 42
+        }
+      ],
+      "values": {
+        "-2": "Fails",
+        "-1": "Doesn't seem to work",
+        " 0": "No score",
+        "+1": "Works for me",
+        "+2": "Verified"
+      },
+      "default_value": 0
+    }
+  },
+  "current_revision": "155658085799464e8d198edb7c8bd62de9ec9ae6",
+  "revisions": {
+    "155658085799464e8d198edb7c8bd62de9ec9ae6": {
+      "kind": "REWORK",
+      "_number": 3,
+      "created": "2020-07-15 23:01:02.000000000",
+      "uploader": {
+        "_account_id": 7
+      },
+      "ref": "refs/changes/39/18839/3",
+      "fetch": {
+        "anonymous http": {
+          "url": "https://softwarefactory-project.io/r/software-factory/gerrit-haskell",
+          "ref": "refs/changes/39/18839/3"
+        }
+      }
+    }
+  }
+}
diff --git a/test/data/ReviewInput.json b/test/data/ReviewInput.json
new file mode 100644
--- /dev/null
+++ b/test/data/ReviewInput.json
@@ -0,0 +1,1 @@
+{"message":"Thanks!"}
diff --git a/test/data/ReviewResult.json b/test/data/ReviewResult.json
new file mode 100644
--- /dev/null
+++ b/test/data/ReviewResult.json
@@ -0,0 +1,5 @@
+{
+  "labels": {
+    "Code-Review": -1
+  }
+}
