Add code coverage and more unit tests
diff --git a/.gitignore b/.gitignore
index ddfc902..5b58e6a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
 api-gateway-config/target/*
 api-gateway-config/conf.d/includes/resolvers.conf
 *.iml
+.DS_STORE
 
 # Webstorm files
 .idea
diff --git a/Dockerfile-test b/Dockerfile-test
index 44d3548..f56bb35 100644
--- a/Dockerfile-test
+++ b/Dockerfile-test
@@ -18,13 +18,14 @@
     && cd /etc/api-gateway/tests \
     && curl -R -O https://raw.githubusercontent.com/bsm/fakengx/master/fakengx.lua
 
-RUN echo "... Installing lua rocks ..." \
+RUN echo "... Installing lua modules ..." \
     && luarocks install busted \
     && luarocks install luabitop \
     && luarocks install luasocket \
     && luarocks install sha1 \
     && luarocks install md5 \
-    && luarocks install fakeredis
+    && luarocks install fakeredis \
+    && luarocks install luacov
 
 COPY init-test.sh /etc/init-container-test.sh
 ONBUILD COPY init-test.sh /etc/init-container-test.sh
diff --git a/api-gateway-config/scripts/lua/lib/logger.lua b/api-gateway-config/scripts/lua/lib/logger.lua
index cf089cf..c1e5680 100644
--- a/api-gateway-config/scripts/lua/lib/logger.lua
+++ b/api-gateway-config/scripts/lua/lib/logger.lua
@@ -30,15 +30,9 @@
   ngx.log(ngx.ERR, s)
 end
 
---- Handle debug stream
+--- Handle debug stream to stdout
 -- @param s String to write to debug stream
 function _M.debug(s)
-  print(s)
-end
-
---- Output to stdout
--- @param s String to write to stdout
-function _M.info(s)
   os.execute("echo \"" .. s .. "\"")
 end
 
diff --git a/api-gateway-config/scripts/lua/lib/redis.lua b/api-gateway-config/scripts/lua/lib/redis.lua
index 40a0093..f8144fc 100644
--- a/api-gateway-config/scripts/lua/lib/redis.lua
+++ b/api-gateway-config/scripts/lua/lib/redis.lua
@@ -50,7 +50,7 @@
     if retryCount == 1 then
       msg = utils.concatStrings({msg:sub(1, -3), "."})
     end
-    logger.info(msg)
+    logger.debug(msg)
     retryCount = retryCount - 1
     os.execute("sleep 1")
     connect, err = red:connect(host, port)
@@ -164,6 +164,8 @@
   local ok, err = red:del(key)
   if not ok then
     request.err(500, utils.concatStrings({"Failed deleting resource: ", err}))
+  else
+    return ok
   end
 end
 
@@ -172,7 +174,7 @@
 -- @param key redis subscription key to create
 function _M.createSubscription(red, key)
   -- Add/update a subscription key to redis
-  local ok, err = red:set(key, true)
+  local ok, err = red:set(key, '')
   if not ok then
     request.err(500, utils.concatStrings({"Failed adding subscription to redis", err}))
   end
@@ -219,7 +221,7 @@
 --- Sync with redis on startup and create conf files for resources that are already in redis
 -- @param red redis client instance
 function syncWithRedis(red)
-  logger.info("\nCreating nginx conf files for existing resources...")
+  logger.debug("\nCreating nginx conf files for existing resources...")
   local redisKeys, err = red:keys("*")
   if not redisKeys then
     request.err(500, util.concatStrings({"Failed to sync with Redis: ", err}))
@@ -246,14 +248,14 @@
           -- Create new conf file
           local resourceObj = _M.getResource(red, redisKey, REDIS_FIELD)
           local fileLocation = filemgmt.createResourceConf(BASE_CONF_DIR, tenant, ngx.escape_uri(gatewayPath), resourceObj)
-          logger.info(utils.concatStrings({"Updated file: ", fileLocation}))
+          logger.debug(utils.concatStrings({"Updated file: ", fileLocation}))
         end
         index = index + 1
       end
     end
   end
   if resourcesExist == false then
-    logger.info("No existing resources.")
+    logger.debug("No existing resources.")
   end
 end
 
@@ -289,12 +291,12 @@
       local resourceObj = _M.getResource(redisGetClient, redisKey, REDIS_FIELD)
       if resourceObj == nil then
         local fileLocation = filemgmt.deleteResourceConf(BASE_CONF_DIR, tenant, ngx.escape_uri(gatewayPath))
-        logger.info(utils.concatStrings({"Redis key deleted: ", redisKey}))
-        logger.info(utils.concatStrings({"Deleted file: ", fileLocation}))
+        logger.debug(utils.concatStrings({"Redis key deleted: ", redisKey}))
+        logger.debug(utils.concatStrings({"Deleted file: ", fileLocation}))
       else
         local fileLocation = filemgmt.createResourceConf(BASE_CONF_DIR, tenant, ngx.escape_uri(gatewayPath), resourceObj)
-        logger.info(utils.concatStrings({"Redis key updated: ", redisKey}))
-        logger.info(utils.concatStrings({"Updated file: ", fileLocation}))
+        logger.debug(utils.concatStrings({"Redis key updated: ", redisKey}))
+        logger.debug(utils.concatStrings({"Updated file: ", fileLocation}))
       end
     end
   end
diff --git a/api-gateway-config/scripts/lua/management.lua b/api-gateway-config/scripts/lua/management.lua
index ada9f4a..d582b2b 100644
--- a/api-gateway-config/scripts/lua/management.lua
+++ b/api-gateway-config/scripts/lua/management.lua
@@ -155,7 +155,7 @@
   -- Initialize and connect to redis
   local redisGetClient = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 1000)
   local redisSubClient = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 60000) -- read_reply will timeout every minute
-  logger.info(utils.concatStrings({"\nConnected to redis at ", REDIS_HOST, ":", REDIS_PORT}))
+  logger.debug(utils.concatStrings({"\nConnected to redis at ", REDIS_HOST, ":", REDIS_PORT}))
   redis.subscribe(redisSubClient, redisGetClient)
   ngx.exit(200)
 end
diff --git a/api-gateway-config/tests/fakengx.lua b/api-gateway-config/tests/fakengx.lua
new file mode 100644
index 0000000..add7f71
--- /dev/null
+++ b/api-gateway-config/tests/fakengx.lua
@@ -0,0 +1,526 @@
+-- Copyright (c) 2012 Dimitrij Denissenko
+--
+--  Permission is hereby granted, free of charge, to any person obtaining a copy of
+--  this software and associated documentation files (the "Software"), to deal in
+--  the Software without restriction, including without limitation the rights to
+--  use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+--  of the Software, and to permit persons to whom the Software is furnished to do
+--  so, subject to the following conditions:
+--
+--  The above copyright notice and this permission notice shall be included in all
+--  copies or substantial portions of the Software.
+--
+--  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+--  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+--  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+--  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+--  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+--  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+--  SOFTWARE.
+
+local bit    = require 'bit'
+local socket = require 'socket'
+local sha1   = require 'sha1'
+local md5    = require 'md5'
+local mime   = require 'mime'
+local CRC32  = { 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D }
+
+-- Helpers
+local encode_param = function(str)
+  return tostring(str):gsub("\n", "\r\n"):gsub("([^%w_])", function (c)
+    return string.format("%%%02X", string.byte(c))
+  end)
+end
+
+local encode_params = function(tab)
+  local list = {}
+  for k, v in pairs(tab) do
+    table.insert(list, encode_param(k) .. "=" .. encode_param(v))
+  end
+  return table.concat(list, "&")
+end
+
+local function reverse_merge(src, defs)
+  local opts = {}
+  for k,v in pairs(src) do opts[k] = v end
+  for k,v in pairs(defs) do
+    if src[k] then v = src[k] end
+    opts[k] = v
+  end
+  return opts
+end
+
+local function stub_options(opts, method)
+  opts = opts or {}
+  if method and opts["method"] == nil then opts["method"] = method end
+  if type(opts.args) == "table" then opts["args"] = encode_params(opts.args) end
+  return opts
+end
+
+local function stub_response(res)
+  return reverse_merge(res or {},  { status = 200, headers = {}, body = "" })
+end
+
+local control_chars = {
+  ["\a"] = "\\a",  ["\b"] = "\\b", ["\f"] = "\\f",  ["\n"] = "\\n",
+  ["\r"] = "\\r",  ["\t"] = "\\t", ["\v"] = "\\v",  ["\\"] = "\\\\"
+}
+
+local function replace_control_char(c)
+  return control_chars[c]
+end
+
+local function stub_format(uri, opts)
+  local pad = 0
+  for k,_ in pairs(opts) do
+    if k ~= "method" and #k > pad then pad = #k end
+  end
+
+  local msg = "  " .. (opts.method or "GET") .. " " .. uri .. "\n"
+  for k,v in pairs(opts) do
+    if k ~= "method" then
+      k   = k .. ":" .. string.rep(" ", pad + 1 - #k)
+      msg = msg .. "  " .. k .. v:gsub("(%c)", replace_control_char) .. "\n"
+    end
+  end
+  return msg
+end
+
+-- Capture Registry
+local Captures = {}
+
+function Captures:new()
+  local this = { stubs = {} }
+  setmetatable(this, { __index = self })
+  return this
+end
+
+function Captures:length()
+  return #self.stubs
+end
+
+function Captures:each(fun)
+  for i=self:length(),1,-1 do
+    local stub = self.stubs[i]
+    fun(stub)
+  end
+end
+
+function Captures:find(uri, opts)
+  opts = stub_options(opts, "GET")
+
+  for i=self:length(),1,-1 do
+    local stub = self.stubs[i]
+    if uri == stub.uri then
+      local is_match = true
+
+      for k,v in pairs(stub.opts) do
+        if type(v) == 'function' then
+          is_match = v(opts[k])
+        elseif type(v) == 'string' and v:sub(1, 2) == "~>" then
+          is_match = tostring(opts[k]):match(v:sub(3)) and true
+        elseif opts[k] ~= v then
+          is_match = false
+        end
+        if not is_match then break end
+      end
+      if is_match then return stub end
+    end
+  end
+
+  return nil
+end
+
+function Captures:stub(uri, opts, res)
+  local stub = { uri = uri, opts = stub_options(opts), res = stub_response(res), calls = {} }
+  table.insert(self.stubs, stub)
+  return stub
+end
+
+-- TCP Proxy
+local TCP = {}
+
+function TCP:new()
+  return setmetatable({ host = nil, port = 0, timeout = 0, keepalive = {-1, 0}, data = {} }, { __index = self })
+end
+
+function TCP:connect(host, port)
+  self.host = host
+  self.port = port
+  return true, nil
+end
+
+function TCP:settimeout(value)
+  self.timeout = value
+end
+
+function TCP:setkeepalive(...)
+  self.keepalive = {...}
+end
+
+function TCP:send(msg)
+  table.insert(self.data, msg)
+end
+
+-- UDP Proxy
+local UDP = {}
+
+function UDP:new()
+  return setmetatable({ host = nil, port = 0, timeout = 0, data = {}, closed = false }, { __index = self })
+end
+
+function UDP:setpeername(host, port)
+  self.host = host
+  self.port = port
+  return true, nil
+end
+
+function UDP:settimeout(value)
+  self.timeout = value
+end
+
+function UDP:send(msg)
+  table.insert(self.data, msg)
+  return true, nil
+end
+
+function UDP:close()
+  self.closed = true
+  return true, nil
+end
+
+-- DICT Proxy
+local SharedDict = {}
+
+function SharedDict:new()
+  return setmetatable({ data = {} }, { __index = self })
+end
+
+function SharedDict:get(key)
+  return self.data[key], 0
+end
+
+function SharedDict:set(key, value)
+  self.data[key] = value
+  return true, nil, false
+end
+
+function SharedDict:add(key, value)
+  if self.data[key] ~= nil then
+    return false, "exists", false
+  end
+
+  self.data[key] = value
+  return true, nil, false
+end
+
+function SharedDict:replace(key, value)
+  if self.data[key] == nil then
+    return false, "not found", false
+  end
+
+  self.data[key] = value
+  return true, nil, false
+end
+
+function SharedDict:delete(key)
+  self.data[key] = nil
+end
+
+function SharedDict:incr(key, value)
+  if not self.data[key] then
+    return nil, "not found"
+  elseif type(self.data[key]) ~= "number" then
+    return nil, "not a number"
+  end
+
+  self.data[key] = self.data[key] + value
+  return self.data[key], nil
+end
+
+-- NGX Prototype
+local protoype = {
+
+  -- Log constants
+  STDERR = 0,
+  EMERG  = 1,
+  ALERT  = 2,
+  CRIT   = 3,
+  ERR    = 4,
+  WARN   = 5,
+  NOTICE = 6,
+  INFO   = 7,
+  DEBUG  = 8,
+
+  -- HTTP Method Constants
+  HTTP_GET    = "GET",
+  HTTP_HEAD   = "HEAD",
+  HTTP_POST   = "POST",
+  HTTP_PUT    = "PUT",
+  HTTP_DELETE = "DELETE",
+
+  -- HTTP Status Constants
+  HTTP_OK                        = 200,
+  HTTP_CREATED                   = 201,
+  HTTP_ACCEPTED                  = 202,
+  HTTP_NO_CONTENT                = 204,
+  HTTP_PARTIAL_CONTENT           = 206,
+  HTTP_SPECIAL_RESPONSE          = 300,
+  HTTP_MOVED_PERMANENTLY         = 301,
+  HTTP_MOVED_TEMPORARILY         = 302,
+  HTTP_SEE_OTHER                 = 303,
+  HTTP_NOT_MODIFIED              = 304,
+  HTTP_BAD_REQUEST               = 400,
+  HTTP_UNAUTHORIZED              = 401,
+  HTTP_FORBIDDEN                 = 403,
+  HTTP_NOT_FOUND                 = 404,
+  HTTP_NOT_ALLOWED               = 405,
+  HTTP_REQUEST_TIME_OUT          = 408,
+  HTTP_CONFLICT                  = 409,
+  HTTP_LENGTH_REQUIRED           = 411,
+  HTTP_PRECONDITION_FAILED       = 412,
+  HTTP_REQUEST_ENTITY_TOO_LARGE  = 413,
+  HTTP_REQUEST_URI_TOO_LARGE     = 414,
+  HTTP_UNSUPPORTED_MEDIA_TYPE    = 415,
+  HTTP_RANGE_NOT_SATISFIABLE     = 416,
+  HTTP_CLOSE                     = 444,
+  HTTP_NGINX_CODES               = 494,
+  HTTP_REQUEST_HEADER_TOO_LARGE  = 494,
+  HTTP_INTERNAL_SERVER_ERROR     = 500,
+  HTTP_NOT_IMPLEMENTED           = 501,
+  HTTP_BAD_GATEWAY               = 502,
+  HTTP_SERVICE_UNAVAILABLE       = 503,
+  HTTP_GATEWAY_TIME_OUT          = 504,
+  HTTP_INSUFFICIENT_STORAGE      = 507,
+
+}
+
+-- NGX Builder
+local fakengx = {}
+
+-- Constructor
+function fakengx.new()
+  local ngx = {}
+  for k, v in pairs(protoype) do
+    ngx[k] = v
+  end
+  setmetatable(ngx, getmetatable(protoype))
+
+  -- Create namespaces
+  ngx.req       = {}
+  ngx.re        = {}
+  ngx.socket    = {}
+  ngx.thread    = {}
+  ngx.location  = {}
+  ngx.shared    = {}
+
+  -- Create shared dict API
+  setmetatable(ngx.shared, {
+    __index = function(t, k)
+      t[k] = SharedDict:new()
+      return t[k]
+    end
+  })
+
+  function ngx._reset()
+    ngx.status    = 200
+    ngx.var       = {}
+    ngx.ctx       = {}
+    ngx.header    = {}
+    ngx.arg       = {}
+
+    -- Internal Registries
+    ngx._captures = Captures:new()
+    ngx._sockets  = {}
+    ngx._body     = ""
+    ngx._log      = ""
+    ngx._exit     = nil
+
+    for k,_ in pairs(ngx.shared) do
+      ngx.shared[k] = nil
+    end
+  end
+
+  -- Reset once
+  ngx._reset()
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.print
+  function ngx.print(s)
+    ngx._body = ngx._body .. s
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.say
+  function ngx.say(s)
+    ngx.print(s .. "\n")
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.log
+  function ngx.log(level, ...)
+    local args = {...}
+    for i=1,#args do args[i] = tostring(args[i]) or "nil" end
+    ngx._log = ngx._log .. "LOG(" .. tostring(level) .. "): " .. table.concat(args) .. "\n"
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.time
+  function ngx.time()
+    if not ngx._time then
+      ngx._time = os.time()
+    end
+    return ngx._time
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.update_time
+  function ngx.update_time()
+    ngx._time = nil
+    ngx._now = nil
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.now
+  function ngx.now()
+    if not ngx._now then
+      ngx._now = socket.gettime()
+    end
+    return ngx._now
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.cookie_time
+  function ngx.cookie_time(t)
+    return os.date('!%a, %d-%b-%Y %H:%M:%S GMT', t)
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.exit
+  function ngx.exit(status)
+    if status > ngx.status then ngx.status = status end
+    ngx._exit = status
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.crc32_short
+  function ngx.crc32_short(s)
+    local crc, l, i = 0xFFFFFFFF, string.len(s)
+    for i = 1, l, 1 do
+     crc = bit.bxor(bit.rshift(crc, 8), CRC32[bit.band(bit.bxor(crc, string.byte(s, i)), 0xFF) + 1])
+    end
+    return bit.bxor(crc, -1) % 2^32
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.hmac_sha1
+  function ngx.hmac_sha1(secret_key, str)
+    return sha1.hmac_sha1_binary(secret_key, str)
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.sha1_bin
+  function ngx.sha1_bin(str)
+    return sha1.sha1_binary(str)
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.md5
+  function ngx.md5(str)
+    return md5.sumhexa(str)
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.md5_bin
+  function ngx.md5_bin(str)
+    return md5.sum(str)
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.escape_uri
+  function ngx.escape_uri(str)
+    return tostring(str):gsub("\n", "\r\n"):gsub("([^%w_ ])", function (c)
+      return string.format("%%%02X", string.byte(c))
+    end):gsub(" ", "+")
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.unescape_uri
+  function ngx.unescape_uri(str)
+    return tostring(str):gsub("+", " "):gsub("\r\n", "\n"):gsub("%%(%x%x)", function(h)
+      return string.char(tonumber(h,16))
+    end)
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.encode_args
+  function ngx.encode_args(tab)
+    return encode_params(tab)
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.location.capture
+  function ngx.location.capture(uri, opts)
+    local stub = ngx._captures:find(uri, opts)
+    if not stub then
+      local msg = "\n\nUnstubbed request:\n\n" .. stub_format(uri, opts or {}) .. "\nStubbed were:\n"
+      ngx._captures:each(function(stub)
+        msg = msg .. "\n" .. stub_format(stub.uri, stub.opts or {})
+      end)
+      error(msg)
+    end
+
+    table.insert(stub.calls, { uri = uri, opts = opts })
+    return stub.res
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.location.capture_multi
+  function ngx.location.capture_multi(...)
+    local requests  = ...
+    local responses = {}
+    for i, request in ipairs(requests) do
+      table.insert(responses, ngx.location.capture(request[1], request[2]))
+    end
+    return unpack(responses)
+  end
+
+  -- Stub a capture
+  function ngx.location.stub(...)
+    return ngx._captures:stub(...)
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.req.read_body
+  function ngx.req.read_body()
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.socket.tcp
+  function ngx.socket.tcp()
+    local sock = TCP:new()
+    table.insert(ngx._sockets, sock)
+    return sock
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.socket.udp
+  function ngx.socket.udp()
+    local sock = UDP:new()
+    table.insert(ngx._sockets, sock)
+    return sock
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.encode_base64
+  function ngx.encode_base64(s)
+    return mime.b64(s)
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.decode_base64
+  function ngx.decode_base64(s)
+    return mime.unb64(s)
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.thread.spawn
+  function ngx.thread.spawn(fun, ...)
+    return { fun = fun, args = {...} }
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.thread.wait
+  function ngx.thread.wait(thread)
+    return true, thread.fun(unpack(thread.args))
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.re.gmatch
+  function ngx.re.gmatch(s, pattern)
+    return string.gmatch(s, pattern)
+  end
+
+  -- http://wiki.nginx.org/HttpLuaModule#ngx.re.match
+  function ngx.re.match(s, pattern)
+    return string.match(s, pattern)
+  end
+
+  return ngx
+end
+
+return fakengx
diff --git a/api-gateway-config/tests/test.lua b/api-gateway-config/tests/test.lua
index af2bf6a..3c9e789 100644
--- a/api-gateway-config/tests/test.lua
+++ b/api-gateway-config/tests/test.lua
@@ -21,49 +21,28 @@
 -- Unit tests for the apigateway using the busted framework.
 -- @author Alex Song (songs)
 
-package.path = package.path .. ';/usr/local/share/lua/5.2/?.lua' ..
-    ';/usr/local/api-gateway/lualib/?.lua;/etc/api-gateway/scripts/lua/?.lua'
-package.cpath = package.cpath .. ';/usr/local/lib/lua/5.2/?.so;/usr/local/api-gateway/lualib/?.so'
-
+local handle = io.popen('pwd')
+local pwd = handle:read('*a'):sub(1, -2)  -- get current working directory
+handle:close()
+package.path = package.path .. ';' .. pwd .. '/../scripts/lua/?.lua'
 local fakengx = require 'fakengx'
 local fakeredis = require 'fakeredis'
 local cjson = require 'cjson'
-
-local redis = require 'lib/redis'
 local request = require 'lib/request'
+local utils = require 'lib/utils'
+local logger = require 'lib/logger'
+local redis = require 'lib/redis'
 
-describe('Testing Redis module', function()
-  before_each(function()
-    _G.ngx = fakengx.new()
-    red = fakeredis.new()
-  end)
-  it('should generate a resource obj to store in redis', function()
-    local key = 'resources:guest:hello'
-    local gatewayMethod = 'GET'
-    local backendUrl = 'https://httpbin.org:8000/get'
-    local backendMethod = gatewayMethod
-    local apiId = 12345
-    local policies
-    local security
-    local expected = {
-      operations = {
-        [gatewayMethod] = {
-          backendUrl = backendUrl,
-          backendMethod = backendMethod
-        }
-      },
-      apiId = apiId
-    }
-    expected = cjson.encode(expected)
-    local generated = redis.generateResourceObj(red, key, gatewayMethod, backendUrl, backendMethod, apiId, policies, security)
-    assert.are.same(expected, generated)
-  end)
-end)
+
+------------------------------------
+---- Unit tests for lib modules ----
+------------------------------------
 
 describe('Testing Request module', function()
   before_each(function()
     _G.ngx = fakengx.new()
   end)
+
   it('should return correct error response', function()
     local code = 500
     local msg = 'Internal server error\n'
@@ -71,6 +50,7 @@
     assert.are.equal(ngx._body, 'Error: ' .. msg)
     assert.are.equal(ngx._exit, code)
   end)
+
   it('should return correct success response', function()
     local code = 200
     local msg ='Success!\n'
@@ -79,3 +59,222 @@
     assert.are.equal(ngx._exit, code)
   end)
 end)
+
+
+describe('Testing utils module', function()
+  before_each(function()
+    _G.ngx = fakengx.new()
+  end)
+
+  it('should concatenate strings properly', function()
+    local expected = 'hello' .. 'gateway' .. 'world'
+    local generated = utils.concatStrings({'hello', 'gateway', 'world'})
+    assert.are.equal(expected, generated)
+  end)
+
+  it('should serialize lua table', function()
+    -- Empty table
+    local expected = {}
+    local serialized = utils.serializeTable(expected)
+    loadstring('generated = ' .. serialized)() -- convert serialzed string to lua table
+    assert.are.same(expected, generated)
+
+    -- Simple table
+    expected = {
+      test = true
+    }
+    serialized = utils.serializeTable(expected)
+    loadstring('generated = ' .. serialized)() -- convert serialzed string to lua table
+    assert.are.same(expected, generated)
+
+    -- Complex nested table
+    expected = {
+      test1 = {
+        nested = 'value'
+      },
+      test2 = true
+    }
+    serialized = utils.serializeTable(expected)
+    loadstring('generated = ' .. serialized)() -- convert serialzed string to lua table
+    assert.are.same(expected, generated)
+  end)
+
+  it('should convert templated path parameter', function()
+    -- TODO: Add test cases for convertTemplatedPathParam(m)
+  end)
+
+  it('should convert json body to lua table', function()
+    -- General case
+    local jsonString = '{ "apiId" : 12345, "policies" : [{"test":true}], "security" : {}}'
+    local expected = cjson.decode(jsonString)
+    local args = {}
+    args[jsonString] = true
+    local generated = utils.convertJSONBody(args)
+    assert.are.same(expected, generated)
+
+    -- "=" sign in original body, resulting in separated out key
+    -- original json string is '{ "apiId" : 12345, "policies" : [{"test":true}], "security" : {"apiKey":"a53kw2kfq302="}}'
+    local firstHalf = '{ "apiId" : 12345, "policies" : [{"test":true}], "security" : {"apiKey":"a53kw2kfq302'
+    local secondHalf = '"}}'
+    expected = {
+      apiId = 12345,
+      policies = {{test=true}},
+      security = {apiKey = "a53kw2kfq302="}
+    }
+    args = {}
+    args[firstHalf] = secondHalf
+    generated = utils.convertJSONBody(args)
+    assert.are.same(expected, generated)
+  end)
+end)
+
+
+describe('Testing logger module', function()
+  it('Should handle error stream', function()
+    local msg = 'Error!'
+    logger.err(msg)
+    local expected = 'LOG(4): ' .. msg .. '\n'
+    local generated = ngx._log
+    assert.are.equal(expected, generated)
+  end)
+end)
+
+
+describe('Testing Redis module', function()
+  before_each(function()
+    _G.ngx = fakengx.new()
+    red = fakeredis.new()
+  end)
+
+  it('should generate resource object to store in redis', function()
+    -- Resource object with no policies or security
+    local key = 'resources:guest:hello'
+    local gatewayMethod = 'GET'
+    local backendUrl = 'https://httpbin.org/get'
+    local backendMethod = gatewayMethod
+    local apiId = 12345
+    local policies
+    local security
+    local resourceObj = {
+      operations = {
+        [gatewayMethod] = {
+          backendUrl = backendUrl,
+          backendMethod = backendMethod
+        }
+      },
+      apiId = apiId
+    }
+    local expected = resourceObj
+    local generated = cjson.decode(redis.generateResourceObj(red, key, gatewayMethod, backendUrl, backendMethod, apiId, policies, security))
+    assert.are.same(expected, generated)
+
+    -- Resource object with policy added
+    local policyList = [[
+      [{
+          "type":"rateLimit",
+          "value":[{
+              "interval":60,
+              "rate":100,
+              "scope":"api",
+              "subscription": "true"
+          }]
+      }]
+    ]]
+    policies = cjson.decode(policyList)
+    resourceObj.operations[gatewayMethod].policies = policies
+    expected = resourceObj
+    generated = cjson.decode(redis.generateResourceObj(red, key, gatewayMethod, backendUrl, backendMethod, apiId, policies, security))
+    assert.are.same(expected, generated)
+
+    -- Resource object with security added
+    local securityObj = [[
+      {
+        "type":"apiKey",
+        "scope":"api",
+        "header":"myheader"
+      }
+    ]]
+    security = cjson.decode(securityObj)
+    resourceObj.operations[gatewayMethod].security = security
+    expected = resourceObj
+    generated = cjson.decode(redis.generateResourceObj(red, key, gatewayMethod, backendUrl, backendMethod, apiId, policies, security))
+    assert.are.same(expected, generated)
+
+    -- Update already existing resource object
+    local field = 'resources'
+    redis.createResource(red, key, field, cjson.encode(generated))
+    local newGatewayMethod = 'POST'
+    resourceObj.operations[newGatewayMethod] = {
+      backendUrl = backendUrl,
+      backendMethod = backendMethod
+    }
+    policies = nil
+    security = nil
+    expected = resourceObj
+    generated = cjson.decode(redis.generateResourceObj(red, key, newGatewayMethod, backendUrl, backendMethod, apiId, policies, security))
+    assert.are.same(expected, generated)
+  end)
+
+  it('should get a resource from redis', function()
+    local key = 'resources:guest:hello'
+    local field = 'resources'
+    -- resource doesn't exist in redis
+    local generated = redis.getResource(red, key, field)
+    assert.are.same(nil, generated)
+
+    -- resource exists in redis
+    local expected = redis.generateResourceObj(red, key, 'GET', 'https://httpbin.org/get', 'GET', '12345', nil, nil)
+    red:hset(key, field, expected)
+    generated = redis.getResource(red, key, field)
+    assert.are.same(expected, generated)
+  end)
+
+  it('should create a resource in redis', function()
+    local key = 'resources:guest:hello'
+    local field = 'resources'
+    local expected = redis.generateResourceObj(red, key, 'GET', 'https://httpbin.org/get', 'GET', '12345', nil, nil)
+    redis.createResource(red, key, field, expected)
+    local generated = redis.getResource(red, key, field)
+    assert.are.same(expected, generated)
+  end)
+
+  it('should delete a resource in redis', function()
+    -- Key doesn't exist - throw 404
+    local key = 'resources:guest:hello'
+    local field = 'resources'
+    redis.deleteResource(red, key, field)
+    assert.are.equal(ngx._exit, 404)
+    -- Key exists - deleted properly
+    local resourceObj = redis.generateResourceObj(red, key, 'GET', 'https://httpbin.org/get', 'GET', '12345', nil, nil)
+    redis.createResource(red, key, field, resourceObj)
+    local expected = 1
+    local generated = redis.deleteResource(red, key, field)
+    assert.are.same(expected, generated)
+  end)
+
+  it('shoud create an API Key subscription', function()
+    local key = 'subscriptions:test:apikey'
+    redis.createSubscription(red, key)
+    assert.are.same(true, red:exists(key))
+  end)
+
+  it('should delete an API Key subscription', function()
+    -- API key doesn't exist in redis - throw 404
+    local key = 'subscriptions:test:apikey'
+    redis.deleteSubscription(red, key)
+    assert.are.equal(404, ngx._exit)
+
+    -- API key to delete exists in redis
+    red:set(key, '')
+    redis.deleteSubscription(red, key)
+    assert.are.equal(false, red:exists(key))
+  end)
+end)
+
+--TODO: filemgmt
+
+---------------------------------------
+---- Unit tests for policy modules ----
+---------------------------------------
+
+--TODO: mapping, rateLimit, security
\ No newline at end of file
diff --git a/init-test.sh b/init-test.sh
deleted file mode 100644
index e83d454..0000000
--- a/init-test.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-cd /etc/api-gateway/tests
-busted --output=TAP test.lua
\ No newline at end of file
diff --git a/run-tests.sh b/run-tests.sh
new file mode 100755
index 0000000..189ef3a
--- /dev/null
+++ b/run-tests.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+# Run unit tests
+cd api-gateway-config/tests
+busted -c --output=TAP test.lua
+
+# Generate code coverage report
+luacov ../scripts/lua/
+cat luacov.report.out
+rm luacov.report.out && rm luacov.stats.out
\ No newline at end of file