local cjson   = require "cjson"
local helpers = require "spec.helpers"
local admin_api = require "spec.fixtures.admin_api"
local secret = require "kong.plugins.oauth2.secret"


for _, strategy in helpers.each_strategy() do
  describe("Plugin: oauth (API) [#" .. strategy .. "]", function()
    local admin_client
    local db

    lazy_setup(function()
      local _
      _, db = helpers.get_db_utils(strategy, {
        "routes",
        "services",
        "consumers",
        "plugins",
        "oauth2_tokens",
        "oauth2_authorization_codes",
        "oauth2_credentials",
      })

      helpers.prepare_prefix()

      assert(helpers.start_kong({
        database   = strategy,
        nginx_conf = "spec/fixtures/custom_nginx.template",
      }))

      admin_client = helpers.admin_client()
    end)

    lazy_teardown(function()
      if admin_client then admin_client:close() end
      assert(helpers.stop_kong())
      helpers.clean_prefix()
    end)

    describe("/consumers/:consumer/oauth2/", function()
      local consumer
      local service

      lazy_setup(function()
        service = admin_api.services:insert({ host = "oauth2_token.test" })
        consumer = admin_api.consumers:insert({ username = "bob" })
        admin_api.consumers:insert({ username = "sally" })
      end)

      lazy_teardown(function()
        admin_api.consumers:remove(consumer)
        admin_api.services:remove(service)
      end)

      describe("POST", function()
        it("creates a oauth2 credential", function()
          local res = assert(admin_client:send {
            method  = "POST",
            path    = "/consumers/bob/oauth2",
            body    = {
              name          = "Test APP",
              redirect_uris = { "http://google.test/" },
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(201, res))
          assert.equal(consumer.id, body.consumer.id)
          assert.equal("Test APP", body.name)
          assert.same({ "http://google.test/" }, body.redirect_uris)

          res = assert(admin_client:send {
            method = "POST",
            path   = "/consumers/bob/oauth2",
            body   = {
              name          = "Test APP",
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(201, res))
          assert.equal(consumer.id, body.consumer.id)
          assert.equal("Test APP", body.name)
          assert.same(ngx.null, body.redirect_uris)
        end)
        it("creates an oauth2 credential with tags", function()
          local res = assert(admin_client:send {
            method  = "POST",
            path    = "/consumers/bob/oauth2",
            body    = {
              name          = "Tags APP",
              redirect_uris = { "http://example.test/" },
              tags = { "tag1", "tag2" },
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(201, res)
          local json = cjson.decode(body)
          assert.equal(consumer.id, json.consumer.id)
          assert.equal("tag1", json.tags[1])
          assert.equal("tag2", json.tags[2])
        end)
        it("creates a oauth2 credential with multiple redirect_uris", function()
          local res = assert(admin_client:send {
            method  = "POST",
            path    = "/consumers/bob/oauth2",
            body    = {
              name          = "Test APP",
              redirect_uris = { "http://google.test/", "http://google.example/" },
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(201, res))
          assert.equal(consumer.id, body.consumer.id)
          assert.equal("Test APP", body.name)
          assert.same({ "http://google.test/", "http://google.example/" }, body.redirect_uris)
        end)
        it("creates multiple oauth2 credentials with the same client_secret", function()
          local res = assert(admin_client:send {
            method  = "POST",
            path    = "/consumers/bob/oauth2",
            body    = {
              name          = "Test APP",
              redirect_uris = { "http://google.test/" },
              client_secret = "secret123",
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          assert.res_status(201, res)
          res = assert(admin_client:send {
            method = "POST",
            path   = "/consumers/sally/oauth2",
            body   = {
              name          = "Test APP",
              redirect_uris = { "http://google.test/" },
              client_secret = "secret123",
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          assert.res_status(201, res)
        end)
        it("creates oauth2 credential with a hashed auto-generated client_secret", function()
          -- this is quite useless as nobody knows the client_secret
          local res = assert(admin_client:send {
            method  = "POST",
            path    = "/consumers/bob/oauth2",
            body    = {
              name          = "Test APP",
              redirect_uris = { "http://google.test/" },
              hash_secret = true,
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(201, res))
          assert.equal(true, body.hash_secret)
          assert.equal(false, secret.needs_rehash(body.client_secret))
        end)
        it("creates oauth2 credential with a hashed client_secret", function()
          local res = assert(admin_client:send {
            method  = "POST",
            path    = "/consumers/bob/oauth2",
            body    = {
              name          = "Test APP",
              redirect_uris = { "http://google.test/" },
              client_secret = "test",
              hash_secret   = true,
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(201, res))
          assert.equal(true, body.hash_secret)
          assert.equal(false, secret.needs_rehash(body.client_secret))
          assert.equal(true, secret.verify("test", body.client_secret))
          assert.equal(false, secret.verify("invalid", body.client_secret))
        end)
        it("creates oauth2 credential without hashing the auto-generated client_secret", function()
          local res = assert(admin_client:send {
            method  = "POST",
            path    = "/consumers/bob/oauth2",
            body    = {
              name          = "Test APP",
              redirect_uris = { "http://google.test/" },
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(201, res))
          assert.equal(false, body.hash_secret)
          assert.equal(true, secret.needs_rehash(body.client_secret))
        end)
        it("creates oauth2 credential without hashing the client_secret", function()
          local res = assert(admin_client:send {
            method  = "POST",
            path    = "/consumers/bob/oauth2",
            body    = {
              name          = "Test APP",
              redirect_uris = { "http://google.test/" },
              client_secret = "test",
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(201, res))
          assert.equal(false, body.hash_secret)
          assert.equal(true, secret.needs_rehash(body.client_secret))
          assert.equal(false, secret.verify("test", body.client_secret))
          assert.equal(false, secret.verify("invalid", body.client_secret))
        end)
        describe("errors", function()
          it("returns bad request", function()
            local res = assert(admin_client:send {
              method  = "POST",
              path    = "/consumers/bob/oauth2",
              body    = {},
              headers = {
                ["Content-Type"] = "application/json"
              }
            })
            local body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({ name = "required field missing" }, json.fields)
          end)
          it("returns bad request with invalid redirect_uris", function()
            local res = assert(admin_client:send {
              method  = "POST",
              path    = "/consumers/bob/oauth2",
              body    = {
                name             = "Test APP",
                redirect_uris    = { "not-valid" }
              },
              headers = {
                ["Content-Type"] = "application/json"
              }
            })
            local body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({ redirect_uris = { "cannot parse 'not-valid'" } }, json.fields)

            local res = assert(admin_client:send {
              method  = "POST",
              path    = "/consumers/bob/oauth2",
              body    = {
                name            = "Test APP",
                redirect_uris   = { "http://test.test/#with-fragment" },
              },
              headers = {
                ["Content-Type"] = "application/json"
              }
            })
            local body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({ redirect_uris = { "fragment not allowed in 'http://test.test/#with-fragment'" } }, json.fields)

            local res = assert(admin_client:send {
              method  = "POST",
              path    = "/consumers/bob/oauth2",
              body    = {
                name             = "Test APP",
                redirect_uris    = {"http://valid.test", "not-valid"}
              },
              headers = {
                ["Content-Type"] = "application/json"
              }
            })
            local body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({ redirect_uris = { ngx.null, "cannot parse 'not-valid'" } }, json.fields)

            local res = assert(admin_client:send {
              method  = "POST",
              path    = "/consumers/bob/oauth2",
              body    = {
                name             = "Test APP",
                redirect_uris    = {"http://valid.test", "http://test.test/#with-fragment"}
              },
              headers = {
                ["Content-Type"] = "application/json"
              }
            })
            local body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({ redirect_uris = {
                            ngx.null,
                            "fragment not allowed in 'http://test.test/#with-fragment'"
                        } }, json.fields)
          end)
        end)
      end)

      describe("PUT", function()
        it("creates an oauth2 credential", function()
          local res = assert(admin_client:send {
            method  = "PUT",
            path    = "/consumers/bob/oauth2/client_one",
            body = {
              name             = "Test APP",
              redirect_uris    = { "http://google.test/" },
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.equal(consumer.id, body.consumer.id)
          assert.equal("Test APP", body.name)
          assert.equal("client_one", body.client_id)
          assert.same({ "http://google.test/" }, body.redirect_uris)

          local res = assert(admin_client:send {
            method  = "PUT",
            path    = "/consumers/bob/oauth2/client_one",
            body = {
              name             = "Test APP",
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.equal(consumer.id, body.consumer.id)
          assert.equal("Test APP", body.name)
          assert.equal("client_one", body.client_id)
          assert.same(ngx.null, body.redirect_uris)
        end)
        describe("errors", function()
          it("returns bad request", function()
            local res = assert(admin_client:send {
              method  = "PUT",
              path    = "/consumers/bob/oauth2/client_two",
              body    = {},
              headers = {
                ["Content-Type"] = "application/json"
              }
            })
            local body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({ name = "required field missing" }, json.fields)
          end)
        end)
      end)

      describe("GET", function()
        local consumer = admin_api.consumers:insert({
          username = "get_test",
        })
        local credentials = {}
        lazy_setup(function()
          for i = 1, 3 do
            credentials[i] = admin_api.oauth2_credentials:insert {
              name          = "app" .. i,
              redirect_uris = { helpers.mock_upstream_ssl_url },
              consumer      = { id = consumer.id },
            }
          end
        end)
        lazy_teardown(function()
          for _, credential in ipairs(credentials) do
            admin_api.oauth2_credentials:remove(credential)
          end
        end)
        it("retrieves the first page", function()
          local res = assert(admin_client:send {
            method  = "GET",
            path    = "/consumers/get_test/oauth2"
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.is_table(json.data)
          assert.equal(3, #json.data)
        end)
      end)
    end)

    describe("/consumers/:consumer/oauth2/:id", function()
      local credential
      local consumer
      local service

      lazy_setup(function()
        service = admin_api.services:insert({ host = "oauth2_token.test" })
        consumer = admin_api.consumers:insert({ username = "bob" })
      end)

      lazy_teardown(function()
        admin_api.consumers:remove(consumer)
        admin_api.services:remove(service)
      end)

      before_each(function()
        credential = admin_api.oauth2_credentials:insert {
          name          = "test app",
          redirect_uris = { helpers.mock_upstream_ssl_url },
          consumer      = { id = consumer.id },
        }
      end)

      after_each(function()
        admin_api.oauth2_credentials:remove(credential)
      end)

      describe("GET", function()
        it("retrieves oauth2 credential by id", function()
          local res = assert(admin_client:send {
            method  = "GET",
            path    = "/consumers/bob/oauth2/" .. credential.id
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.equal(credential.id, json.id)
        end)
        it("retrieves oauth2 credential by client id", function()
          local res = assert(admin_client:send {
            method  = "GET",
            path    = "/consumers/bob/oauth2/" .. credential.client_id
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.equal(credential.id, json.id)
        end)
        it("retrieves credential by id only if the credential belongs to the specified consumer", function()
          local alice = admin_api.consumers:insert {
            username = "alice"
          }
          finally(function()
            admin_api.consumers:remove(alice)
          end)

          local res = assert(admin_client:send {
            method  = "GET",
            path    = "/consumers/bob/oauth2/" .. credential.id
          })
          assert.res_status(200, res)

          res = assert(admin_client:send {
            method = "GET",
            path   = "/consumers/alice/oauth2/" .. credential.id
          })
          assert.res_status(404, res)
        end)
        it("retrieves credential by clientid only if the credential belongs to the specified consumer", function()
          local res = assert(admin_client:send {
            method  = "GET",
            path    = "/consumers/bob/oauth2/" .. credential.client_id
          })
          assert.res_status(200, res)

          res = assert(admin_client:send {
            method = "GET",
            path   = "/consumers/alice/oauth2/" .. credential.client_id
          })
          assert.res_status(404, res)
        end)
      end)

      describe("PATCH", function()
        it("updates a credential by id", function()
          local previous_name = credential.name

          local res = assert(admin_client:send {
            method  = "PATCH",
            path    = "/consumers/bob/oauth2/" .. credential.id,
            body    = {
              name             = "4321"
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.not_equal(previous_name, json.name)
        end)
        it("updates a credential by client id", function()
          local previous_name = credential.name

          local res = assert(admin_client:send {
            method  = "PATCH",
            path    = "/consumers/bob/oauth2/" .. credential.client_id,
            body    = {
              name             = "4321UDP"
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.not_equal(previous_name, json.name)
        end)
        describe("errors", function()
          it("handles invalid input", function()
            local res = assert(admin_client:send {
              method  = "PATCH",
              path    = "/consumers/bob/oauth2/" .. credential.id,
              body    = {
                redirect_uris = { "not-valid" },
              },
              headers = {
                ["Content-Type"] = "application/json"
              }
            })
            local body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({ redirect_uris = { "cannot parse 'not-valid'" } }, json.fields)
          end)
          it("updating client_secret requires hash_secret", function()
            local res = assert(admin_client:send {
              method  = "PATCH",
              path    = "/consumers/bob/oauth2/" .. credential.id,
              body    = {
                client_secret = "test",
              },
              headers = {
                ["Content-Type"] = "application/json"
              }
            })
            local body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({
              ["@entity"] = {
                  "all or none of these fields must be set: 'hash_secret', 'client_secret'"
                }
              }, json.fields)
          end)
          it("updating hash_secret requires client_secret", function()
            local res = assert(admin_client:send {
              method  = "PATCH",
              path    = "/consumers/bob/oauth2/" .. credential.id,
              body    = {
                hash_secret = true,
              },
              headers = {
                ["Content-Type"] = "application/json"
              }
            })
            local body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({
              ["@entity"] = {
                  "all or none of these fields must be set: 'hash_secret', 'client_secret'"
                }
              }, json.fields)
          end)
        end)
      end)

      describe("DELETE", function()
        it("deletes a credential", function()
          local res = assert(admin_client:send {
            method  = "DELETE",
            path    = "/consumers/bob/oauth2/" .. credential.id,
          })
          assert.res_status(204, res)
        end)
        describe("errors", function()
          it("returns 400 on invalid input", function()
            local res = assert(admin_client:send {
              method  = "DELETE",
              path    = "/consumers/bob/oauth2/blah"
            })
            assert.res_status(404, res)
          end)
          it("returns 404 if not found", function()
            local res = assert(admin_client:send {
              method  = "DELETE",
              path    = "/consumers/bob/oauth2/00000000-0000-0000-0000-000000000000"
            })
            assert.res_status(404, res)
          end)
        end)
      end)
    end)

    describe("/oauth2", function()
      describe("POST", function()
        local consumer
        local service

        lazy_setup(function()
          service = admin_api.services:insert({ host = "oauth2_token.test" })
          consumer = admin_api.consumers:insert({ username = "bob" })
        end)

        lazy_teardown(function()
          admin_api.consumers:remove(consumer)
          admin_api.services:remove(service)
        end)

        it("does not create oauth2 credential when missing consumer", function()
          local res = assert(admin_client:send {
            method = "POST",
            path = "/oauth2",
            body = {
              name = "test",
              redirect_uris =  { "http://localhost/" },
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same("schema violation (consumer: required field missing)", json.message)
        end)

        it("creates oauth2 credential", function()
          local res = assert(admin_client:send {
            method = "POST",
            path = "/oauth2",
            body = {
              name = "test",
              redirect_uris = { "http://localhost/" },
              consumer = {
                id = consumer.id
              }
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(201, res)
          local json = cjson.decode(body)
          assert.equal("test", json.name)
        end)
      end)
    end)

    describe("/oauth2/:client_id_or_id", function()
      describe("PUT", function()
        local consumer
        local service

        lazy_setup(function()
          service = admin_api.services:insert({ host = "oauth2_token.test" })
          consumer = admin_api.consumers:insert({ username = "bob" })
        end)

        lazy_teardown(function()
          admin_api.consumers:remove(consumer)
          admin_api.services:remove(service)
        end)

        it("does not create oauth2 credential when missing consumer", function()
          local res = assert(admin_client:send {
            method = "PUT",
            path = "/oauth2/client-1",
            body = {
              name = "test",
              redirect_uris =  { "http://localhost/" },
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same("schema violation (consumer: required field missing)", json.message)
        end)

        it("creates oauth2 credential", function()
          local res = assert(admin_client:send {
            method = "PUT",
            path = "/oauth2/client-1",
            body    = {
              name = "test",
              redirect_uris =  { "http://localhost/" },
              consumer = {
                id = consumer.id
              }
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.equal("client-1", json.client_id)
          assert.equal("test", json.name)
        end)
      end)
    end)

    describe("/oauth2_tokens/", function()
      describe("POST", function()
        local oauth2_credential
        local consumer
        local service

        lazy_setup(function()
          service = admin_api.services:insert({ host = "oauth2_token.test" })
          consumer = admin_api.consumers:insert({ username = "bob" })
          oauth2_credential = admin_api.oauth2_credentials:insert {
            name          = "Test APP",
            redirect_uris = { helpers.mock_upstream_ssl_url },
            consumer      = { id = consumer.id },
          }
        end)

        lazy_teardown(function()
          admin_api.consumers:remove(consumer)
          admin_api.services:remove(service)
        end)

        it("creates a oauth2 token", function()
          local res = assert(admin_client:send {
            method  = "POST",
            path    = "/oauth2_tokens",
            body    = {
              credential = { id = oauth2_credential.id },
              service    = { id = service.id },
              expires_in = 10
            },
            headers = {
              ["Content-Type"] = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(201, res))
          assert.equal(oauth2_credential.id, body.credential.id)
          assert.equal(10, body.expires_in)
          assert.truthy(body.access_token)
          assert.truthy(body.service.id)
          assert.same(ngx.null, body.refresh_token)
          assert.equal("bearer", body.token_type)
        end)
        describe("errors", function()
          it("returns bad request", function()
            local res = assert(admin_client:send {
              method  = "POST",
              path    = "/oauth2_tokens",
              body    = {},
              headers = {
                ["Content-Type"] = "application/json"
              }
            })
            local body = assert.res_status(400, res)
            local json = cjson.decode(body)
            assert.same({
              expires_in = "required field missing",
              credential = 'required field missing',
            }, json.fields)
          end)
        end)
      end)

      describe("GET", function()
        local oauth2_credential
        local consumer
        local service

        lazy_setup(function()
          service = admin_api.services:insert({ host = "oauth2_token.test" })
          consumer = admin_api.consumers:insert({ username = "bob" })
          oauth2_credential = admin_api.oauth2_credentials:insert {
            name          = "Test APP",
            redirect_uris = { helpers.mock_upstream_ssl_url },
            consumer      = { id = consumer.id },
          }
        end)

        lazy_teardown(function()
          admin_api.consumers:remove(consumer)
          admin_api.services:remove(service)
        end)

        it("retrieves the first page", function()
          for i = 1, 3 do
            admin_api.oauth2_tokens:insert {
              credential = { id = oauth2_credential.id },
              service    = { id = service.id },
              expires_in = 10
            }
          end

          local res = assert(admin_client:send {
            method  = "GET",
            path    = "/oauth2_tokens"
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.is_table(json.data)
          assert.equal(3, #json.data)
        end)
      end)

      describe("/oauth2_tokens/:id", function()
        local oauth2_credential
        local consumer
        local service

        lazy_setup(function()
          service = admin_api.services:insert({ host = "oauth2_token.test" })
          consumer = admin_api.consumers:insert({ username = "bob" })
          oauth2_credential = admin_api.oauth2_credentials:insert {
            name          = "Test APP",
            redirect_uris = { helpers.mock_upstream_ssl_url },
            consumer      = { id = consumer.id },
          }
        end)

        lazy_teardown(function()
          admin_api.consumers:remove(consumer)
          admin_api.services:remove(service)
        end)

        local token
        before_each(function()
          token = db.oauth2_tokens:insert {
            credential = { id = oauth2_credential.id },
            service    = { id = service.id },
            expires_in = 10
          }
        end)
        after_each(function()
          admin_api.oauth2_tokens:remove(token)
        end)

        describe("GET", function()
          it("retrieves oauth2 token by id", function()
            local res = assert(admin_client:send {
              method  = "GET",
              path    = "/oauth2_tokens/" .. token.id
            })
            local body = assert.res_status(200, res)
            local json = cjson.decode(body)
            assert.equal(token.id, json.id)
          end)
          it("retrieves oauth2 token by access_token", function()
            local res = assert(admin_client:send {
              method  = "GET",
              path    = "/oauth2_tokens/" .. token.access_token
            })
            local body = assert.res_status(200, res)
            local json = cjson.decode(body)
            assert.equal(token.id, json.id)
          end)
        end)

        describe("PUT", function()
          it("creates an oauth2 credential", function()
            local res = assert(admin_client:send {
              method  = "PUT",
              path    = "/oauth2_tokens/foobar",
              body    = {
                credential = { id = oauth2_credential.id },
                service    = { id = service.id },
                expires_in = 10
              },
              headers = {
                ["Content-Type"] = "application/json"
              }
            })
            local body = cjson.decode(assert.res_status(200, res))
            assert.equal(oauth2_credential.id, body.credential.id)
            assert.equal(10, body.expires_in)
            assert.equal("foobar", body.access_token)
            assert.equal(ngx.null, body.refresh_token)
            assert.equal("bearer", body.token_type)
          end)
          describe("errors", function()
            it("returns bad request", function()
              local res = assert(admin_client:send {
                method  = "PUT",
                path    = "/oauth2_tokens/foobar",
                body    = {},
                headers = {
                  ["Content-Type"] = "application/json"
                }
              })
              local body = assert.res_status(400, res)
              local json = cjson.decode(body)
              assert.same({
                expires_in = "required field missing",
                credential = 'required field missing',
              }, json.fields)
            end)
          end)
        end)

        describe("PATCH", function()
          it("updates a token by id", function()
            local previous_expires_in = token.expires_in

            local res = assert(admin_client:send {
              method  = "PATCH",
              path    = "/oauth2_tokens/" .. token.id,
              body    = {
                expires_in       = 20
              },
              headers = {
                ["Content-Type"] = "application/json"
              }
            })
            local body = assert.res_status(200, res)
            local json = cjson.decode(body)
            assert.not_equal(previous_expires_in, json.expires_in)
          end)
          it("updates a token by access_token", function()
            local previous_expires_in = token.expires_in

            local res = assert(admin_client:send {
              method  = "PATCH",
              path    = "/oauth2_tokens/" .. token.access_token,
              body   = {
                expires_in       = 400
              },
              headers = {
                ["Content-Type"] = "application/json"
              }
            })
            local body = assert.res_status(200, res)
            local json = cjson.decode(body)
            assert.not_equal(previous_expires_in, json.expires_in)
          end)
          describe("errors", function()
            it("handles invalid input", function()
              local res = assert(admin_client:send {
                method  = "PATCH",
                path    = "/oauth2_tokens/" .. token.id,
                body    = {
                  expires_in       = "hello"
                },
                headers = {
                  ["Content-Type"] = "application/json"
                }
              })
              local body = assert.res_status(400, res)
              local json = cjson.decode(body)
              assert.same({ expires_in = "expected an integer" }, json.fields)
            end)
          end)
        end)

        describe("DELETE", function()
          it("deletes a token", function()
            local res = assert(admin_client:send {
              method  = "DELETE",
              path    = "/oauth2_tokens/" .. token.id,
            })
            assert.res_status(204, res)
          end)
          describe("errors", function()
            it("returns 204 on inexisting tokens", function()
              local res = assert(admin_client:send {
                method  = "DELETE",
                path    = "/oauth2_tokens/blah"
              })
              assert.res_status(204, res)

              local res = assert(admin_client:send {
                method  = "DELETE",
                path    = "/oauth2_tokens/00000000-0000-0000-0000-000000000000"
              })
              assert.res_status(204, res)
            end)
          end)
        end)
      end)
    end)
  end)
end
