diff --git a/kong/dao/cassandra/apis.lua b/kong/dao/cassandra/apis.lua index 5852f508b7e..3933f5f2a33 100644 --- a/kong/dao/cassandra/apis.lua +++ b/kong/dao/cassandra/apis.lua @@ -26,31 +26,4 @@ function Apis:find_all() return apis end --- @override -function Apis:delete(where_t) - local ok, err = Apis.super.delete(self, where_t) - if not ok then - return false, err - end - - -- delete all related plugins configurations - local plugins_dao = self._factory.plugins_configurations - local select_q, columns = query_builder.select(plugins_dao._table, {api_id = where_t.id}, plugins_dao._column_family_details) - - for rows, err in plugins_dao:execute(select_q, columns, {api_id = where_t.id}, {auto_paging = true}) do - if err then - return nil, err - end - - for _, row in ipairs(rows) do - local ok_del_plugin, err = plugins_dao:delete({id = row.id}) - if not ok_del_plugin then - return nil, err - end - end - end - - return ok -end - return {apis = Apis} diff --git a/kong/dao/cassandra/base_dao.lua b/kong/dao/cassandra/base_dao.lua index eb29457aae6..69899501b26 100644 --- a/kong/dao/cassandra/base_dao.lua +++ b/kong/dao/cassandra/base_dao.lua @@ -44,6 +44,7 @@ function BaseDao:new(properties) self._properties = properties self._statements_cache = {} + self._cascade_delete_hooks = {} end -- Marshall an entity. Does nothing by default, @@ -258,9 +259,7 @@ function BaseDao:execute(query, columns, args_to_bind, options) end -- Execute statement - local results, err = self:_execute(query, args, options) - - return results, err + return self:_execute(query, args, options) end -- Check all fields marked with a `unique` in the schema do not already exist. @@ -551,10 +550,50 @@ function BaseDao:find(page_size, paging_state) return self:find_by_keys(nil, page_size, paging_state) end +-- Add a delete hook on a parent DAO of a foreign row. +-- The delete hook will basically "cascade delete" all foreign rows of a parent row. +-- @see cassandra/factory.lua ':load_daos()' +-- @param foreign_dao_name Name (string) of the parent DAO +-- @param foreign_column Name (string) of the foreign column +-- @param parent_column Name (string) of the parent column identifying the parent row +function BaseDao:add_delete_hook(foreign_dao_name, foreign_column, parent_column) + + -- The actual delete hook + -- @param deleted_primary_key The value of the deleted row's primary key + -- @return boolean True if success, false otherwise + -- @return table A DAOError in case of error + local delete_hook = function(deleted_primary_key) + local foreign_dao = self._factory[foreign_dao_name] + local select_args = { + [foreign_column] = deleted_primary_key[parent_column] + } + + -- Iterate over all rows with the foreign key and delete them. + -- Rows need to be deleted by PRIMARY KEY, and we only have the value of the foreign key, hence we need + -- to retrieve all rows with the foreign key, and then delete them, identifier by their own primary key. + local select_q, columns = query_builder.select(foreign_dao._table, select_args, foreign_dao._column_family_details ) + for rows, err in foreign_dao:execute(select_q, columns, select_args, {auto_paging = true}) do + if err then + return false, err + end + for _, row in ipairs(rows) do + local ok_del_foreign_row, err = foreign_dao:delete(row) + if not ok_del_foreign_row then + return false, err + end + end + end + + return true + end + + table.insert(self._cascade_delete_hooks, delete_hook) +end + -- Delete the row at a given PRIMARY KEY. -- @param `where_t` A table containing the PRIMARY KEY (columns/values) of the row to delete -- @return `success` True if deleted, false if otherwise or not found --- @return `error` Error if any during the query execution +-- @return `error` Error if any during the query execution or the cascade delete hook function BaseDao:delete(where_t) assert(self._primary_key ~= nil and type(self._primary_key) == "table" , "Entity does not have a primary_key") assert(where_t ~= nil and type(where_t) == "table", "where_t must be a table") @@ -569,7 +608,21 @@ function BaseDao:delete(where_t) local t_primary_key = extract_primary_key(where_t, self._primary_key, self._clustering_key) local delete_q, where_columns = query_builder.delete(self._table, t_primary_key) - return self:execute(delete_q, where_columns, where_t) + local results, err = self:execute(delete_q, where_columns, where_t) + if err then + return false, err + end + + -- Delete successful, trigger cascade delete hooks if any. + local foreign_err + for _, hook in ipairs(self._cascade_delete_hooks) do + foreign_err = select(2, hook(t_primary_key)) + if foreign_err then + return false, foreign_err + end + end + + return results end -- Truncate the table of this DAO diff --git a/kong/dao/cassandra/consumers.lua b/kong/dao/cassandra/consumers.lua index b054e1d7354..9eda80991ab 100644 --- a/kong/dao/cassandra/consumers.lua +++ b/kong/dao/cassandra/consumers.lua @@ -1,5 +1,4 @@ local BaseDao = require "kong.dao.cassandra.base_dao" -local query_builder = require "kong.dao.cassandra.query_builder" local consumers_schema = require "kong.dao.schemas.consumers" local Consumers = BaseDao:extend() @@ -11,31 +10,4 @@ function Consumers:new(properties) Consumers.super.new(self, properties) end --- @override -function Consumers:delete(where_t) - local ok, err = Consumers.super.delete(self, where_t) - if not ok then - return false, err - end - - local plugins_dao = self._factory.plugins_configurations - local select_q, columns = query_builder.select(plugins_dao._table, {consumer_id = where_t.id}, plugins_dao._column_family_details) - - -- delete all related plugins configurations - for rows, err in plugins_dao:execute(select_q, columns, {consumer_id = where_t.id}, {auto_paging = true}) do - if err then - return nil, err - end - - for _, row in ipairs(rows) do - local ok_del_plugin, err = plugins_dao:delete({id = row.id}) - if not ok_del_plugin then - return nil, err - end - end - end - - return ok -end - -return { consumers = Consumers } +return {consumers = Consumers} diff --git a/kong/dao/cassandra/factory.lua b/kong/dao/cassandra/factory.lua index d743162715e..9c7288a0634 100644 --- a/kong/dao/cassandra/factory.lua +++ b/kong/dao/cassandra/factory.lua @@ -36,24 +36,53 @@ function CassandraFactory:new(properties, plugins) -- Load plugins DAOs if plugins then - for _, v in ipairs(plugins) do - local loaded, plugin_daos_mod = utils.load_module_if_exists("kong.plugins."..v..".daos") - if loaded then - if ngx then - ngx.log(ngx.DEBUG, "Loading DAO for plugin: "..v) - end - self:load_daos(plugin_daos_mod) - elseif ngx then - ngx.log(ngx.DEBUG, "No DAO loaded for plugin: "..v) + self:load_plugins(plugins) + end +end + +-- Load an array of plugins (array of plugins names). If any of those plugins have DAOs, +-- they will be loaded into the factory. +-- @param plugins Array of plugins names +function CassandraFactory:load_plugins(plugins) + for _, v in ipairs(plugins) do + local loaded, plugin_daos_mod = utils.load_module_if_exists("kong.plugins."..v..".daos") + if loaded then + if ngx then + ngx.log(ngx.DEBUG, "Loading DAO for plugin: "..v) end + self:load_daos(plugin_daos_mod) + elseif ngx then + ngx.log(ngx.DEBUG, "No DAO loaded for plugin: "..v) end end end +-- Load a plugin's DAOs (plugins can have more than one DAO) in the factory and create cascade delete hooks. +-- Cascade delete hooks are triggered when a parent of a foreign row is deleted. +-- @param plugin_daos A table with key/values representing daos names and instances. function CassandraFactory:load_daos(plugin_daos) + local dao for name, plugin_dao in pairs(plugin_daos) do - self.daos[name] = plugin_dao(self._properties) - self.daos[name]._factory = self + dao = plugin_dao(self._properties) + dao._factory = self + self.daos[name] = dao + if dao._schema then + -- Check for any foreign relations to trigger cascade deletes + for field_name, field in pairs(dao._schema.fields) do + if field.foreign ~= nil then + -- Foreign key columns need to be queryable, hence they need to have an index + assert(field.queryable, "Foreign property "..field_name.." of shema "..name.." must be queryable (have an index)") + + local parent_dao_name, parent_column = unpack(stringy.split(field.foreign, ":")) + assert(parent_dao_name ~= nil, "Foreign property "..field_name.." of schema "..name.." must contain 'parent_dao:parent_column") + assert(parent_column ~= nil, "Foreign property "..field_name.." of schema "..name.." must contain 'parent_dao:parent_column") + + -- Add delete hook to the parent DAO + local parent_dao = self[parent_dao_name] + parent_dao:add_delete_hook(name, field_name, parent_column) + end + end + end end end diff --git a/kong/plugins/basicauth/daos.lua b/kong/plugins/basicauth/daos.lua index 0a18a2682df..bb047d7cbae 100644 --- a/kong/plugins/basicauth/daos.lua +++ b/kong/plugins/basicauth/daos.lua @@ -5,7 +5,7 @@ local SCHEMA = { fields = { id = { type = "id", dao_insert_value = true }, created_at = { type = "timestamp", dao_insert_value = true }, - consumer_id = { type = "id", required = true, foreign = "consumers:id" }, + consumer_id = { type = "id", required = true, queryable = true, foreign = "consumers:id" }, username = { type = "string", required = true, unique = true, queryable = true }, password = { type = "string" } } diff --git a/kong/plugins/keyauth/daos.lua b/kong/plugins/keyauth/daos.lua index 91aa4065e3e..382547970be 100644 --- a/kong/plugins/keyauth/daos.lua +++ b/kong/plugins/keyauth/daos.lua @@ -14,7 +14,7 @@ local SCHEMA = { fields = { id = { type = "id", dao_insert_value = true }, created_at = { type = "timestamp", dao_insert_value = true }, - consumer_id = { type = "id", required = true, foreign = "consumers:id" }, + consumer_id = { type = "id", required = true, queryable = true, foreign = "consumers:id" }, key = { type = "string", required = false, unique = true, queryable = true, func = generate_if_missing } } } diff --git a/kong/plugins/oauth2/access.lua b/kong/plugins/oauth2/access.lua index 6b5ebb00888..02777922138 100644 --- a/kong/plugins/oauth2/access.lua +++ b/kong/plugins/oauth2/access.lua @@ -213,9 +213,9 @@ local function issue_token(conf) response_params = {[ERROR] = "access_denied", error_description = "You must use HTTPS"} else local grant_type = parameters[GRANT_TYPE] - if not (grant_type == GRANT_AUTHORIZATION_CODE or - grant_type == GRANT_REFRESH_TOKEN or - (conf.enable_client_credentials and grant_type == GRANT_CLIENT_CREDENTIALS) or + if not (grant_type == GRANT_AUTHORIZATION_CODE or + grant_type == GRANT_REFRESH_TOKEN or + (conf.enable_client_credentials and grant_type == GRANT_CLIENT_CREDENTIALS) or (conf.enable_password_grant and grant_type == GRANT_PASSWORD)) then response_params = {[ERROR] = "invalid_request", error_description = "Invalid "..GRANT_TYPE} end @@ -353,7 +353,7 @@ function _M.execute(conf) -- Check if the API has a path and if it's being invoked with the path resolver local path_prefix = (ngx.ctx.api.path and stringy.startswith(ngx.var.request_uri, ngx.ctx.api.path)) and ngx.ctx.api.path or "" if stringy.endswith(path_prefix, "/") then - path_prefix = path_prefix:sub(1, path_prefix:len() - 1) + path_prefix = path_prefix:sub(1, path_prefix:len() - 1) end if ngx.req.get_method() == "POST" then diff --git a/kong/plugins/oauth2/daos.lua b/kong/plugins/oauth2/daos.lua index e5ad9b1ce44..9cb239dedf9 100644 --- a/kong/plugins/oauth2/daos.lua +++ b/kong/plugins/oauth2/daos.lua @@ -20,7 +20,7 @@ local OAUTH2_CREDENTIALS_SCHEMA = { primary_key = {"id"}, fields = { id = { type = "id", dao_insert_value = true }, - consumer_id = { type = "id", required = true, foreign = "consumers:id" }, + consumer_id = { type = "id", required = true, queryable = true, foreign = "consumers:id" }, name = { type = "string", required = true }, client_id = { type = "string", required = false, unique = true, queryable = true, func = generate_if_missing }, client_secret = { type = "string", required = false, unique = true, func = generate_if_missing }, @@ -45,7 +45,7 @@ local OAUTH2_TOKENS_SCHEMA = { primary_key = {"id"}, fields = { id = { type = "id", dao_insert_value = true }, - credential_id = { type = "id", required = true, foreign = "oauth2_credentials:id" }, + credential_id = { type = "id", required = true, queryable = true, foreign = "oauth2_credentials:id" }, token_type = { type = "string", required = true, enum = { BEARER }, default = BEARER }, access_token = { type = "string", required = false, unique = true, queryable = true, immutable = true, func = generate_if_missing }, refresh_token = { type = "string", required = false, unique = true, queryable = true, immutable = true, func = generate_refresh_token }, diff --git a/kong/plugins/oauth2/migrations/cassandra.lua b/kong/plugins/oauth2/migrations/cassandra.lua index b688b09303f..e1136d7e5b8 100644 --- a/kong/plugins/oauth2/migrations/cassandra.lua +++ b/kong/plugins/oauth2/migrations/cassandra.lua @@ -55,6 +55,19 @@ local Migrations = { DROP TABLE oauth2_tokens; ]] end + }, + { + name = "2015-08-24-215800_cascade_delete_index", + up = function() + return [[ + CREATE INDEX IF NOT EXISTS oauth2_credential_id_idx ON oauth2_tokens(credential_id); + ]] + end, + down = function() + return [[ + DROP INDEX oauth2_credential_id_idx; + ]] + end } } diff --git a/spec/integration/dao/cassandra/base_dao_spec.lua b/spec/integration/dao/cassandra/base_dao_spec.lua index d4fa1357afb..66695826c15 100644 --- a/spec/integration/dao/cassandra/base_dao_spec.lua +++ b/spec/integration/dao/cassandra/base_dao_spec.lua @@ -481,6 +481,10 @@ describe("Cassandra", function() describe(":delete()", function() + teardown(function() + spec_helper.drop_db() + end) + describe_core_collections(function(type, collection) it("should error if called with invalid parameters", function() @@ -512,110 +516,6 @@ describe("Cassandra", function() end) end) - - describe("APIs", function() - local api, untouched_api - - setup(function() - spec_helper.drop_db() - local fixtures = spec_helper.insert_fixtures { - api = { - { name = "cascade delete", - public_dns = "mockbin.com", - target_url = "http://mockbin.com" }, - { name = "untouched cascade delete", - public_dns = "untouched.com", - target_url = "http://mockbin.com" } - }, - plugin_configuration = { - {name = "keyauth", __api = 1}, - {name = "ratelimiting", value = { minute = 6}, __api = 1}, - {name = "filelog", value = {path = "/tmp/spec.log" }, __api = 1}, - - {name = "keyauth", __api = 2} - } - } - api = fixtures.api[1] - untouched_api = fixtures.api[2] - end) - - teardown(function() - spec_helper.drop_db() - end) - - it("should delete all related plugins_configurations when deleting an API", function() - local ok, err = dao_factory.apis:delete(api) - assert.falsy(err) - assert.True(ok) - - -- Make sure we have 0 matches - local results, err = dao_factory.plugins_configurations:find_by_keys { - api_id = api.id - } - assert.falsy(err) - assert.equal(0, #results) - - -- Make sure the untouched API still has its plugins - results, err = dao_factory.plugins_configurations:find_by_keys { - api_id = untouched_api.id - } - assert.falsy(err) - assert.equal(1, #results) - end) - - end) - - describe("Consumers", function() - local consumer, untouched_consumer - - setup(function() - spec_helper.drop_db() - local fixtures = spec_helper.insert_fixtures { - api = { - { name = "cascade delete", - public_dns = "mockbin.com", - target_url = "http://mockbin.com" } - }, - consumer = { - {username = "king kong"}, - {username = "untouched consumer"} - }, - plugin_configuration = { - {name = "ratelimiting", value = { minute = 6}, __api = 1, __consumer = 1}, - {name = "response_transformer", __api = 1, __consumer = 1}, - {name = "filelog", value = {path = "/tmp/spec.log" }, __api = 1, __consumer = 1}, - - {name = "request_transformer", __api = 1, __consumer = 2} - } - } - consumer = fixtures.consumer[1] - untouched_consumer = fixtures.consumer[2] - end) - - teardown(function() - spec_helper.drop_db() - end) - - it("should delete all related plugins_configurations when deleting a Consumer", function() - local ok, err = dao_factory.consumers:delete(consumer) - assert.True(ok) - assert.falsy(err) - - local results, err = dao_factory.plugins_configurations:find_by_keys { - consumer_id = consumer.id - } - assert.falsy(err) - assert.are.same(0, #results) - - -- Make sure the untouched Consumer still has its plugin - results, err = dao_factory.plugins_configurations:find_by_keys { - consumer_id = untouched_consumer.id - } - assert.falsy(err) - assert.are.same(1, #results) - end) - - end) end) -- describe :delete() -- diff --git a/spec/integration/dao/cassandra/cascade_spec.lua b/spec/integration/dao/cassandra/cascade_spec.lua new file mode 100644 index 00000000000..75d8cdca21e --- /dev/null +++ b/spec/integration/dao/cassandra/cascade_spec.lua @@ -0,0 +1,257 @@ +local spec_helper = require "spec.spec_helpers" + +local env = spec_helper.get_env() +local dao_factory = env.dao_factory + +dao_factory:load_plugins({"keyauth", "basicauth", "oauth2"}) + +describe("Cassandra cascade delete", function() + + setup(function() + spec_helper.prepare_db() + end) + + describe("API -> plugin_configurations", function() + local api, untouched_api + + setup(function() + local fixtures = spec_helper.insert_fixtures { + api = { + {name = "cascade delete", + public_dns = "mockbin.com", + target_url = "http://mockbin.com"}, + {name = "untouched cascade delete", + public_dns = "untouched.com", + target_url = "http://mockbin.com"} + }, + plugin_configuration = { + {name = "keyauth", __api = 1}, + {name = "ratelimiting", value = {minute = 6}, __api = 1}, + {name = "filelog", value = {path = "/tmp/spec.log"}, __api = 1}, + {name = "keyauth", __api = 2} + } + } + api = fixtures.api[1] + untouched_api = fixtures.api[2] + end) + + teardown(function() + spec_helper.drop_db() + end) + + it("should delete foreign plugins_configurations when deleting an API", function() + local ok, err = dao_factory.apis:delete(api) + assert.falsy(err) + assert.True(ok) + + -- Make sure we have 0 matches + local results, err = dao_factory.plugins_configurations:find_by_keys { + api_id = api.id + } + assert.falsy(err) + assert.equal(0, #results) + + -- Make sure the untouched API still has its plugins + results, err = dao_factory.plugins_configurations:find_by_keys { + api_id = untouched_api.id + } + assert.falsy(err) + assert.equal(1, #results) + end) + end) + + describe("Consumer -> plugin_configurations", function() + local consumer, untouched_consumer + + setup(function() + local fixtures = spec_helper.insert_fixtures { + api = { + {name = "cascade delete", + public_dns = "mockbin.com", + target_url = "http://mockbin.com"} + }, + consumer = { + {username = "king kong"}, + {username = "untouched consumer"} + }, + plugin_configuration = { + {name = "ratelimiting", value = {minute = 6}, __api = 1, __consumer = 1}, + {name = "response_transformer", __api = 1, __consumer = 1}, + {name = "filelog", value = {path = "/tmp/spec.log"}, __api = 1, __consumer = 1}, + {name = "request_transformer", __api = 1, __consumer = 2} + } + } + consumer = fixtures.consumer[1] + untouched_consumer = fixtures.consumer[2] + end) + + teardown(function() + spec_helper.drop_db() + end) + + it("should delete foreign plugins_configurations when deleting a Consumer", function() + local ok, err = dao_factory.consumers:delete(consumer) + assert.falsy(err) + assert.True(ok) + + local results, err = dao_factory.plugins_configurations:find_by_keys { + consumer_id = consumer.id + } + assert.falsy(err) + assert.equal(0, #results) + + -- Make sure the untouched Consumer still has its plugin + results, err = dao_factory.plugins_configurations:find_by_keys { + consumer_id = untouched_consumer.id + } + assert.falsy(err) + assert.equal(1, #results) + end) + end) + + describe("Consumer -> keyauth_credentials", function() + local consumer, untouched_consumer + + setup(function() + local fixtures = spec_helper.insert_fixtures { + consumer = { + {username = "cascade_delete_consumer"}, + {username = "untouched_consumer"} + }, + keyauth_credential = { + {key = "apikey123", __consumer = 1}, + {key = "apikey456", __consumer = 2} + } + } + consumer = fixtures.consumer[1] + untouched_consumer = fixtures.consumer[2] + end) + + teardown(function() + spec_helper.drop_db() + end) + + it("should delete foreign keyauth_credentials when deleting a Consumer", function() + local ok, err = dao_factory.consumers:delete(consumer) + assert.falsy(err) + assert.True(ok) + + local results, err = dao_factory.keyauth_credentials:find_by_keys { + consumer_id = consumer.id + } + assert.falsy(err) + assert.equal(0, #results) + + results, err = dao_factory.keyauth_credentials:find_by_keys { + consumer_id = untouched_consumer.id + } + assert.falsy(err) + assert.equal(1, #results) + end) + end) + + describe("Consumer -> basicauth_credentials", function() + local consumer, untouched_consumer + + setup(function() + local fixtures = spec_helper.insert_fixtures { + consumer = { + {username = "cascade_delete_consumer"}, + {username = "untouched_consumer"} + }, + basicauth_credential = { + {username = "username", password = "password", __consumer = 1}, + {username = "username2", password = "password2", __consumer = 2} + } + } + consumer = fixtures.consumer[1] + untouched_consumer = fixtures.consumer[2] + end) + + teardown(function() + spec_helper.drop_db() + end) + + it("should delete foreign basicauth_credentials when deleting a Consumer", function() + local ok, err = dao_factory.consumers:delete(consumer) + assert.falsy(err) + assert.True(ok) + + local results, err = dao_factory.basicauth_credentials:find_by_keys { + consumer_id = consumer.id + } + assert.falsy(err) + assert.equal(0, #results) + + results, err = dao_factory.basicauth_credentials:find_by_keys { + consumer_id = untouched_consumer.id + } + assert.falsy(err) + assert.equal(1, #results) + end) + end) + + describe("Consumer -> oauth2_credentials -> oauth2_tokens", function() + local consumer, untouched_consumer, credential + + setup(function() + local fixtures = spec_helper.insert_fixtures { + consumer = { + {username = "cascade_delete_consumer"}, + {username = "untouched_consumer"} + }, + oauth2_credential = { + {client_id = "clientid123", + client_secret = "secret123", + redirect_uri = "http://google.com/kong", + name = "testapp", + __consumer = 1}, + {client_id = "clientid1232", + client_secret = "secret1232", + redirect_uri = "http://google.com/kong", + name = "testapp", + __consumer = 2} + } + } + consumer = fixtures.consumer[1] + untouched_consumer = fixtures.consumer[2] + credential = fixtures.oauth2_credential[1] + + local _, err = dao_factory.oauth2_tokens:insert { + credential_id = credential.id, + authenticated_userid = consumer.id, + expires_in = 100, + scope = "email" + } + assert.falsy(err) + end) + + teardown(function() + spec_helper.drop_db() + end) + + it("should delete foreign oauth2_credentials and tokens when deleting a Consumer", function() + local ok, err = dao_factory.consumers:delete(consumer) + assert.falsy(err) + assert.True(ok) + + local results, err = dao_factory.oauth2_credentials:find_by_keys { + consumer_id = consumer.id + } + assert.falsy(err) + assert.equal(0, #results) + + results, err = dao_factory.oauth2_tokens:find_by_keys { + credential_id = credential.id + } + assert.falsy(err) + assert.equal(0, #results) + + results, err = dao_factory.oauth2_credentials:find_by_keys { + consumer_id = untouched_consumer.id + } + assert.falsy(err) + assert.equal(1, #results) + end) + end) +end) diff --git a/spec/plugins/oauth2/api_spec.lua b/spec/plugins/oauth2/api_spec.lua index 9fa162a18b3..707cd9be4c2 100644 --- a/spec/plugins/oauth2/api_spec.lua +++ b/spec/plugins/oauth2/api_spec.lua @@ -13,7 +13,7 @@ describe("OAuth 2 Credentials API", function() teardown(function() spec_helper.stop_kong() end) - + describe("/consumers/:consumer/oauth2/", function() setup(function() @@ -40,7 +40,7 @@ describe("OAuth 2 Credentials API", function() end) end) - + describe("PUT", function() setup(function() spec_helper.get_env().dao_factory.keyauth_credentials:delete({id=credential.id}) @@ -60,7 +60,7 @@ describe("OAuth 2 Credentials API", function() end) end) - + describe("GET", function() it("should retrieve all", function() @@ -72,9 +72,9 @@ describe("OAuth 2 Credentials API", function() end) end) - + describe("/consumers/:consumer/oauth2/:id", function() - + describe("GET", function() it("should retrieve by id", function()