@@ -28,7 +62,7 @@
Note that this does not revoke our OAuth app access; you will have to do so in your
- Twitter profile settings or your
+ Twitter profile settings or your
GitHub profile settings.
diff --git a/config/routes.rb b/config/routes.rb
index 6aec1d332..44bc0b218 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -181,6 +181,8 @@ Rails.application.routes.draw do
delete "users/remove_association", to: "users#remove_association"
delete "users/destroy", to: "users#destroy"
post "organizations/generate_new_secret" => "organizations#generate_new_secret"
+ post "users/api_secrets" => "api_secrets#create"
+ delete "users/api_secrets" => "api_secrets#destroy"
# The priority is based upon order of creation: first created -> highest priority.
# See how all your routes lay out with "rake routes".
diff --git a/db/migrate/20181219215401_create_api_secrets.rb b/db/migrate/20181219215401_create_api_secrets.rb
new file mode 100644
index 000000000..33fe61067
--- /dev/null
+++ b/db/migrate/20181219215401_create_api_secrets.rb
@@ -0,0 +1,13 @@
+class CreateApiSecrets < ActiveRecord::Migration[5.1]
+ def change
+ create_table :api_secrets do |t|
+ t.string :secret
+ t.integer :user_id
+ t.string :description, null: false
+
+ t.timestamps
+ end
+ add_index :api_secrets, :secret, unique: true
+ add_index :api_secrets, :user_id
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e67ef4a23..cfeccf580 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20181130224531) do
+ActiveRecord::Schema.define(version: 20181219215401) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -35,6 +35,16 @@ ActiveRecord::Schema.define(version: 20181130224531) do
t.index ["user_id", "user_type"], name: "index_ahoy_messages_on_user_id_and_user_type"
end
+ create_table "api_secrets", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.string "description", null: false
+ t.string "secret"
+ t.datetime "updated_at", null: false
+ t.integer "user_id"
+ t.index ["secret"], name: "index_api_secrets_on_secret", unique: true
+ t.index ["user_id"], name: "index_api_secrets_on_user_id"
+ end
+
create_table "articles", id: :serial, force: :cascade do |t|
t.string "abuse_removal_reason"
t.boolean "allow_big_edits", default: true
diff --git a/spec/factories/api_secrets.rb b/spec/factories/api_secrets.rb
new file mode 100644
index 000000000..ec026cfb8
--- /dev/null
+++ b/spec/factories/api_secrets.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :api_secret do
+ user
+ description { Faker::Lorem.sentence }
+ secret { SecureRandom.base58(24) }
+ end
+end
diff --git a/spec/models/api_secret_spec.rb b/spec/models/api_secret_spec.rb
new file mode 100644
index 000000000..dd2efda75
--- /dev/null
+++ b/spec/models/api_secret_spec.rb
@@ -0,0 +1,8 @@
+require "rails_helper"
+
+RSpec.describe ApiSecret, type: :model do
+ describe "validations" do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to validate_presence_of(:description) }
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 7cd936573..ba6353621 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe User, type: :model do
before { mock_auth_hash }
describe "validations" do
+ it { is_expected.to have_many(:api_secrets) }
it { is_expected.to have_many(:articles) }
it { is_expected.to have_many(:badge_achievements).dependent(:destroy) }
it { is_expected.to have_many(:badges).through(:badge_achievements) }
diff --git a/spec/policies/api_secret_policy_spec.rb b/spec/policies/api_secret_policy_spec.rb
new file mode 100644
index 000000000..e6f1638a9
--- /dev/null
+++ b/spec/policies/api_secret_policy_spec.rb
@@ -0,0 +1,29 @@
+require "rails_helper"
+
+RSpec.describe ApiSecretPolicy do
+ subject { described_class.new(user, api_secret) }
+
+ let(:api_secret) { build(:api_secret) }
+ let(:valid_attributes) { %i[description] }
+
+ context "when user is not signed in" do
+ let(:user) { nil }
+
+ it { within_block_is_expected.to raise_error(Pundit::NotAuthorizedError) }
+ end
+
+ context "when user owns the secret" do
+ let(:user) { api_secret.user }
+
+ it { is_expected.to permit_actions %i[create destroy] }
+ it { is_expected.to permit_mass_assignment_of(valid_attributes) }
+ end
+
+ context "when user does not own the secret" do
+ let(:user) { build(:user) }
+
+ it { is_expected.to permit_actions %i[create] }
+ it { is_expected.to forbid_actions %i[destroy] }
+ it { is_expected.to permit_mass_assignment_of(valid_attributes) }
+ end
+end
diff --git a/spec/requests/api_secrets_create_spec.rb b/spec/requests/api_secrets_create_spec.rb
new file mode 100644
index 000000000..cd0446006
--- /dev/null
+++ b/spec/requests/api_secrets_create_spec.rb
@@ -0,0 +1,44 @@
+require "rails_helper"
+
+RSpec.describe "ApiSecretsCreate", type: :request do
+ let(:user) { build(:user) }
+
+ before { sign_in user }
+
+ describe "POST /users/api_secrets" do
+ context "when create succeeds" do
+ let(:valid_params) { { description: "My Test 3rd Party App" } }
+
+ it "creates an ApiSecret for the user" do
+ expect { post "/users/api_secrets", params: { api_secret: valid_params } }.
+ to change { user.api_secrets.count }.by 1
+ end
+
+ it "sets the description" do
+ post "/users/api_secrets", params: { api_secret: valid_params }
+ expect(user.api_secrets.last.description).to eq valid_params[:description]
+ end
+
+ it "flashes a message containing the token" do
+ post "/users/api_secrets", params: { api_secret: valid_params }
+ expect(flash[:notice]).to include(ApiSecret.last.secret)
+ expect(flash[:error]).to be_nil
+ end
+ end
+
+ context "when create fails" do
+ let(:invalid_params) { { description: nil } } # Force model validation error
+
+ it "does not create the ApiSecret" do
+ expect { post "/users/api_secrets", params: { api_secret: invalid_params } }.
+ not_to (change { user.api_secrets.count })
+ end
+
+ it "flashes an error message" do
+ post "/users/api_secrets", params: { api_secret: invalid_params }
+ expect(flash[:error]).to be_truthy
+ expect(flash[:notice]).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/requests/api_secrets_destroy_spec.rb b/spec/requests/api_secrets_destroy_spec.rb
new file mode 100644
index 000000000..3c4203898
--- /dev/null
+++ b/spec/requests/api_secrets_destroy_spec.rb
@@ -0,0 +1,41 @@
+require "rails_helper"
+
+RSpec.describe "ApiSecretsDestroy", type: :request do
+ let(:api_secret) { create(:api_secret) }
+ let(:user) { api_secret.user }
+
+ before { sign_in user }
+
+ describe "DELETE /users/api_secrets" do
+ context "when delete succeeds" do
+ it "deletes the ApiSecret for the user" do
+ expect { delete "/users/api_secrets", params: { id: api_secret.id } }.
+ to change { user.api_secrets.count }.by -1
+ end
+
+ it "flashes a notice" do
+ delete "/users/api_secrets", params: { id: api_secret.id }
+ expect(flash[:notice]).to be_truthy
+ expect(flash[:error]).to be_nil
+ end
+ end
+
+ context "when delete fails" do
+ before do
+ allow(ApiSecret).to receive(:find_by_id).and_return api_secret
+ allow(api_secret).to receive(:destroy).and_return false
+ end
+
+ it "does not delete the ApiSecret" do
+ expect { delete "/users/api_secrets", params: { id: api_secret.id } }.
+ not_to (change { user.api_secrets.count })
+ end
+
+ it "flashes an error message" do
+ delete "/users/api_secrets", params: { id: api_secret.id }
+ expect(flash[:error]).to be_truthy
+ expect(flash[:notice]).to be_nil
+ end
+ end
+ end
+end