+
+ L'image ci-dessus est une création de David Revoy sous licence CC-By.
+
diff --git a/public/manifest.json b/public/manifest.json
deleted file mode 100644
index 26e169ad2f879e744acaad338fa6a0332e89f7ee..0000000000000000000000000000000000000000
--- a/public/manifest.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "name": "Mastodon",
- "description": "Mastodon is a free, open-source social network server.",
- "icons": [
- {
- "src": "\/android-chrome-192x192.png",
- "sizes": "192x192",
- "type": "image\/png"
- }
- ],
- "theme_color": "#282c37",
- "display": "standalone",
- "start_url": "/web/timelines/home"
-}
diff --git a/public/oops.jpg b/public/oops.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..fa5eb914beb66a4a2c0cea95b59269ed90e2e1a3
Binary files /dev/null and b/public/oops.jpg differ
diff --git a/spec/controllers/admin/instances_controller_spec.rb b/spec/controllers/admin/instances_controller_spec.rb
index 37fa8dd9a00a2e4cb2c79766a03707f7a2847a08..f57e3fa97d9e52e7445eede95f23495400e8a74d 100644
--- a/spec/controllers/admin/instances_controller_spec.rb
+++ b/spec/controllers/admin/instances_controller_spec.rb
@@ -8,8 +8,23 @@ RSpec.describe Admin::InstancesController, type: :controller do
end
describe 'GET #index' do
- it 'returns http success' do
- get :index
+ around do |example|
+ default_per_page = Account.default_per_page
+ Account.paginates_per 1
+ example.run
+ Account.paginates_per default_per_page
+ end
+
+ it 'renders instances' do
+ Fabricate(:account, domain: 'popular')
+ Fabricate(:account, domain: 'popular')
+ Fabricate(:account, domain: 'less.popular')
+
+ get :index, params: { page: 2 }
+
+ instances = assigns(:instances).to_a
+ expect(instances.size).to eq 1
+ expect(instances[0].domain).to eq 'less.popular'
expect(response).to have_http_status(:success)
end
diff --git a/spec/controllers/admin/pubsubhubbub_controller_spec.rb b/spec/controllers/admin/pubsubhubbub_controller_spec.rb
deleted file mode 100644
index c2bab5daca45987181963273c9702724dc6ea024..0000000000000000000000000000000000000000
--- a/spec/controllers/admin/pubsubhubbub_controller_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-require 'rails_helper'
-
-RSpec.describe Admin::PubsubhubbubController, type: :controller do
- render_views
-
- describe 'GET #index' do
- before do
- sign_in Fabricate(:user, admin: true), scope: :user
- end
-
- it 'returns http success' do
- get :index
- expect(response).to have_http_status(:success)
- end
- end
-end
diff --git a/spec/controllers/admin/reports_controller_spec.rb b/spec/controllers/admin/reports_controller_spec.rb
index 89b58ad0745b44f3a6558e5c1d94a8cf6ac49e72..71a185147bf64b0cfffce85cecbbd4b8905290c8 100644
--- a/spec/controllers/admin/reports_controller_spec.rb
+++ b/spec/controllers/admin/reports_controller_spec.rb
@@ -10,27 +10,38 @@ describe Admin::ReportsController do
describe 'GET #index' do
it 'returns http success with no filters' do
- allow(Report).to receive(:unresolved).and_return(Report.all)
+ specified = Fabricate(:report, action_taken: false)
+ Fabricate(:report, action_taken: true)
+
get :index
+ reports = assigns(:reports).to_a
+ expect(reports.size).to eq 1
+ expect(reports[0]).to eq specified
expect(response).to have_http_status(:success)
- expect(Report).to have_received(:unresolved)
end
it 'returns http success with resolved filter' do
- allow(Report).to receive(:resolved).and_return(Report.all)
+ specified = Fabricate(:report, action_taken: true)
+ Fabricate(:report, action_taken: false)
+
get :index, params: { resolved: 1 }
+ reports = assigns(:reports).to_a
+ expect(reports.size).to eq 1
+ expect(reports[0]).to eq specified
+
expect(response).to have_http_status(:success)
- expect(Report).to have_received(:resolved)
end
end
describe 'GET #show' do
- it 'returns http success' do
+ it 'renders report' do
report = Fabricate(:report)
get :show, params: { id: report }
+
+ expect(assigns(:report)).to eq report
expect(response).to have_http_status(:success)
end
end
diff --git a/spec/controllers/admin/resets_controller_spec.rb b/spec/controllers/admin/resets_controller_spec.rb
index a0a77478e03b0460b8819a94fb4b17d2aebe2e41..a20a460bd0559322d9248668d7e6f12f38a7bf15 100644
--- a/spec/controllers/admin/resets_controller_spec.rb
+++ b/spec/controllers/admin/resets_controller_spec.rb
@@ -10,6 +10,10 @@ describe Admin::ResetsController do
describe 'POST #create' do
it 'redirects to admin accounts page' do
+ expect_any_instance_of(User).to receive(:send_reset_password_instructions) do |value|
+ expect(value.account_id).to eq account.id
+ end
+
post :create, params: { account_id: account.id }
expect(response).to redirect_to(admin_accounts_path)
diff --git a/spec/controllers/admin/settings_controller_spec.rb b/spec/controllers/admin/settings_controller_spec.rb
index 7fd49a0f11ae6c3167b448e42558e03f5d2cd3aa..d9dde3c92ae888111c36ee526dc04cfdcf6a9948 100644
--- a/spec/controllers/admin/settings_controller_spec.rb
+++ b/spec/controllers/admin/settings_controller_spec.rb
@@ -47,20 +47,36 @@ RSpec.describe Admin::SettingsController, type: :controller do
end
end
- it 'updates a settings value' do
- Setting.site_title = 'Original'
- patch :update, params: { site_title: 'New title' }
+ context do
+ around do |example|
+ site_title = Setting.site_title
+ example.run
+ Setting.site_title = site_title
+ end
- expect(response).to redirect_to(edit_admin_settings_path)
- expect(Setting.site_title).to eq 'New title'
+ it 'updates a settings value' do
+ Setting.site_title = 'Original'
+ patch :update, params: { site_title: 'New title' }
+
+ expect(response).to redirect_to(edit_admin_settings_path)
+ expect(Setting.site_title).to eq 'New title'
+ end
end
- it 'typecasts open_registrations to boolean' do
- Setting.open_registrations = false
- patch :update, params: { open_registrations: 'true' }
+ context do
+ around do |example|
+ open_registrations = Setting.open_registrations
+ example.run
+ Setting.open_registrations = open_registrations
+ end
- expect(response).to redirect_to(edit_admin_settings_path)
- expect(Setting.open_registrations).to eq true
+ it 'typecasts open_registrations to boolean' do
+ Setting.open_registrations = false
+ patch :update, params: { open_registrations: 'true' }
+
+ expect(response).to redirect_to(edit_admin_settings_path)
+ expect(Setting.open_registrations).to eq true
+ end
end
end
end
diff --git a/spec/controllers/admin/silences_controller_spec.rb b/spec/controllers/admin/silences_controller_spec.rb
index 16b326542d37f89aaeced6690fddc0af0b0f87e7..78560eb393a483b5834698dee9141437d5a70d6b 100644
--- a/spec/controllers/admin/silences_controller_spec.rb
+++ b/spec/controllers/admin/silences_controller_spec.rb
@@ -3,23 +3,30 @@ require 'rails_helper'
describe Admin::SilencesController do
render_views
- let(:account) { Fabricate(:account) }
before do
sign_in Fabricate(:user, admin: true), scope: :user
end
describe 'POST #create' do
it 'redirects to admin accounts page' do
+ account = Fabricate(:account, silenced: false)
+
post :create, params: { account_id: account.id }
+ account.reload
+ expect(account.silenced?).to eq true
expect(response).to redirect_to(admin_accounts_path)
end
end
describe 'DELETE #destroy' do
it 'redirects to admin accounts page' do
+ account = Fabricate(:account, silenced: true)
+
delete :destroy, params: { account_id: account.id }
+ account.reload
+ expect(account.silenced?).to eq false
expect(response).to redirect_to(admin_accounts_path)
end
end
diff --git a/spec/controllers/admin/subscriptions_controller_spec.rb b/spec/controllers/admin/subscriptions_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eb6f12b16e1a5a4a354a7cf61f0b3a3f0ae57d61
--- /dev/null
+++ b/spec/controllers/admin/subscriptions_controller_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+require 'rails_helper'
+
+RSpec.describe Admin::SubscriptionsController, type: :controller do
+ render_views
+
+ describe 'GET #index' do
+ around do |example|
+ default_per_page = Subscription.default_per_page
+ Subscription.paginates_per 1
+ example.run
+ Subscription.paginates_per default_per_page
+ end
+
+ before do
+ sign_in Fabricate(:user, admin: true), scope: :user
+ end
+
+ it 'renders subscriptions' do
+ Fabricate(:subscription)
+ specified = Fabricate(:subscription)
+
+ get :index
+
+ subscriptions = assigns(:subscriptions)
+ expect(subscriptions.count).to eq 1
+ expect(subscriptions[0]).to eq specified
+
+ expect(response).to have_http_status(:success)
+ end
+ end
+end
diff --git a/spec/controllers/admin/suspensions_controller_spec.rb b/spec/controllers/admin/suspensions_controller_spec.rb
index 2d9adc23d6ef0c7aeec5040a5aeb603417f863eb..ddfc938d1876c10d6d57ad99d1dadc10c8c96253 100644
--- a/spec/controllers/admin/suspensions_controller_spec.rb
+++ b/spec/controllers/admin/suspensions_controller_spec.rb
@@ -3,13 +3,15 @@ require 'rails_helper'
describe Admin::SuspensionsController do
render_views
- let(:account) { Fabricate(:account) }
before do
sign_in Fabricate(:user, admin: true), scope: :user
end
describe 'POST #create' do
it 'redirects to admin accounts page' do
+ account = Fabricate(:account, suspended: false)
+ expect(Admin::SuspensionWorker).to receive(:perform_async).with(account.id)
+
post :create, params: { account_id: account.id }
expect(response).to redirect_to(admin_accounts_path)
@@ -18,8 +20,12 @@ describe Admin::SuspensionsController do
describe 'DELETE #destroy' do
it 'redirects to admin accounts page' do
+ account = Fabricate(:account, suspended: true)
+
delete :destroy, params: { account_id: account.id }
+ account.reload
+ expect(account.suspended?).to eq false
expect(response).to redirect_to(admin_accounts_path)
end
end
diff --git a/spec/controllers/admin/two_factor_authentications_controller_spec.rb b/spec/controllers/admin/two_factor_authentications_controller_spec.rb
index 69f26039a066cc60c87ab3f1d5b3c41d4e9149c1..4c1aa88d7547f216353a80f0946400d5e8486a0c 100644
--- a/spec/controllers/admin/two_factor_authentications_controller_spec.rb
+++ b/spec/controllers/admin/two_factor_authentications_controller_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
describe Admin::TwoFactorAuthenticationsController do
render_views
- let(:user) { Fabricate(:user) }
+ let(:user) { Fabricate(:user, otp_required_for_login: true) }
before do
sign_in Fabricate(:user, admin: true), scope: :user
end
@@ -11,6 +11,9 @@ describe Admin::TwoFactorAuthenticationsController do
describe 'DELETE #destroy' do
it 'redirects to admin accounts page' do
delete :destroy, params: { user_id: user.id }
+
+ user.reload
+ expect(user.otp_required_for_login).to eq false
expect(response).to redirect_to(admin_accounts_path)
end
end
diff --git a/spec/controllers/api/activitypub/activities_controller_spec.rb b/spec/controllers/api/activitypub/activities_controller_spec.rb
index 2c966a45a24e28b0ab93f48393b3a9a448ecd348..07df28ac2b6c1b934e2a250f9b78e8aa0ae1e6de 100644
--- a/spec/controllers/api/activitypub/activities_controller_spec.rb
+++ b/spec/controllers/api/activitypub/activities_controller_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-RSpec.describe Api::Activitypub::ActivitiesController, type: :controller do
+RSpec.describe Api::ActivityPub::ActivitiesController, type: :controller do
render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
diff --git a/spec/controllers/api/activitypub/notes_controller_spec.rb b/spec/controllers/api/activitypub/notes_controller_spec.rb
index 39ddec03e28284c3232bd5f65e2282c986bffb50..a0f05dc65932ea0965b354cb3436eca26a7f5c67 100644
--- a/spec/controllers/api/activitypub/notes_controller_spec.rb
+++ b/spec/controllers/api/activitypub/notes_controller_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-RSpec.describe Api::Activitypub::NotesController, type: :controller do
+RSpec.describe Api::ActivityPub::NotesController, type: :controller do
render_views
let(:user_alice) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
diff --git a/spec/controllers/api/activitypub/outbox_controller_spec.rb b/spec/controllers/api/activitypub/outbox_controller_spec.rb
index 99797c582df671a2b889aaf222179a83062d74b8..049cf451d712a0c6c86f202f68a328c446ca36f4 100644
--- a/spec/controllers/api/activitypub/outbox_controller_spec.rb
+++ b/spec/controllers/api/activitypub/outbox_controller_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-RSpec.describe Api::Activitypub::OutboxController, type: :controller do
+RSpec.describe Api::ActivityPub::OutboxController, type: :controller do
render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
diff --git a/spec/controllers/api/base_controller_spec.rb b/spec/controllers/api/base_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7d5e0116c0da8b01cbf2d2343030e252ccbb2e74
--- /dev/null
+++ b/spec/controllers/api/base_controller_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+class FakeService; end
+
+describe Api::BaseController do
+ controller do
+ def success
+ head 200
+ end
+
+ def error
+ FakeService.new
+ end
+ end
+
+ describe 'Forgery protection' do
+ before do
+ routes.draw { post 'success' => 'api/base#success' }
+ end
+
+ it 'does not protect from forgery' do
+ ActionController::Base.allow_forgery_protection = true
+ post 'success'
+ expect(response).to have_http_status(:success)
+ end
+ end
+
+ describe 'Error handling' do
+ ERRORS_WITH_CODES = {
+ ActiveRecord::RecordInvalid => 422,
+ Mastodon::ValidationError => 422,
+ ActiveRecord::RecordNotFound => 404,
+ Goldfinger::Error => 422,
+ HTTP::Error => 503,
+ OpenSSL::SSL::SSLError => 503,
+ Mastodon::NotPermittedError => 403,
+ }
+
+ before do
+ routes.draw { get 'error' => 'api/base#error' }
+ end
+
+ ERRORS_WITH_CODES.each do |error, code|
+ it "Handles error class of #{error}" do
+ expect(FakeService).to receive(:new).and_raise(error)
+
+ get 'error'
+ expect(response).to have_http_status(code)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/oembed_controller_spec.rb b/spec/controllers/api/oembed_controller_spec.rb
index 511cdb4639cb80b18c9edf2fe529a6f5e0d2432f..43631a7e5a0b23abac5fdd222c35db65e8f603e4 100644
--- a/spec/controllers/api/oembed_controller_spec.rb
+++ b/spec/controllers/api/oembed_controller_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
RSpec.describe Api::OEmbedController, type: :controller do
+ render_views
+
let(:alice) { Fabricate(:account, username: 'alice') }
let(:status) { Fabricate(:status, text: 'Hello world', account: alice) }
diff --git a/spec/controllers/api/salmon_controller_spec.rb b/spec/controllers/api/salmon_controller_spec.rb
index 6897caeebbeb425014bdc992175104df1efe082a..3e4686200b6e31a0e5fef535dc63143553868de6 100644
--- a/spec/controllers/api/salmon_controller_spec.rb
+++ b/spec/controllers/api/salmon_controller_spec.rb
@@ -13,29 +13,42 @@ RSpec.describe Api::SalmonController, type: :controller do
end
describe 'POST #update' do
- before do
- request.env['RAW_POST_DATA'] = File.read(File.join(Rails.root, 'spec', 'fixtures', 'salmon', 'mention.xml'))
- post :update, params: { id: account.id }
+ context 'with valid post data' do
+ before do
+ request.env['RAW_POST_DATA'] = File.read(File.join(Rails.root, 'spec', 'fixtures', 'salmon', 'mention.xml'))
+ post :update, params: { id: account.id }
+ end
+
+ it 'contains XML in the request body' do
+ expect(request.body.read).to be_a String
+ end
+
+ it 'returns http success' do
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'creates remote account' do
+ expect(Account.find_by(username: 'gargron', domain: 'quitter.no')).to_not be_nil
+ end
+
+ it 'creates status' do
+ expect(Status.find_by(uri: 'tag:quitter.no,2016-03-20:noticeId=1276923:objectType=note')).to_not be_nil
+ end
+
+ it 'creates mention for target account' do
+ expect(account.mentions.count).to eq 1
+ end
end
- it 'contains XML in the request body' do
- expect(request.body.read).to be_a String
- end
-
- it 'returns http success' do
- expect(response).to have_http_status(:success)
- end
-
- it 'creates remote account' do
- expect(Account.find_by(username: 'gargron', domain: 'quitter.no')).to_not be_nil
- end
-
- it 'creates status' do
- expect(Status.find_by(uri: 'tag:quitter.no,2016-03-20:noticeId=1276923:objectType=note')).to_not be_nil
- end
+ context 'with invalid post data' do
+ before do
+ request.env['RAW_POST_DATA'] = ''
+ post :update, params: { id: account.id }
+ end
- it 'creates mention for target account' do
- expect(account.mentions.count).to eq 1
+ it 'returns http success' do
+ expect(response).to have_http_status(202)
+ end
end
end
end
diff --git a/spec/controllers/api/subscriptions_controller_spec.rb b/spec/controllers/api/subscriptions_controller_spec.rb
index 97e36420d6a4f969bfdf445848252b6d474c0686..76f9740ca5c0078a0dfae9e757163ee01e6e13c8 100644
--- a/spec/controllers/api/subscriptions_controller_spec.rb
+++ b/spec/controllers/api/subscriptions_controller_spec.rb
@@ -6,16 +6,29 @@ RSpec.describe Api::SubscriptionsController, type: :controller do
let(:account) { Fabricate(:account, username: 'gargron', domain: 'quitter.no', remote_url: 'topic_url', secret: 'abc') }
describe 'GET #show' do
- before do
- get :show, params: { :id => account.id, 'hub.topic' => 'topic_url', 'hub.challenge' => '456', 'hub.lease_seconds' => "#{86400 * 30}" }
- end
+ context 'with valid subscription' do
+ before do
+ get :show, params: { :id => account.id, 'hub.topic' => 'topic_url', 'hub.challenge' => '456', 'hub.lease_seconds' => "#{86400 * 30}" }
+ end
- it 'returns http success' do
- expect(response).to have_http_status(:success)
+ it 'returns http success' do
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'echoes back the challenge' do
+ expect(response.body).to match '456'
+ end
end
- it 'echoes back the challenge' do
- expect(response.body).to match '456'
+ context 'with invalid subscription' do
+ before do
+ expect_any_instance_of(Account).to receive_message_chain(:subscription, :valid?).and_return(false)
+ get :show, params: { :id => account.id }
+ end
+
+ it 'returns http success' do
+ expect(response).to have_http_status(:missing)
+ end
end
end
diff --git a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..89dbca111900a13fc9966430846053bac0ccaaeb
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb
@@ -0,0 +1,55 @@
+require 'rails_helper'
+
+describe Api::V1::Accounts::CredentialsController do
+ render_views
+
+ let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+ let(:token) { double acceptable?: true, resource_owner_id: user.id }
+
+ before do
+ allow(controller).to receive(:doorkeeper_token) { token }
+ end
+
+ describe 'GET #show' do
+ it 'returns http success' do
+ get :show
+ expect(response).to have_http_status(:success)
+ end
+ end
+
+ describe 'PATCH #update' do
+ describe 'with valid data' do
+ before do
+ patch :update, params: {
+ display_name: "Alice Isn't Dead",
+ note: "Hi!\n\nToot toot!",
+ avatar: fixture_file_upload('files/avatar.gif', 'image/gif'),
+ header: fixture_file_upload('files/attachment.jpg', 'image/jpeg'),
+ }
+ end
+
+ it 'returns http success' do
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'updates account info' do
+ user.account.reload
+
+ expect(user.account.display_name).to eq("Alice Isn't Dead")
+ expect(user.account.note).to eq("Hi!\n\nToot toot!")
+ expect(user.account.avatar).to exist
+ expect(user.account.header).to exist
+ end
+ end
+
+ describe 'with invalid data' do
+ before do
+ patch :update, params: { note: 'This is too long. ' * 10 }
+ end
+
+ it 'returns http unprocessable entity' do
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..171852c755d557546c76b7670cd557729431618c
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb
@@ -0,0 +1,21 @@
+require 'rails_helper'
+
+describe Api::V1::Accounts::FollowerAccountsController do
+ render_views
+
+ let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+ let(:token) { double acceptable?: true, resource_owner_id: user.id }
+
+ before do
+ Fabricate(:follow, target_account: user.account)
+ allow(controller).to receive(:doorkeeper_token) { token }
+ end
+
+ describe 'GET #index' do
+ it 'returns http success' do
+ get :index, params: { account_id: user.account.id, limit: 1 }
+
+ expect(response).to have_http_status(:success)
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a4cad9163e7c3922f451848798d04d7c6c330ba8
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb
@@ -0,0 +1,21 @@
+require 'rails_helper'
+
+describe Api::V1::Accounts::FollowingAccountsController do
+ render_views
+
+ let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+ let(:token) { double acceptable?: true, resource_owner_id: user.id }
+
+ before do
+ Fabricate(:follow, account: user.account)
+ allow(controller).to receive(:doorkeeper_token) { token }
+ end
+
+ describe 'GET #index' do
+ it 'returns http success' do
+ get :index, params: { account_id: user.account.id, limit: 1 }
+
+ expect(response).to have_http_status(:success)
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e281afcb92286ad71bef40a79e8ee0622a16e511
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
@@ -0,0 +1,69 @@
+require 'rails_helper'
+
+describe Api::V1::Accounts::RelationshipsController do
+ render_views
+
+ let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+ let(:token) { double acceptable?: true, resource_owner_id: user.id }
+
+ before do
+ allow(controller).to receive(:doorkeeper_token) { token }
+ end
+
+ describe 'GET #index' do
+ let(:simon) { Fabricate(:user, email: 'simon@example.com', account: Fabricate(:account, username: 'simon')).account }
+ let(:lewis) { Fabricate(:user, email: 'lewis@example.com', account: Fabricate(:account, username: 'lewis')).account }
+
+ before do
+ user.account.follow!(simon)
+ lewis.follow!(user.account)
+ end
+
+ context 'provided only one ID' do
+ before do
+ get :index, params: { id: simon.id }
+ end
+
+ it 'returns http success' do
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'returns JSON with correct data' do
+ json = body_as_json
+
+ expect(json).to be_a Enumerable
+ expect(json.first[:following]).to be true
+ expect(json.first[:followed_by]).to be false
+ end
+ end
+
+ context 'provided multiple IDs' do
+ before do
+ get :index, params: { id: [simon.id, lewis.id] }
+ end
+
+ it 'returns http success' do
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'returns JSON with correct data' do
+ json = body_as_json
+
+ expect(json).to be_a Enumerable
+ expect(json.first[:id]).to be simon.id
+ expect(json.first[:following]).to be true
+ expect(json.first[:followed_by]).to be false
+ expect(json.first[:muting]).to be false
+ expect(json.first[:requested]).to be false
+ expect(json.first[:domain_blocking]).to be false
+
+ expect(json.second[:id]).to be lewis.id
+ expect(json.second[:following]).to be false
+ expect(json.second[:followed_by]).to be true
+ expect(json.second[:muting]).to be false
+ expect(json.second[:requested]).to be false
+ expect(json.second[:domain_blocking]).to be false
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/accounts/search_controller_spec.rb b/spec/controllers/api/v1/accounts/search_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..40c82437dddecf3b448dca4250216578389ced20
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/search_controller_spec.rb
@@ -0,0 +1,20 @@
+require 'rails_helper'
+
+RSpec.describe Api::V1::Accounts::SearchController, type: :controller do
+ render_views
+
+ let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+ let(:token) { double acceptable?: true, resource_owner_id: user.id }
+
+ before do
+ allow(controller).to receive(:doorkeeper_token) { token }
+ end
+
+ describe 'GET #show' do
+ it 'returns http success' do
+ get :show, params: { q: 'query' }
+
+ expect(response).to have_http_status(:success)
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..55cb5bcc24ce5ee1f88ef313748dc362496d2481
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+describe Api::V1::Accounts::StatusesController do
+ render_views
+
+ let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+ let(:token) { double acceptable?: true, resource_owner_id: user.id }
+
+ before do
+ allow(controller).to receive(:doorkeeper_token) { token }
+ Fabricate(:status, account: user.account)
+ end
+
+ describe 'GET #index' do
+ it 'returns http success' do
+ get :index, params: { account_id: user.account.id, limit: 1 }
+
+ expect(response).to have_http_status(:success)
+ expect(response.headers['Link'].links.size).to eq(2)
+ end
+ end
+
+ describe 'GET #index with only media' do
+ it 'returns http success' do
+ get :index, params: { account_id: user.account.id, only_media: true }
+
+ expect(response).to have_http_status(:success)
+ end
+ end
+
+ describe 'GET #index with exclude replies' do
+ it 'returns http success' do
+ get :index, params: { account_id: user.account.id, exclude_replies: true }
+
+ expect(response).to have_http_status(:success)
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
index dec81aff5231b8db1dc8f63e484634be180a4a31..216a9cb3b602dbd7630db6a5ae2f6987509fa59f 100644
--- a/spec/controllers/api/v1/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts_controller_spec.rb
@@ -17,78 +17,6 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
end
end
- describe 'GET #search' do
- it 'returns http success' do
- get :search, params: { q: 'query' }
-
- expect(response).to have_http_status(:success)
- end
- end
-
- describe 'GET #verify_credentials' do
- it 'returns http success' do
- get :verify_credentials
- expect(response).to have_http_status(:success)
- end
- end
-
- describe 'PATCH #update_credentials' do
- describe 'with valid data' do
- before do
- patch :update_credentials, params: {
- display_name: "Alice Isn't Dead",
- note: "Hi!\n\nToot toot!",
- avatar: fixture_file_upload('files/avatar.gif', 'image/gif'),
- header: fixture_file_upload('files/attachment.jpg', 'image/jpeg'),
- }
- end
-
- it 'returns http success' do
- expect(response).to have_http_status(:success)
- end
-
- it 'updates account info' do
- user.account.reload
-
- expect(user.account.display_name).to eq("Alice Isn't Dead")
- expect(user.account.note).to eq("Hi!\n\nToot toot!")
- expect(user.account.avatar).to exist
- expect(user.account.header).to exist
- end
- end
-
- describe 'with invalid data' do
- before do
- patch :update_credentials, params: { note: 'This is too long. ' * 10 }
- end
-
- it 'returns http unprocessable entity' do
- expect(response).to have_http_status(:unprocessable_entity)
- end
- end
- end
-
- describe 'GET #statuses' do
- it 'returns http success' do
- get :statuses, params: { id: user.account.id }
- expect(response).to have_http_status(:success)
- end
- end
-
- describe 'GET #followers' do
- it 'returns http success' do
- get :followers, params: { id: user.account.id }
- expect(response).to have_http_status(:success)
- end
- end
-
- describe 'GET #following' do
- it 'returns http success' do
- get :following, params: { id: user.account.id }
- expect(response).to have_http_status(:success)
- end
- end
-
describe 'POST #follow' do
let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
@@ -197,61 +125,4 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
expect(user.account.muting?(other_account)).to be false
end
end
-
- describe 'GET #relationships' do
- let(:simon) { Fabricate(:user, email: 'simon@example.com', account: Fabricate(:account, username: 'simon')).account }
- let(:lewis) { Fabricate(:user, email: 'lewis@example.com', account: Fabricate(:account, username: 'lewis')).account }
-
- before do
- user.account.follow!(simon)
- lewis.follow!(user.account)
- end
-
- context 'provided only one ID' do
- before do
- get :relationships, params: { id: simon.id }
- end
-
- it 'returns http success' do
- expect(response).to have_http_status(:success)
- end
-
- it 'returns JSON with correct data' do
- json = body_as_json
-
- expect(json).to be_a Enumerable
- expect(json.first[:following]).to be true
- expect(json.first[:followed_by]).to be false
- end
- end
-
- context 'provided multiple IDs' do
- before do
- get :relationships, params: { id: [simon.id, lewis.id] }
- end
-
- it 'returns http success' do
- expect(response).to have_http_status(:success)
- end
-
- it 'returns JSON with correct data' do
- json = body_as_json
-
- expect(json).to be_a Enumerable
- expect(json.first[:id]).to be simon.id
- expect(json.first[:following]).to be true
- expect(json.first[:followed_by]).to be false
- expect(json.first[:muting]).to be false
- expect(json.first[:requested]).to be false
- expect(json.first[:domain_blocking]).to be false
-
- expect(json.second[:id]).to be lewis.id
- expect(json.second[:following]).to be false
- expect(json.second[:followed_by]).to be true
- expect(json.second[:muting]).to be false
- expect(json.second[:requested]).to be false
- expect(json.second[:domain_blocking]).to be false
- end
- end
- end
end
diff --git a/spec/controllers/api/v1/blocks_controller_spec.rb b/spec/controllers/api/v1/blocks_controller_spec.rb
index ca20a2d172f9cde09aa61f00877eddaa7e77c7bb..4fd968b27c88495bb1e2c39b78932be628f9c794 100644
--- a/spec/controllers/api/v1/blocks_controller_spec.rb
+++ b/spec/controllers/api/v1/blocks_controller_spec.rb
@@ -7,12 +7,14 @@ RSpec.describe Api::V1::BlocksController, type: :controller do
let(:token) { double acceptable?: true, resource_owner_id: user.id }
before do
+ Fabricate(:block, account: user.account)
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do
it 'returns http success' do
- get :index
+ get :index, params: { limit: 1 }
+
expect(response).to have_http_status(:success)
end
end
diff --git a/spec/controllers/api/v1/domain_blocks_controller_spec.rb b/spec/controllers/api/v1/domain_blocks_controller_spec.rb
index c3331744daa7e9c73e22da69cee5fdc0ea09bb17..ff5c5f3302d1c991a84f1d7b97d354425ad9652f 100644
--- a/spec/controllers/api/v1/domain_blocks_controller_spec.rb
+++ b/spec/controllers/api/v1/domain_blocks_controller_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Api::V1::DomainBlocksController, type: :controller do
describe 'GET #show' do
before do
- get :show
+ get :show, params: { limit: 1 }
end
it 'returns http success' do
diff --git a/spec/controllers/api/v1/favourites_controller_spec.rb b/spec/controllers/api/v1/favourites_controller_spec.rb
index a6e9963e5231153415cb3c8a4697ac22652b4508..062e91adcc6f9b470a2c3c15c8621d8637c61487 100644
--- a/spec/controllers/api/v1/favourites_controller_spec.rb
+++ b/spec/controllers/api/v1/favourites_controller_spec.rb
@@ -7,12 +7,14 @@ RSpec.describe Api::V1::FavouritesController, type: :controller do
let(:token) { double acceptable?: true, resource_owner_id: user.id }
before do
+ Fabricate(:favourite, account: user.account)
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do
it 'returns http success' do
- get :index
+ get :index, params: { limit: 1 }
+
expect(response).to have_http_status(:success)
end
end
diff --git a/spec/controllers/api/v1/follow_requests_controller_spec.rb b/spec/controllers/api/v1/follow_requests_controller_spec.rb
index a90d2d2902a3fd15a7c5c1a0b87a1d70b72468f1..d455a0255d3e266b5f8a51ad07618f0a28014bfe 100644
--- a/spec/controllers/api/v1/follow_requests_controller_spec.rb
+++ b/spec/controllers/api/v1/follow_requests_controller_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do
describe 'GET #index' do
before do
- get :index
+ get :index, params: { limit: 1 }
end
it 'returns http success' do
diff --git a/spec/controllers/api/v1/media_controller_spec.rb b/spec/controllers/api/v1/media_controller_spec.rb
index b1d9798eac20282412b57c5ed774a8f9a69792a8..00dcac95dcca59b8cafc8f97339d0cc11da3914c 100644
--- a/spec/controllers/api/v1/media_controller_spec.rb
+++ b/spec/controllers/api/v1/media_controller_spec.rb
@@ -11,6 +11,30 @@ RSpec.describe Api::V1::MediaController, type: :controller do
end
describe 'POST #create' do
+ describe 'with paperclip errors' do
+ context 'when imagemagick cant identify the file type' do
+ before do
+ expect_any_instance_of(Account).to receive_message_chain(:media_attachments, :create!).and_raise(Paperclip::Errors::NotIdentifiedByImageMagickError)
+ post :create, params: { file: fixture_file_upload('files/attachment.jpg', 'image/jpeg') }
+ end
+
+ it 'returns http 422' do
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+
+ context 'when there is a generic error' do
+ before do
+ expect_any_instance_of(Account).to receive_message_chain(:media_attachments, :create!).and_raise(Paperclip::Error)
+ post :create, params: { file: fixture_file_upload('files/attachment.jpg', 'image/jpeg') }
+ end
+
+ it 'returns http 422' do
+ expect(response).to have_http_status(:error)
+ end
+ end
+ end
+
context 'image/jpeg' do
before do
post :create, params: { file: fixture_file_upload('files/attachment.jpg', 'image/jpeg') }
diff --git a/spec/controllers/api/v1/mutes_controller_spec.rb b/spec/controllers/api/v1/mutes_controller_spec.rb
index be8a5e7dd2b7ea7bdb894bdc619abf6f384c82d3..85aad43841441c13d846527faa98d7535b22ef00 100644
--- a/spec/controllers/api/v1/mutes_controller_spec.rb
+++ b/spec/controllers/api/v1/mutes_controller_spec.rb
@@ -7,12 +7,14 @@ RSpec.describe Api::V1::MutesController, type: :controller do
let(:token) { double acceptable?: true, resource_owner_id: user.id }
before do
+ Fabricate(:mute, account: user.account)
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do
it 'returns http success' do
- get :index
+ get :index, params: { limit: 1 }
+
expect(response).to have_http_status(:success)
end
end
diff --git a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1acb990a0088cd1ab09415ddd98f942fae5dcf7b
--- /dev/null
+++ b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
@@ -0,0 +1,66 @@
+require 'rails_helper'
+
+RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :controller do
+ render_views
+
+ let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+ let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
+ let(:token) { double acceptable?: true, resource_owner_id: user.id, application: app }
+
+ context 'with an oauth token' do
+ before do
+ allow(controller).to receive(:doorkeeper_token) { token }
+ end
+
+ describe 'GET #index' do
+ let(:status) { Fabricate(:status, account: user.account) }
+
+ before do
+ Fabricate(:favourite, status: status)
+ end
+
+ it 'returns http success' do
+ get :index, params: { status_id: status.id, limit: 1 }
+ expect(response).to have_http_status(:success)
+ expect(response.headers['Link'].links.size).to eq(2)
+ end
+ end
+
+ end
+
+ context 'without an oauth token' do
+ before do
+ allow(controller).to receive(:doorkeeper_token) { nil }
+ end
+
+ context 'with a private status' do
+ let(:status) { Fabricate(:status, account: user.account, visibility: :private) }
+
+ describe 'GET #index' do
+ before do
+ Fabricate(:favourite, status: status)
+ end
+
+ it 'returns http unautharized' do
+ get :index, params: { status_id: status.id }
+ expect(response).to have_http_status(:missing)
+ end
+ end
+ end
+
+ context 'with a public status' do
+ let(:status) { Fabricate(:status, account: user.account, visibility: :public) }
+
+ describe 'GET #index' do
+ before do
+ Fabricate(:favourite, status: status)
+ end
+
+ it 'returns http success' do
+ get :index, params: { status_id: status.id }
+ expect(response).to have_http_status(:success)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/statuses/favourites_controller_spec.rb b/spec/controllers/api/v1/statuses/favourites_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eb77072d259f7238957acb5c66af5128aba8853f
--- /dev/null
+++ b/spec/controllers/api/v1/statuses/favourites_controller_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Statuses::FavouritesController do
+ render_views
+
+ let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+ let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
+ let(:token) { double acceptable?: true, resource_owner_id: user.id, application: app }
+
+ context 'with an oauth token' do
+ before do
+ allow(controller).to receive(:doorkeeper_token) { token }
+ end
+
+ describe 'POST #create' do
+ let(:status) { Fabricate(:status, account: user.account) }
+
+ before do
+ post :create, params: { status_id: status.id }
+ end
+
+ it 'returns http success' do
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'updates the favourites count' do
+ expect(status.favourites.count).to eq 1
+ end
+
+ it 'updates the favourited attribute' do
+ expect(user.account.favourited?(status)).to be true
+ end
+
+ it 'return json with updated attributes' do
+ hash_body = body_as_json
+
+ expect(hash_body[:id]).to eq status.id
+ expect(hash_body[:favourites_count]).to eq 1
+ expect(hash_body[:favourited]).to be true
+ end
+ end
+
+ describe 'POST #destroy' do
+ let(:status) { Fabricate(:status, account: user.account) }
+
+ before do
+ FavouriteService.new.call(user.account, status)
+ post :destroy, params: { status_id: status.id }
+ end
+
+ it 'returns http success' do
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'updates the favourites count' do
+ expect(status.favourites.count).to eq 0
+ end
+
+ it 'updates the favourited attribute' do
+ expect(user.account.favourited?(status)).to be false
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/statuses/mutes_controller_spec.rb b/spec/controllers/api/v1/statuses/mutes_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1f8c29e3d8b807d73bc054fb254beef1d12ac899
--- /dev/null
+++ b/spec/controllers/api/v1/statuses/mutes_controller_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Statuses::MutesController do
+ render_views
+
+ let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+ let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
+ let(:token) { double acceptable?: true, resource_owner_id: user.id, application: app }
+
+ context 'with an oauth token' do
+ before do
+ allow(controller).to receive(:doorkeeper_token) { token }
+ end
+
+ describe 'POST #create' do
+ let(:status) { Fabricate(:status, account: user.account) }
+
+ before do
+ post :create, params: { status_id: status.id }
+ end
+
+ it 'returns http success' do
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'creates a conversation mute' do
+ expect(ConversationMute.find_by(account: user.account, conversation_id: status.conversation_id)).to_not be_nil
+ end
+ end
+
+ describe 'POST #destroy' do
+ let(:status) { Fabricate(:status, account: user.account) }
+
+ before do
+ user.account.mute_conversation!(status.conversation)
+ post :destroy, params: { status_id: status.id }
+ end
+
+ it 'returns http success' do
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'destroys the conversation mute' do
+ expect(ConversationMute.find_by(account: user.account, conversation_id: status.conversation_id)).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c5624023f183621537885ce4a03ba937a2269d7c
--- /dev/null
+++ b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
@@ -0,0 +1,65 @@
+require 'rails_helper'
+
+RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controller do
+ render_views
+
+ let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+ let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
+ let(:token) { double acceptable?: true, resource_owner_id: user.id, application: app }
+
+ context 'with an oauth token' do
+ before do
+ allow(controller).to receive(:doorkeeper_token) { token }
+ end
+
+ describe 'GET #index' do
+ let(:status) { Fabricate(:status, account: user.account) }
+
+ before do
+ Fabricate(:status, reblog_of_id: status.id)
+ end
+
+ it 'returns http success' do
+ get :index, params: { status_id: status.id, limit: 1 }
+ expect(response).to have_http_status(:success)
+ expect(response.headers['Link'].links.size).to eq(2)
+ end
+ end
+ end
+
+ context 'without an oauth token' do
+ before do
+ allow(controller).to receive(:doorkeeper_token) { nil }
+ end
+
+ context 'with a private status' do
+ let(:status) { Fabricate(:status, account: user.account, visibility: :private) }
+
+ describe 'GET #index' do
+ before do
+ Fabricate(:status, reblog_of_id: status.id)
+ end
+
+ it 'returns http unautharized' do
+ get :index, params: { status_id: status.id }
+ expect(response).to have_http_status(:missing)
+ end
+ end
+ end
+
+ context 'with a public status' do
+ let(:status) { Fabricate(:status, account: user.account, visibility: :public) }
+
+ describe 'GET #index' do
+ before do
+ Fabricate(:status, reblog_of_id: status.id)
+ end
+
+ it 'returns http success' do
+ get :index, params: { status_id: status.id }
+ expect(response).to have_http_status(:success)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb b/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..36c323736bb8b2576eb78877bf85770634deef4f
--- /dev/null
+++ b/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Statuses::ReblogsController do
+ render_views
+
+ let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+ let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
+ let(:token) { double acceptable?: true, resource_owner_id: user.id, application: app }
+
+ context 'with an oauth token' do
+ before do
+ allow(controller).to receive(:doorkeeper_token) { token }
+ end
+
+ describe 'POST #create' do
+ let(:status) { Fabricate(:status, account: user.account) }
+
+ before do
+ post :create, params: { status_id: status.id }
+ end
+
+ it 'returns http success' do
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'updates the reblogs count' do
+ expect(status.reblogs.count).to eq 1
+ end
+
+ it 'updates the reblogged attribute' do
+ expect(user.account.reblogged?(status)).to be true
+ end
+
+ it 'return json with updated attributes' do
+ hash_body = body_as_json
+
+ expect(hash_body[:reblog][:id]).to eq status.id
+ expect(hash_body[:reblog][:reblogs_count]).to eq 1
+ expect(hash_body[:reblog][:reblogged]).to be true
+ end
+ end
+
+ describe 'POST #destroy' do
+ let(:status) { Fabricate(:status, account: user.account) }
+
+ before do
+ ReblogService.new.call(user.account, status)
+ post :destroy, params: { status_id: status.id }
+ end
+
+ it 'returns http success' do
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'updates the reblogs count' do
+ expect(status.reblogs.count).to eq 0
+ end
+
+ it 'updates the reblogged attribute' do
+ expect(user.account.reblogged?(status)).to be false
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb
index ac3b2dc7d6f12697c2bfae1f368fe0ab8cb06598..3d65180ab3a73a67aa74be61c50b7f1ee8025aca 100644
--- a/spec/controllers/api/v1/statuses_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses_controller_spec.rb
@@ -34,32 +34,6 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
end
end
- describe 'GET #reblogged_by' do
- let(:status) { Fabricate(:status, account: user.account) }
-
- before do
- post :reblog, params: { id: status.id }
- end
-
- it 'returns http success' do
- get :reblogged_by, params: { id: status.id }
- expect(response).to have_http_status(:success)
- end
- end
-
- describe 'GET #favourited_by' do
- let(:status) { Fabricate(:status, account: user.account) }
-
- before do
- post :favourite, params: { id: status.id }
- end
-
- it 'returns http success' do
- get :favourited_by, params: { id: status.id }
- expect(response).to have_http_status(:success)
- end
- end
-
describe 'POST #create' do
before do
post :create, params: { status: 'Hello world' }
@@ -85,137 +59,6 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
expect(Status.find_by(id: status.id)).to be nil
end
end
-
- describe 'POST #reblog' do
- let(:status) { Fabricate(:status, account: user.account) }
-
- before do
- post :reblog, params: { id: status.id }
- end
-
- it 'returns http success' do
- expect(response).to have_http_status(:success)
- end
-
- it 'updates the reblogs count' do
- expect(status.reblogs.count).to eq 1
- end
-
- it 'updates the reblogged attribute' do
- expect(user.account.reblogged?(status)).to be true
- end
-
- it 'return json with updated attributes' do
- hash_body = body_as_json
-
- expect(hash_body[:reblog][:id]).to eq status.id
- expect(hash_body[:reblog][:reblogs_count]).to eq 1
- expect(hash_body[:reblog][:reblogged]).to be true
- end
- end
-
- describe 'POST #unreblog' do
- let(:status) { Fabricate(:status, account: user.account) }
-
- before do
- post :reblog, params: { id: status.id }
- post :unreblog, params: { id: status.id }
- end
-
- it 'returns http success' do
- expect(response).to have_http_status(:success)
- end
-
- it 'updates the reblogs count' do
- expect(status.reblogs.count).to eq 0
- end
-
- it 'updates the reblogged attribute' do
- expect(user.account.reblogged?(status)).to be false
- end
- end
-
- describe 'POST #favourite' do
- let(:status) { Fabricate(:status, account: user.account) }
-
- before do
- post :favourite, params: { id: status.id }
- end
-
- it 'returns http success' do
- expect(response).to have_http_status(:success)
- end
-
- it 'updates the favourites count' do
- expect(status.favourites.count).to eq 1
- end
-
- it 'updates the favourited attribute' do
- expect(user.account.favourited?(status)).to be true
- end
-
- it 'return json with updated attributes' do
- hash_body = body_as_json
-
- expect(hash_body[:id]).to eq status.id
- expect(hash_body[:favourites_count]).to eq 1
- expect(hash_body[:favourited]).to be true
- end
- end
-
- describe 'POST #unfavourite' do
- let(:status) { Fabricate(:status, account: user.account) }
-
- before do
- post :favourite, params: { id: status.id }
- post :unfavourite, params: { id: status.id }
- end
-
- it 'returns http success' do
- expect(response).to have_http_status(:success)
- end
-
- it 'updates the favourites count' do
- expect(status.favourites.count).to eq 0
- end
-
- it 'updates the favourited attribute' do
- expect(user.account.favourited?(status)).to be false
- end
- end
-
- describe 'POST #mute' do
- let(:status) { Fabricate(:status, account: user.account) }
-
- before do
- post :mute, params: { id: status.id }
- end
-
- it 'returns http success' do
- expect(response).to have_http_status(:success)
- end
-
- it 'creates a conversation mute' do
- expect(ConversationMute.find_by(account: user.account, conversation_id: status.conversation_id)).to_not be_nil
- end
- end
-
- describe 'POST #unmute' do
- let(:status) { Fabricate(:status, account: user.account) }
-
- before do
- post :mute, params: { id: status.id }
- post :unmute, params: { id: status.id }
- end
-
- it 'returns http success' do
- expect(response).to have_http_status(:success)
- end
-
- it 'destroys the conversation mute' do
- expect(ConversationMute.find_by(account: user.account, conversation_id: status.conversation_id)).to be_nil
- end
- end
end
context 'without an oauth token' do
@@ -250,28 +93,6 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
expect(response).to have_http_status(:missing)
end
end
-
- describe 'GET #reblogged_by' do
- before do
- post :reblog, params: { id: status.id }
- end
-
- it 'returns http unautharized' do
- get :reblogged_by, params: { id: status.id }
- expect(response).to have_http_status(:missing)
- end
- end
-
- describe 'GET #favourited_by' do
- before do
- post :favourite, params: { id: status.id }
- end
-
- it 'returns http unautharized' do
- get :favourited_by, params: { id: status.id }
- expect(response).to have_http_status(:missing)
- end
- end
end
context 'with a public status' do
@@ -301,28 +122,6 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
expect(response).to have_http_status(:success)
end
end
-
- describe 'GET #reblogged_by' do
- before do
- post :reblog, params: { id: status.id }
- end
-
- it 'returns http success' do
- get :reblogged_by, params: { id: status.id }
- expect(response).to have_http_status(:success)
- end
- end
-
- describe 'GET #favourited_by' do
- before do
- post :favourite, params: { id: status.id }
- end
-
- it 'returns http success' do
- get :favourited_by, params: { id: status.id }
- expect(response).to have_http_status(:success)
- end
- end
end
end
end
diff --git a/spec/controllers/api/v1/streaming_controller_spec.rb b/spec/controllers/api/v1/streaming_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..daf2807e7f1953a553cae6cbf6d1e624e2b3cde7
--- /dev/null
+++ b/spec/controllers/api/v1/streaming_controller_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::StreamingController do
+ around(:each) do |example|
+ before = Rails.configuration.x.streaming_api_base_url
+ Rails.configuration.x.streaming_api_base_url = Rails.configuration.x.web_domain
+ example.run
+ Rails.configuration.x.streaming_api_base_url = before
+ end
+
+ before(:each) do
+ request.headers.merge! Host: Rails.configuration.x.web_domain
+ end
+
+ context 'with streaming api on same host' do
+ describe 'GET #index' do
+ it 'raises ActiveRecord::RecordNotFound' do
+ get :index
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'with streaming api on different host' do
+ before(:each) do
+ Rails.configuration.x.streaming_api_base_url = 'wss://streaming-' + Rails.configuration.x.web_domain
+ @streaming_host = URI.parse(Rails.configuration.x.streaming_api_base_url).host
+ end
+
+ describe 'GET #index' do
+ it 'redirects to streaming host' do
+ get :index, params: {access_token: 'deadbeef', stream: 'public'}
+ expect(response).to have_http_status(301)
+ request_uri = URI.parse(request.url)
+ redirect_to_uri = URI.parse(response.location)
+ [:scheme, :path, :query, :fragment].each do |part|
+ expect(redirect_to_uri.send(part)).to eq(request_uri.send(part)), "redirect target #{part}"
+ end
+ expect(redirect_to_uri.host).to eq(@streaming_host), "redirect target host"
+ end
+ end
+ end
+
+end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 1b209feb50e31f82ade970195981304b90029be1..6a54a78c56654a7c848df29450953ebd28eace6c 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -37,20 +37,32 @@ describe ApplicationController, type: :controller do
end
end
+ context 'forgery' do
+ subject do
+ ActionController::Base.allow_forgery_protection = true
+ routes.draw { post 'success' => 'anonymous#success' }
+ post 'success'
+ end
+
+ include_examples 'respond_with_error', 422
+ end
+
it "does not force ssl if LOCAL_HTTPS is not 'true'" do
routes.draw { get 'success' => 'anonymous#success' }
- ENV['LOCAL_HTTPS'] = ''
- allow(Rails.env).to receive(:production?).and_return(true)
- get 'success'
- expect(response).to have_http_status(:success)
+ ClimateControl.modify LOCAL_HTTPS: '' do
+ allow(Rails.env).to receive(:production?).and_return(true)
+ get 'success'
+ expect(response).to have_http_status(:success)
+ end
end
it "forces ssl if LOCAL_HTTPS is 'true'" do
routes.draw { get 'success' => 'anonymous#success' }
- ENV['LOCAL_HTTPS'] = 'true'
- allow(Rails.env).to receive(:production?).and_return(true)
- get 'success'
- expect(response).to redirect_to('https://test.host/success')
+ ClimateControl.modify LOCAL_HTTPS: 'true' do
+ allow(Rails.env).to receive(:production?).and_return(true)
+ get 'success'
+ expect(response).to redirect_to('https://test.host/success')
+ end
end
describe 'helper_method :current_account' do
@@ -254,7 +266,7 @@ describe ApplicationController, type: :controller do
shared_examples 'receives :with_includes' do |fabricator, klass|
it 'uses raw if it is not an ActiveRecord::Relation' do
record = Fabricate(fabricator)
- expect(C.new.cache_collection([record], klass)).to match_array([record])
+ expect(C.new.cache_collection([record], klass)).to eq [record]
end
end
@@ -265,7 +277,7 @@ describe ApplicationController, type: :controller do
record = Fabricate(fabricator)
relation = klass.none
allow(relation).to receive(:cache_ids).and_return([record])
- expect(C.new.cache_collection(relation, klass)).to match_array([record])
+ expect(C.new.cache_collection(relation, klass)).to eq [record]
end
end
diff --git a/spec/controllers/auth/confirmations_controller_spec.rb b/spec/controllers/auth/confirmations_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cf7f91e528e9d3748d01fd1b608e7e9b3c0845ae
--- /dev/null
+++ b/spec/controllers/auth/confirmations_controller_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Auth::ConfirmationsController, type: :controller do
+ describe 'GET #new' do
+ it 'returns http success' do
+ @request.env['devise.mapping'] = Devise.mappings[:user]
+ get :new
+ expect(response).to have_http_status(:success)
+ end
+ end
+end
diff --git a/spec/controllers/auth/passwords_controller_spec.rb b/spec/controllers/auth/passwords_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..60b225efaed5065d278ee438fdff2c12e28cfa34
--- /dev/null
+++ b/spec/controllers/auth/passwords_controller_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Auth::PasswordsController, type: :controller do
+ describe 'GET #new' do
+ it 'returns http success' do
+ @request.env['devise.mapping'] = Devise.mappings[:user]
+ get :new
+ expect(response).to have_http_status(:success)
+ end
+ end
+end
diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb
index 525b8254d27c2167528977d4f2bfa6942c42f9c3..06fdbaabc317a4a4a80385646b775ecb1da82a6a 100644
--- a/spec/controllers/auth/sessions_controller_spec.rb
+++ b/spec/controllers/auth/sessions_controller_spec.rb
@@ -65,6 +65,20 @@ RSpec.describe Auth::SessionsController, type: :controller do
end
end
+ context 'using email with uppercase letters' do
+ before do
+ post :create, params: { user: { email: user.email.upcase, password: user.password } }
+ end
+
+ it 'redirects to home' do
+ expect(response).to redirect_to(root_path)
+ end
+
+ it 'logs the user in' do
+ expect(controller.current_user).to eq user
+ end
+ end
+
context 'using an invalid password' do
before do
post :create, params: { user: { email: user.email, password: 'wrongpw' } }
@@ -129,6 +143,26 @@ RSpec.describe Auth::SessionsController, type: :controller do
return codes
end
+ context 'using email and password' do
+ before do
+ post :create, params: { user: { email: user.email, password: user.password } }
+ end
+
+ it 'renders two factor authentication page' do
+ expect(controller).to render_template("two_factor")
+ end
+ end
+
+ context 'using upcase email and password' do
+ before do
+ post :create, params: { user: { email: user.email.upcase, password: user.password } }
+ end
+
+ it 'renders two factor authentication page' do
+ expect(controller).to render_template("two_factor")
+ end
+ end
+
context 'using a valid OTP' do
before do
post :create, params: { user: { otp_attempt: user.current_otp } }, session: { otp_user_id: user.id }
diff --git a/spec/controllers/authorize_follows_controller_spec.rb b/spec/controllers/authorize_follows_controller_spec.rb
index 1bc4eb6b776f3de67b0433384e9592803ddc591e..b801aa661748854aef28e8f2e54e52c91dbe81c0 100644
--- a/spec/controllers/authorize_follows_controller_spec.rb
+++ b/spec/controllers/authorize_follows_controller_spec.rb
@@ -30,7 +30,7 @@ describe AuthorizeFollowsController do
it 'renders error when account cant be found' do
service = double
- allow(FollowRemoteAccountService).to receive(:new).and_return(service)
+ allow(ResolveRemoteAccountService).to receive(:new).and_return(service)
allow(service).to receive(:call).with('missing@hostname').and_return(nil)
get :show, params: { acct: 'acct:missing@hostname' }
@@ -54,7 +54,7 @@ describe AuthorizeFollowsController do
it 'sets account from acct uri' do
account = Account.new
service = double
- allow(FollowRemoteAccountService).to receive(:new).and_return(service)
+ allow(ResolveRemoteAccountService).to receive(:new).and_return(service)
allow(service).to receive(:call).with('found@hostname').and_return(account)
get :show, params: { acct: 'acct:found@hostname' }
diff --git a/spec/controllers/concerns/account_controller_concern_spec.rb b/spec/controllers/concerns/account_controller_concern_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bdc181edcf5ddee624108c8256e1fcc8a73e07c0
--- /dev/null
+++ b/spec/controllers/concerns/account_controller_concern_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ApplicationController, type: :controller do
+ controller do
+ include AccountControllerConcern
+
+ def success
+ head 200
+ end
+ end
+
+ before do
+ routes.draw { get 'success' => 'anonymous#success' }
+ end
+
+ context 'when account is suspended' do
+ it 'returns http gone' do
+ account = Fabricate(:account, suspended: true)
+ get 'success', params: { account_username: account.username }
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is not suspended' do
+ it 'assigns @account' do
+ account = Fabricate(:account)
+ get 'success', params: { account_username: account.username }
+ expect(assigns(:account)).to eq account
+ end
+
+ it 'sets link headers' do
+ account = Fabricate(:account, username: 'username')
+ get 'success', params: { account_username: 'username' }
+ expect(response.headers['Link'].to_s).to eq '; rel="lrdd"; type="application/xrd+xml", ; rel="alternate"; type="application/atom+xml"'
+ end
+
+ it 'returns http success' do
+ account = Fabricate(:account)
+ get 'success', params: { account_username: account.username }
+ expect(response).to have_http_status(:success)
+ end
+ end
+end
diff --git a/spec/controllers/concerns/export_controller_concern_spec.rb b/spec/controllers/concerns/export_controller_concern_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9d6f782b99b33dc7d889f3ef5db9a73c8079d485
--- /dev/null
+++ b/spec/controllers/concerns/export_controller_concern_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ApplicationController, type: :controller do
+ controller do
+ include ExportControllerConcern
+ def index
+ send_export_file
+ end
+ def export_data
+ @export.account.username
+ end
+ end
+
+ describe 'GET #index' do
+ it 'returns a csv of the exported data when signed in' do
+ user = Fabricate(:user)
+ sign_in user
+ get :index, format: :csv
+
+ expect(response).to have_http_status(:success)
+ expect(response.content_type).to eq 'text/csv'
+ expect(response.headers['Content-Disposition']).to eq 'attachment; filename="anonymous.csv"'
+ expect(response.body).to eq user.account.username
+ end
+
+ it 'returns unauthorized when not signed in' do
+ get :index, format: :csv
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+end
diff --git a/spec/controllers/concerns/localized_spec.rb b/spec/controllers/concerns/localized_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c917ce85e7d17c4d5cc77da005d5cefdd2bd96c2
--- /dev/null
+++ b/spec/controllers/concerns/localized_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ApplicationController, type: :controller do
+ controller do
+ include Localized
+
+ def success
+ head 200
+ end
+ end
+
+ before do
+ routes.draw { get 'success' => 'anonymous#success' }
+ end
+
+ shared_examples 'default locale' do
+ context 'when DEFAULT_LOCALE environment variable is set' do
+ around do |example|
+ ClimateControl.modify 'DEFAULT_LOCALE' => 'ca', &example.method(:run)
+ I18n.locale = I18n.default_locale
+ end
+
+ it 'sets language specified by ENV if preferred' do
+ request.headers['Accept-Language'] = 'ca, fa'
+ get 'success'
+ expect(I18n.locale).to eq :ca
+ end
+
+ it 'sets available and preferred language if language specified by ENV is not preferred' do
+ request.headers['Accept-Language'] = 'ca-ES, fa'
+ get 'success'
+ expect(I18n.locale).to eq :fa
+ end
+
+ it 'sets language specified by ENV if it is compatible and none of available languages are preferred' do
+ request.headers['Accept-Language'] = 'ca-ES, fa-IR'
+ get 'success'
+ expect(I18n.locale).to eq :ca
+ end
+
+ it 'sets available and compatible langauge if language specified by ENV is not compatible none of available languages are preferred' do
+ request.headers['Accept-Language'] = 'fa-IR'
+ get 'success'
+ expect(I18n.locale).to eq :fa
+ end
+
+ it 'sets language specified by ENV if none of available languages are compatible' do
+ request.headers['Accept-Language'] = ''
+ get 'success'
+ expect(I18n.locale).to eq :ca
+ end
+ end
+
+ context 'when DEFAULT_LOCALE environment variable is not set' do
+ it 'sets default locale if none of available languages are compatible' do
+ request.headers['Accept-Language'] = ''
+ get 'success'
+ expect(I18n.locale).to eq :en
+ end
+ end
+ end
+
+ context 'user with valid locale has signed in' do
+ it "sets user's locale" do
+ user = Fabricate(:user, locale: :ca)
+
+ sign_in(user)
+ get 'success'
+
+ expect(I18n.locale).to eq :ca
+ end
+ end
+
+ context 'user with invalid locale has signed in' do
+ before do
+ user = Fabricate.build(:user, locale: :invalid)
+ user.save!(validate: false)
+ sign_in(user)
+ end
+
+ include_examples 'default locale'
+ end
+
+ context 'user has not signed in' do
+ include_examples 'default locale'
+ end
+end
diff --git a/spec/controllers/concerns/obfuscate_filename_spec.rb b/spec/controllers/concerns/obfuscate_filename_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e06d53c038557ef52ea530bddd159197218bfb6e
--- /dev/null
+++ b/spec/controllers/concerns/obfuscate_filename_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ApplicationController, type: :controller do
+ controller do
+ include ObfuscateFilename
+
+ obfuscate_filename :file
+
+ def file
+ render plain: params[:file]&.original_filename
+ end
+ end
+
+ before do
+ routes.draw { get 'file' => 'anonymous#file' }
+ end
+
+ it 'obfusticates filename if the given parameter is specified' do
+ file = fixture_file_upload('files/imports.txt', 'text/plain')
+ post 'file', params: { file: file }
+ expect(response.body).to end_with '.txt'
+ expect(response.body).not_to include 'imports'
+ end
+
+ it 'does nothing if the given parameter is not specified' do
+ post 'file'
+ end
+end
diff --git a/spec/controllers/concerns/rate_limit_headers_spec.rb b/spec/controllers/concerns/rate_limit_headers_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..719978dc23fc794d87b2265bdf081427da1fc15a
--- /dev/null
+++ b/spec/controllers/concerns/rate_limit_headers_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ApplicationController do
+ controller do
+ include RateLimitHeaders
+
+ def show
+ head 200
+ end
+ end
+
+ before do
+ routes.draw { get 'show' => 'anonymous#show' }
+ end
+
+ describe 'rate limiting' do
+ context 'throttling is off' do
+ before do
+ request.env['rack.attack.throttle_data'] = nil
+ end
+
+ it 'does not apply rate limiting' do
+ get 'show'
+
+ expect(response.headers['X-RateLimit-Limit']).to be_nil
+ expect(response.headers['X-RateLimit-Remaining']).to be_nil
+ expect(response.headers['X-RateLimit-Reset']).to be_nil
+ end
+ end
+
+ context 'throttling is on' do
+ let(:start_time) { DateTime.new(2017, 1, 1, 12, 0, 0).utc }
+
+ before do
+ request.env['rack.attack.throttle_data'] = { 'api' => { limit: 100, count: 20, period: 10 } }
+ travel_to start_time do
+ get 'show'
+ end
+ end
+
+ it 'applies rate limiting limit header' do
+ expect(response.headers['X-RateLimit-Limit']).to eq '100'
+ end
+
+ it 'applies rate limiting remaining header' do
+ expect(response.headers['X-RateLimit-Remaining']).to eq '80'
+ end
+
+ it 'applies rate limiting reset header' do
+ expect(response.headers['X-RateLimit-Reset']).to eq (start_time + 10.seconds).iso8601(6)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/media_controller_spec.rb b/spec/controllers/media_controller_spec.rb
index 2541df7344a2cdb99f1462959e7098854d40fb8b..5b03899e4cc9b3439112fd780605118f96d76295 100644
--- a/spec/controllers/media_controller_spec.rb
+++ b/spec/controllers/media_controller_spec.rb
@@ -30,7 +30,7 @@ describe MediaController do
it 'raises when not permitted to view' do
status = Fabricate(:status)
media_attachment = Fabricate(:media_attachment, status: status)
- allow_any_instance_of(Status).to receive(:permitted?).and_return(false)
+ allow_any_instance_of(MediaController).to receive(:authorize).and_raise(ActiveRecord::RecordNotFound)
get :show, params: { id: media_attachment.to_param }
expect(response).to have_http_status(:missing)
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb
index a5997bba3344fe5a91fe4b0011e327093ebc775e..5c2a62b48cb1fbe25a48928fe03d804f5163234f 100644
--- a/spec/controllers/oauth/authorizations_controller_spec.rb
+++ b/spec/controllers/oauth/authorizations_controller_spec.rb
@@ -7,21 +7,43 @@ RSpec.describe Oauth::AuthorizationsController, type: :controller do
let(:app) { Doorkeeper::Application.create!(name: 'test', redirect_uri: 'http://localhost/') }
- before do
- sign_in Fabricate(:user), scope: :user
- end
-
describe 'GET #new' do
- before do
+ subject do
get :new, params: { client_id: app.uid, response_type: 'code', redirect_uri: 'http://localhost/' }
end
- it 'returns http success' do
- expect(response).to have_http_status(:success)
+ shared_examples 'stores location for user' do
+ it 'stores location for user' do
+ subject
+ expect(controller.stored_location_for(:user)).to eq "/oauth/authorize?client_id=#{app.uid}&redirect_uri=http%3A%2F%2Flocalhost%2F&response_type=code"
+ end
+ end
+
+ context 'when signed in' do
+ before do
+ sign_in Fabricate(:user), scope: :user
+ end
+
+ it 'returns http success' do
+ subject
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'gives options to authorize and deny' do
+ subject
+ expect(response.body).to match(/Authorize/)
+ end
+
+ include_examples 'stores location for user'
end
- it 'gives options to authorize and deny' do
- expect(response.body).to match(/Authorize/)
+ context 'when not signed in' do
+ it 'redirects' do
+ subject
+ expect(response).to redirect_to '/auth/sign_in'
+ end
+
+ include_examples 'stores location for user'
end
end
end
diff --git a/spec/controllers/oauth/authorized_applications_controller_spec.rb b/spec/controllers/oauth/authorized_applications_controller_spec.rb
index f5d64bd90ba839bbd5a996b1c8329744cf5eb258..2a2b92283fa4d3261357d5f5fe7b57ca47877833 100644
--- a/spec/controllers/oauth/authorized_applications_controller_spec.rb
+++ b/spec/controllers/oauth/authorized_applications_controller_spec.rb
@@ -5,17 +5,38 @@ require 'rails_helper'
describe Oauth::AuthorizedApplicationsController do
render_views
- before do
- sign_in Fabricate(:user), scope: :user
- end
-
describe 'GET #index' do
- before do
+ subject do
get :index
end
- it 'returns http success' do
- expect(response).to have_http_status(:success)
+ shared_examples 'stores location for user' do
+ it 'stores location for user' do
+ subject
+ expect(controller.stored_location_for(:user)).to eq "/oauth/authorized_applications"
+ end
+ end
+
+ context 'when signed in' do
+ before do
+ sign_in Fabricate(:user), scope: :user
+ end
+
+ it 'returns http success' do
+ subject
+ expect(response).to have_http_status(:success)
+ end
+
+ include_examples 'stores location for user'
+ end
+
+ context 'when not signed in' do
+ it 'redirects' do
+ subject
+ expect(response).to redirect_to '/auth/sign_in'
+ end
+
+ include_examples 'stores location for user'
end
end
end
diff --git a/spec/controllers/settings/deletes_controller_spec.rb b/spec/controllers/settings/deletes_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9b55090df224a492103f1ac9d39c1378689c2882
--- /dev/null
+++ b/spec/controllers/settings/deletes_controller_spec.rb
@@ -0,0 +1,86 @@
+require 'rails_helper'
+
+describe Settings::DeletesController do
+ render_views
+
+ describe 'GET #show' do
+ context 'when signed in' do
+ let(:user) { Fabricate(:user) }
+
+ before do
+ sign_in user, scope: :user
+ end
+
+ it 'renders confirmation page' do
+ get :show
+ expect(response).to have_http_status(:success)
+ end
+ end
+
+ context 'when not signed in' do
+ it 'redirects' do
+ get :show
+ expect(response).to redirect_to '/auth/sign_in'
+ end
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ context 'when signed in' do
+ let(:user) { Fabricate(:user, password: 'petsmoldoggos') }
+
+ before do
+ sign_in user, scope: :user
+ end
+
+ context 'with correct password' do
+ before do
+ delete :destroy, params: { form_delete_confirmation: { password: 'petsmoldoggos' } }
+ end
+
+ it 'redirects to sign in page' do
+ expect(response).to redirect_to '/auth/sign_in'
+ end
+
+ it 'removes user record' do
+ expect(User.find_by(id: user.id)).to be_nil
+ end
+
+ it 'marks account as suspended' do
+ expect(user.account.reload).to be_suspended
+ end
+ end
+
+ context 'with incorrect password' do
+ before do
+ delete :destroy, params: { form_delete_confirmation: { password: 'blaze420' } }
+ end
+
+ it 'redirects back to confirmation page' do
+ expect(response).to redirect_to settings_delete_path
+ end
+ end
+ end
+
+ context 'when not signed in' do
+ it 'redirects' do
+ delete :destroy
+ expect(response).to redirect_to '/auth/sign_in'
+ end
+ end
+
+ context do
+ around do |example|
+ open_deletion = Setting.open_deletion
+ example.run
+ Setting.open_deletion = open_deletion
+ end
+
+ it 'redirects' do
+ Setting.open_deletion = false
+ delete :destroy
+ expect(response).to redirect_to root_path
+ end
+ end
+ end
+end
diff --git a/spec/controllers/settings/exports/blocked_accounts_controller_spec.rb b/spec/controllers/settings/exports/blocked_accounts_controller_spec.rb
index e79a6729a6b6edff6f74ce4fb618ff8bf6f0dd67..5ff41b7fcd8d38a787145811fc456ec752a6107e 100644
--- a/spec/controllers/settings/exports/blocked_accounts_controller_spec.rb
+++ b/spec/controllers/settings/exports/blocked_accounts_controller_spec.rb
@@ -3,17 +3,15 @@ require 'rails_helper'
describe Settings::Exports::BlockedAccountsController do
render_views
- before do
- sign_in Fabricate(:user), scope: :user
- end
-
describe 'GET #index' do
it 'returns a csv of the blocking accounts' do
+ user = Fabricate(:user)
+ user.account.block!(Fabricate(:account, username: 'username', domain: 'domain'))
+
+ sign_in user, scope: :user
get :index, format: :csv
- expect(response).to have_http_status(:success)
- expect(response.content_type).to eq 'text/csv'
- expect(response.headers['Content-Disposition']).to eq 'attachment; filename="blocked_accounts.csv"'
+ expect(response.body).to eq "username@domain\n"
end
end
end
diff --git a/spec/controllers/settings/exports/following_accounts_controller_spec.rb b/spec/controllers/settings/exports/following_accounts_controller_spec.rb
index 503455feaa4b9d504d6b371d5317be213c1ceb87..786769d2459e6d0d9f69af427a7ec46117c01615 100644
--- a/spec/controllers/settings/exports/following_accounts_controller_spec.rb
+++ b/spec/controllers/settings/exports/following_accounts_controller_spec.rb
@@ -3,17 +3,15 @@ require 'rails_helper'
describe Settings::Exports::FollowingAccountsController do
render_views
- before do
- sign_in Fabricate(:user), scope: :user
- end
-
describe 'GET #index' do
it 'returns a csv of the following accounts' do
+ user = Fabricate(:user)
+ user.account.follow!(Fabricate(:account, username: 'username', domain: 'domain'))
+
+ sign_in user, scope: :user
get :index, format: :csv
- expect(response).to have_http_status(:success)
- expect(response.content_type).to eq 'text/csv'
- expect(response.headers['Content-Disposition']).to eq 'attachment; filename="following_accounts.csv"'
+ expect(response.body).to eq "username@domain\n"
end
end
end
diff --git a/spec/controllers/settings/exports/muted_accounts_controller_spec.rb b/spec/controllers/settings/exports/muted_accounts_controller_spec.rb
index 37c3a0fcfe828ff727e2e99f237d3f2df7324132..f42d7881ed95bbb0fa01241680adbc57570da8fa 100644
--- a/spec/controllers/settings/exports/muted_accounts_controller_spec.rb
+++ b/spec/controllers/settings/exports/muted_accounts_controller_spec.rb
@@ -3,17 +3,15 @@ require 'rails_helper'
describe Settings::Exports::MutedAccountsController do
render_views
- before do
- sign_in Fabricate(:user), scope: :user
- end
-
describe 'GET #index' do
it 'returns a csv of the muting accounts' do
+ user = Fabricate(:user)
+ user.account.mute!(Fabricate(:account, username: 'username', domain: 'domain'))
+
+ sign_in user, scope: :user
get :index, format: :csv
- expect(response).to have_http_status(:success)
- expect(response.content_type).to eq 'text/csv'
- expect(response.headers['Content-Disposition']).to eq 'attachment; filename="muted_accounts.csv"'
+ expect(response.body).to eq "username@domain\n"
end
end
end
diff --git a/spec/controllers/settings/exports_controller_spec.rb b/spec/controllers/settings/exports_controller_spec.rb
index 2be6e4744fa2da0d771e523e0c2a4e994be76cc5..19cb0abdae17f1dc4792209ce8d744b5cc808955 100644
--- a/spec/controllers/settings/exports_controller_spec.rb
+++ b/spec/controllers/settings/exports_controller_spec.rb
@@ -3,15 +3,29 @@ require 'rails_helper'
describe Settings::ExportsController do
render_views
- before do
- sign_in Fabricate(:user), scope: :user
- end
-
describe 'GET #show' do
- it 'returns http success' do
- get :show
+ context 'when signed in' do
+ let(:user) { Fabricate(:user) }
+
+ before do
+ sign_in user, scope: :user
+ end
+
+ it 'renders export' do
+ get :show
+
+ export = assigns(:export)
+ expect(export).to be_instance_of Export
+ expect(export.account).to eq user.account
+ expect(response).to have_http_status(:success)
+ end
+ end
- expect(response).to have_http_status(:success)
+ context 'when not signed in' do
+ it 'redirects' do
+ get :show
+ expect(response).to redirect_to '/auth/sign_in'
+ end
end
end
end
diff --git a/spec/controllers/settings/imports_controller_spec.rb b/spec/controllers/settings/imports_controller_spec.rb
index 8bf6aec2b400977fbe241b1c491244d46614357d..4810be70159d43fee916f0151178e087229e78e2 100644
--- a/spec/controllers/settings/imports_controller_spec.rb
+++ b/spec/controllers/settings/imports_controller_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Settings::ImportsController, type: :controller do
describe 'POST #create' do
it 'redirects to settings path with successful following import' do
service = double(call: nil)
- allow(FollowRemoteAccountService).to receive(:new).and_return(service)
+ allow(ResolveRemoteAccountService).to receive(:new).and_return(service)
post :create, params: {
import: {
type: 'following',
@@ -30,7 +30,7 @@ RSpec.describe Settings::ImportsController, type: :controller do
it 'redirects to settings path with successful blocking import' do
service = double(call: nil)
- allow(FollowRemoteAccountService).to receive(:new).and_return(service)
+ allow(ResolveRemoteAccountService).to receive(:new).and_return(service)
post :create, params: {
import: {
type: 'blocking',
diff --git a/spec/controllers/settings/preferences_controller_spec.rb b/spec/controllers/settings/preferences_controller_spec.rb
index 38e43f211b902ab173e8cb3c66c5fd8fe1c32c22..60fa423023dd5dcd3a6692d770aaac0873a6fba5 100644
--- a/spec/controllers/settings/preferences_controller_spec.rb
+++ b/spec/controllers/settings/preferences_controller_spec.rb
@@ -28,12 +28,14 @@ describe Settings::PreferencesController do
it 'updates user settings' do
user.settings['boost_modal'] = false
+ user.settings['delete_modal'] = true
user.settings['notification_emails'] = user.settings['notification_emails'].merge('follow' => false)
user.settings['interactions'] = user.settings['interactions'].merge('must_be_follower' => true)
put :update, params: {
user: {
setting_boost_modal: '1',
+ setting_delete_modal: '0',
notification_emails: { follow: '1' },
interactions: { must_be_follower: '0' },
}
@@ -42,6 +44,7 @@ describe Settings::PreferencesController do
expect(response).to redirect_to(settings_preferences_path)
user.reload
expect(user.settings['boost_modal']).to be true
+ expect(user.settings['delete_modal']).to be false
expect(user.settings['notification_emails']['follow']).to be true
expect(user.settings['interactions']['must_be_follower']).to be false
end
diff --git a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
index bf555078e473a9d35d28b9284b101e9559eca82a..0676d61613f798632da8f81334c3340e449bb33f 100644
--- a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
+++ b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
@@ -5,41 +5,94 @@ require 'rails_helper'
describe Settings::TwoFactorAuthentication::ConfirmationsController do
render_views
- let(:user) { Fabricate(:user) }
- before do
- user.otp_secret = User.generate_otp_secret(32)
- user.save!
+ let(:user) { Fabricate(:user, email: 'local-part@domain', otp_secret: 'thisisasecretforthespecofnewview') }
- sign_in user, scope: :user
+ shared_examples 'renders :new' do
+ it 'renders the new view' do
+ subject
+
+ expect(assigns(:confirmation)).to be_instance_of Form::TwoFactorConfirmation
+ expect(assigns(:provision_url)).to eq 'otpauth://totp/local-part@domain?secret=thisisasecretforthespecofnewview&issuer=cb6e6126.ngrok.io'
+ expect(assigns(:qrcode)).to be_instance_of RQRCode::QRCode
+ expect(response).to have_http_status(:success)
+ expect(response).to render_template(:new)
+ end
end
describe 'GET #new' do
- it 'returns http success' do
- get :new
+ context 'when signed in' do
+ subject do
+ sign_in user, scope: :user
+ get :new
+ end
- expect(response).to have_http_status(:success)
- expect(response).to render_template(:new)
+ include_examples 'renders :new'
+ end
+
+ it 'redirects if not signed in' do
+ get :new
+ expect(response).to redirect_to('/auth/sign_in')
end
end
describe 'POST #create' do
- describe 'when creation succeeds' do
- it 'renders page with success' do
- allow_any_instance_of(User).to receive(:validate_and_consume_otp!).with('123456').and_return(true)
+ context 'when signed in' do
+ before do
+ sign_in user, scope: :user
+ end
- post :create, params: { form_two_factor_confirmation: { code: '123456' } }
- expect(response).to have_http_status(:success)
- expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index')
+ describe 'when form_two_factor_confirmation parameter is not provided' do
+ it 'raises ActionController::ParameterMissing' do
+ expect { post :create, params: { } }.to raise_error(ActionController::ParameterMissing)
+ end
+ end
+
+ describe 'when creation succeeds' do
+ it 'renders page with success' do
+ otp_backup_codes = user.generate_otp_backup_codes!
+ expect_any_instance_of(User).to receive(:generate_otp_backup_codes!) do |value|
+ expect(value).to eq user
+ otp_backup_codes
+ end
+ expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, arg|
+ expect(value).to eq user
+ expect(arg).to eq '123456'
+ true
+ end
+
+ post :create, params: { form_two_factor_confirmation: { code: '123456' } }
+
+ expect(assigns(:recovery_codes)).to eq otp_backup_codes
+ expect(flash[:notice]).to eq 'Two-factor authentication successfully enabled'
+ expect(response).to have_http_status(:success)
+ expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index')
+ end
end
- end
- describe 'when creation fails' do
- it 'renders the new view' do
- allow_any_instance_of(User).to receive(:validate_and_consume_otp!).with('123456').and_return(false)
+ describe 'when creation fails' do
+ subject do
+ expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, arg|
+ expect(value).to eq user
+ expect(arg).to eq '123456'
+ false
+ end
+
+ post :create, params: { form_two_factor_confirmation: { code: '123456' } }
+ end
+
+ it 'renders the new view' do
+ subject
+ expect(response.body).to include 'The entered code was invalid! Are server time and device time correct?'
+ end
+
+ include_examples 'renders :new'
+ end
+ end
+ context 'when not signed in' do
+ it 'redirects if not signed in' do
post :create, params: { form_two_factor_confirmation: { code: '123456' } }
- expect(response).to have_http_status(:success)
- expect(response).to render_template(:new)
+ expect(response).to redirect_to('/auth/sign_in')
end
end
end
diff --git a/spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb
index 3d6f5faab18bf584798e716df0a33ab391e10a69..aa28cdf3f8fc755ef56965105cd1b30088640bf4 100644
--- a/spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb
+++ b/spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb
@@ -5,21 +5,27 @@ require 'rails_helper'
describe Settings::TwoFactorAuthentication::RecoveryCodesController do
render_views
- let(:user) { Fabricate(:user) }
- before do
- sign_in user, scope: :user
- end
-
describe 'POST #create' do
- it 'updates the codes and shows them on a view' do
- before = user.otp_backup_codes
+ it 'updates the codes and shows them on a view when signed in' do
+ user = Fabricate(:user)
+ otp_backup_codes = user.generate_otp_backup_codes!
+ expect_any_instance_of(User).to receive(:generate_otp_backup_codes!) do |value|
+ expect(value).to eq user
+ otp_backup_codes
+ end
+ sign_in user, scope: :user
post :create
- user.reload
- expect(user.otp_backup_codes).not_to eq(before)
+ expect(assigns(:recovery_codes)).to eq otp_backup_codes
+ expect(flash[:notice]).to eq 'Recovery codes successfully regenerated'
expect(response).to have_http_status(:success)
expect(response).to render_template(:index)
end
+
+ it 'redirects when not signed in' do
+ post :create
+ expect(response).to redirect_to '/auth/sign_in'
+ end
end
end
diff --git a/spec/controllers/settings/two_factor_authentications_controller_spec.rb b/spec/controllers/settings/two_factor_authentications_controller_spec.rb
index 25d7a928d16236da11572b20ccfa554c0f8e350f..4d1a01fcfa8d63ca44dccb264a371e5197badde4 100644
--- a/spec/controllers/settings/two_factor_authentications_controller_spec.rb
+++ b/spec/controllers/settings/two_factor_authentications_controller_spec.rb
@@ -6,47 +6,70 @@ describe Settings::TwoFactorAuthenticationsController do
render_views
let(:user) { Fabricate(:user) }
- before do
- sign_in user, scope: :user
- end
describe 'GET #show' do
- describe 'when user requires otp for login already' do
- it 'returns http success' do
- user.update(otp_required_for_login: true)
- get :show
+ context 'when signed in' do
+ before do
+ sign_in user, scope: :user
+ end
- expect(response).to have_http_status(:success)
+ describe 'when user requires otp for login already' do
+ it 'returns http success' do
+ user.update(otp_required_for_login: true)
+ get :show
+
+ expect(response).to have_http_status(:success)
+ end
+ end
+
+ describe 'when user does not require otp for login' do
+ it 'returns http success' do
+ user.update(otp_required_for_login: false)
+ get :show
+
+ expect(response).to have_http_status(:success)
+ end
end
end
- describe 'when user does not require otp for login' do
- it 'returns http success' do
- user.update(otp_required_for_login: false)
+ context 'when not signed in' do
+ it 'redirects' do
get :show
-
- expect(response).to have_http_status(:success)
+ expect(response).to redirect_to '/auth/sign_in'
end
end
end
describe 'POST #create' do
- describe 'when user requires otp for login already' do
- it 'redirects to show page' do
- user.update(otp_required_for_login: true)
- post :create
+ context 'when signed in' do
+ before do
+ sign_in user, scope: :user
+ end
- expect(response).to redirect_to(settings_two_factor_authentication_path)
+ describe 'when user requires otp for login already' do
+ it 'redirects to show page' do
+ user.update(otp_required_for_login: true)
+ post :create
+
+ expect(response).to redirect_to(settings_two_factor_authentication_path)
+ end
end
- end
- describe 'when creation succeeds' do
- it 'updates user secret' do
- before = user.otp_secret
- post :create
+ describe 'when creation succeeds' do
+ it 'updates user secret' do
+ before = user.otp_secret
+ post :create
- expect(user.reload.otp_secret).not_to eq(before)
- expect(response).to redirect_to(new_settings_two_factor_authentication_confirmation_path)
+ expect(user.reload.otp_secret).not_to eq(before)
+ expect(response).to redirect_to(new_settings_two_factor_authentication_confirmation_path)
+ end
+ end
+ end
+
+ context 'when not signed in' do
+ it 'redirects' do
+ get :show
+ expect(response).to redirect_to '/auth/sign_in'
end
end
end
@@ -55,12 +78,19 @@ describe Settings::TwoFactorAuthenticationsController do
before do
user.update(otp_required_for_login: true)
end
- it 'turns off otp requirement' do
+
+ it 'turns off otp requirement if signed in' do
+ sign_in user, scope: :user
post :destroy
expect(response).to redirect_to(settings_two_factor_authentication_path)
user.reload
expect(user.otp_required_for_login).to eq(false)
end
+
+ it 'redirects if not signed in' do
+ get :show
+ expect(response).to redirect_to '/auth/sign_in'
+ end
end
end
diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb
index c329996c8bc3967dd94c9918c9cce5045be8fc58..88d365624e32bbd05d64862286bc89c662daaa22 100644
--- a/spec/controllers/statuses_controller_spec.rb
+++ b/spec/controllers/statuses_controller_spec.rb
@@ -61,7 +61,7 @@ describe StatusesController do
get :show, params: { account_username: status.account.username, id: status.id }
- expect(assigns(:ancestors)).to match_array([ancestor])
+ expect(assigns(:ancestors)).to eq [ancestor]
end
it 'assigns @ancestors for [] if it is not a reply' do
diff --git a/spec/controllers/stream_entries_controller_spec.rb b/spec/controllers/stream_entries_controller_spec.rb
index db7c52037e1349e37542893afdd98065545737c8..2cc428e0c1be90b0394b0efaf3b4b70b674adfa5 100644
--- a/spec/controllers/stream_entries_controller_spec.rb
+++ b/spec/controllers/stream_entries_controller_spec.rb
@@ -73,8 +73,8 @@ RSpec.describe StreamEntriesController, type: :controller do
get :show, params: { account_username: status.account.username, id: status.stream_entry.id }
- expect(assigns(:ancestors)).to match_array([ancestor])
- expect(assigns(:descendants)).to match_array([descendant])
+ expect(assigns(:ancestors)).to eq [ancestor]
+ expect(assigns(:descendants)).to eq [descendant]
expect(response).to have_http_status(:success)
end
diff --git a/spec/controllers/well_known/host_meta_controller_spec.rb b/spec/controllers/well_known/host_meta_controller_spec.rb
index 8a040021a6e9f4074cb70453f3b9a5e2e1147d67..87c1485ed5f80b9d52f3afdcdd561a421cbc0dc0 100644
--- a/spec/controllers/well_known/host_meta_controller_spec.rb
+++ b/spec/controllers/well_known/host_meta_controller_spec.rb
@@ -8,6 +8,13 @@ describe WellKnown::HostMetaController, type: :controller do
get :show, format: :xml
expect(response).to have_http_status(:success)
+ expect(response.content_type).to eq 'application/xrd+xml'
+ expect(response.body).to eq <
+
+
+
+XML
end
end
end
diff --git a/spec/controllers/well_known/webfinger_controller_spec.rb b/spec/controllers/well_known/webfinger_controller_spec.rb
index 4d588bf4e3e9c3bb3a243789ccc49b4713057da7..3699efb5603d8e2b4e1965bf463d12e585e72e05 100644
--- a/spec/controllers/well_known/webfinger_controller_spec.rb
+++ b/spec/controllers/well_known/webfinger_controller_spec.rb
@@ -4,7 +4,40 @@ describe WellKnown::WebfingerController, type: :controller do
render_views
describe 'GET #show' do
- let(:alice) { Fabricate(:account, username: 'alice') }
+ let(:alice) do
+ Fabricate(:account, username: 'alice')
+ end
+
+ before do
+ alice.private_key = <
+
+ acct:alice@cb6e6126.ngrok.io
+ https://cb6e6126.ngrok.io/@alice
+ https://cb6e6126.ngrok.io/users/alice
+
+
+
+
+
+
+XML
end
it 'returns http not found when account cannot be found' do
@@ -24,13 +79,15 @@ describe WellKnown::WebfingerController, type: :controller do
expect(response).to have_http_status(:not_found)
end
- it 'returns http success when account can be found with alternate domains' do
+ it 'returns JSON when account can be found with alternate domains' do
Rails.configuration.x.alternate_domains = ["foo.org"]
username, domain = alice.to_webfinger_s.split("@")
get :show, params: { resource: "#{username}@foo.org" }, format: :json
expect(response).to have_http_status(:success)
+ expect(response.content_type).to eq 'application/jrd+json'
+ expect(response.body).to eq "{\"subject\":\"acct:alice@cb6e6126.ngrok.io\",\"aliases\":[\"https://cb6e6126.ngrok.io/@alice\",\"https://cb6e6126.ngrok.io/users/alice\"],\"links\":[{\"rel\":\"http://webfinger.net/rel/profile-page\",\"type\":\"text/html\",\"href\":\"https://cb6e6126.ngrok.io/@alice\"},{\"rel\":\"http://schemas.google.com/g/2010#updates-from\",\"type\":\"application/atom+xml\",\"href\":\"https://cb6e6126.ngrok.io/users/alice.atom\"},{\"rel\":\"self\",\"type\":\"application/activity+json\",\"href\":\"https://cb6e6126.ngrok.io/@alice\"},{\"rel\":\"salmon\",\"href\":\"#{api_salmon_url(alice.id)}\"},{\"rel\":\"magic-public-key\",\"href\":\"data:application/magic-public-key,RSA.x4D6DyZa3zGa1XLhd_VG1bLGvK-Dmyz93WJfWNezIKeSuJkmA0f2NmoOfLUoumq9szN2Xt0GLDX06tDajdYPPXgLtDG0o1qqTrIJ7UTyYhbo94Wotl9iJvEwa5IjP1Mn00YJ_KvFrzKCm15PC7up6r-NtHsqoYS8X1KAqcbnptU=.AQAB\"},{\"rel\":\"http://ostatus.org/schema/1.0/subscribe\",\"template\":\"https://cb6e6126.ngrok.io/authorize_follow?acct={uri}\"}]}"
end
it 'returns http not found when account can not be found with alternate domains' do
diff --git a/spec/fabricators/follow_request_fabricator.rb b/spec/fabricators/follow_request_fabricator.rb
index 78a057919dc201ab141ad026fa7e74806110325f..c00ddf84d9b2436a8785c381e52d71f62646bd75 100644
--- a/spec/fabricators/follow_request_fabricator.rb
+++ b/spec/fabricators/follow_request_fabricator.rb
@@ -1,4 +1,4 @@
Fabricator(:follow_request) do
account
- target_account { Fabricate(:account) }
+ target_account { Fabricate(:account, locked: true) }
end
diff --git a/spec/fabricators/media_attachment_fabricator.rb b/spec/fabricators/media_attachment_fabricator.rb
index dc91d708f3347d946688947506520ad6844415ed..bb938e36d9825d6bbd9f1769d84e04731bf9889b 100644
--- a/spec/fabricators/media_attachment_fabricator.rb
+++ b/spec/fabricators/media_attachment_fabricator.rb
@@ -1,3 +1,16 @@
Fabricator(:media_attachment) do
account
+ file do |attrs|
+ [
+ case attrs[:type]
+ when :gifv
+ attachment_fixture ['attachment.gif', 'attachment.webm'].sample
+ when :image
+ attachment_fixture 'attachment.jpg'
+ when nil
+ attachment_fixture ['attachment.gif', 'attachment.jpg', 'attachment.webm'].sample
+ end,
+ nil
+ ].sample
+ end
end
diff --git a/spec/features/log_in_spec.rb b/spec/features/log_in_spec.rb
index f9c222b60ecab162325202ae9835039dbe22ce78..ed626d880ebf1a569cc5049a3ed336094e35a59e 100644
--- a/spec/features/log_in_spec.rb
+++ b/spec/features/log_in_spec.rb
@@ -1,16 +1,47 @@
require "rails_helper"
feature "Log in" do
- scenario "A valid email and password user is able to log in" do
- email = "test@example.com"
- password = "password"
- Fabricate(:user, email: email, password: password)
+ given(:email) { "test@examle.com" }
+ given(:password) { "password" }
+ given(:confirmed_at) { Time.now }
+ background do
+ Fabricate(:user, email: email, password: password, confirmed_at: confirmed_at)
visit new_user_session_path
+ end
+
+ subject { page }
+
+ scenario "A valid email and password user is able to log in" do
fill_in "user_email", with: email
fill_in "user_password", with: password
- click_on "Log in"
+ click_on I18n.t('auth.login')
+
+ is_expected.to have_css("div.app-holder")
+ end
+
+ scenario "A invalid email and password user is not able to log in" do
+ fill_in "user_email", with: "invalid_email"
+ fill_in "user_password", with: "invalid_password"
+ click_on I18n.t('auth.login')
+
+ is_expected.to have_css(".flash-message", text: failure_message("invalid"))
+ end
+
+ context do
+ given(:confirmed_at) { nil }
+
+ scenario "A unconfirmed user is not able to log in" do
+ fill_in "user_email", with: email
+ fill_in "user_password", with: password
+ click_on I18n.t('auth.login')
+
+ is_expected.to have_css(".flash-message", text: failure_message("unconfirmed"))
+ end
+ end
- expect(page).to have_css "div.app-holder"
+ def failure_message(message)
+ keys = User.authentication_keys.map { |key| User.human_attribute_name(key) }
+ I18n.t("devise.failure.#{message}", authentication_keys: keys.join("support.array.words_connector"))
end
end
diff --git a/spec/fixtures/requests/oembed_invalid_xml.html b/spec/fixtures/requests/oembed_invalid_xml.html
new file mode 100644
index 0000000000000000000000000000000000000000..405834f129aa9183029756457bd8da42f6c7bab3
--- /dev/null
+++ b/spec/fixtures/requests/oembed_invalid_xml.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/spec/fixtures/requests/oembed_json.html b/spec/fixtures/requests/oembed_json.html
new file mode 100644
index 0000000000000000000000000000000000000000..773a4f92a24d64a541013e1110913e60c28a097b
--- /dev/null
+++ b/spec/fixtures/requests/oembed_json.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/spec/fixtures/requests/oembed_json_xml.html b/spec/fixtures/requests/oembed_json_xml.html
new file mode 100644
index 0000000000000000000000000000000000000000..b5fc9bed09c6e5243e82cce7b167e280bf6eb547
--- /dev/null
+++ b/spec/fixtures/requests/oembed_json_xml.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/spec/fixtures/requests/oembed_undiscoverable.html b/spec/fixtures/requests/oembed_undiscoverable.html
new file mode 100644
index 0000000000000000000000000000000000000000..a4acdc47a558342f12c34fd29d2e9faf14a923aa
--- /dev/null
+++ b/spec/fixtures/requests/oembed_undiscoverable.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/spec/fixtures/requests/oembed_xml.html b/spec/fixtures/requests/oembed_xml.html
new file mode 100644
index 0000000000000000000000000000000000000000..5d7633e71325503b206b58e822167535f2907cf1
--- /dev/null
+++ b/spec/fixtures/requests/oembed_xml.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/spec/helpers/activitystreams2_builder_helper_spec.rb b/spec/helpers/activitystreams2_builder_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..612ce6ad29307229d0bdfaf065afbdcddbddb946
--- /dev/null
+++ b/spec/helpers/activitystreams2_builder_helper_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Activitystreams2BuilderHelper, type: :helper do
+ it 'returns display name if present' do
+ account = Fabricate(:account, display_name: 'display name', username: 'username')
+ expect(account_name(account)).to eq 'display name'
+ end
+
+ it 'returns username if display name is not present' do
+ account = Fabricate(:account, display_name: '', username: 'username')
+ expect(account_name(account)).to eq 'username'
+ end
+end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 81f38d0b0217e76ae6d0ca7558be17e27cbf2b10..ac54f1f703036a296a198f598374fa0a22f765b7 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -17,6 +17,65 @@ describe ApplicationHelper do
end
end
+ describe 'add_rtl_body_class' do
+ around do |example|
+ current_locale = I18n.locale
+ example.run
+ I18n.locale = current_locale
+ end
+
+ it 'adds rtl body class if locale is Arabic' do
+ I18n.locale = :ar
+ expect(helper.add_rtl_body_class('other classes')).to eq 'other classes rtl'
+ end
+
+ it 'adds rtl body class if locale is Farsi' do
+ I18n.locale = :fa
+ expect(helper.add_rtl_body_class('other classes')).to eq 'other classes rtl'
+ end
+
+ it 'adds rtl if locale is Hebrew' do
+ I18n.locale = :he
+ expect(helper.add_rtl_body_class('other classes')).to eq 'other classes rtl'
+ end
+
+ it 'does not add rtl if locale is Thai' do
+ I18n.locale = :th
+ expect(helper.add_rtl_body_class('other classes')).to eq 'other classes'
+ end
+ end
+
+ describe 'fa_icon' do
+ it 'returns a tag of fixed-width cog' do
+ expect(helper.fa_icon('cog fw')).to eq ''
+ end
+ end
+
+ describe 'favicon_path' do
+ it 'returns /favicon.ico on production enviromnent' do
+ expect(Rails.env).to receive(:production?).and_return(true)
+ expect(helper.favicon_path).to eq '/favicon.ico'
+ end
+ end
+
+ describe 'open_registrations?' do
+ it 'returns true when open for registrations' do
+ without_partial_double_verification do
+ expect(Setting).to receive(:open_registrations).and_return(true)
+ end
+
+ expect(helper.open_registrations?).to eq true
+ end
+
+ it 'returns false when closed for registrations' do
+ without_partial_double_verification do
+ expect(Setting).to receive(:open_registrations).and_return(false)
+ end
+
+ expect(helper.open_registrations?).to eq false
+ end
+ end
+
describe 'show_landing_strip?', without_verify_partial_doubles: true do
describe 'when signed in' do
before do
@@ -45,4 +104,18 @@ describe ApplicationHelper do
end
end
end
+
+ describe 'title' do
+ around do |example|
+ site_title = Setting.site_title
+ example.run
+ Setting.site_title = site_title
+ end
+
+ it 'returns site title on production enviroment' do
+ Setting.site_title = 'site title'
+ expect(Rails.env).to receive(:production?).and_return(true)
+ expect(helper.title).to eq 'site title'
+ end
+ end
end
diff --git a/spec/helpers/flashes_helper_spec.rb b/spec/helpers/flashes_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ea143eed72259cbebf6c456ebdf118122f31a48f
--- /dev/null
+++ b/spec/helpers/flashes_helper_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe FlashesHelper, type: :helper do
+ describe 'user_facing_flashes' do
+ it 'returns user facing flashes' do
+ flash[:alert] = 'an alert'
+ flash[:error] = 'an error'
+ flash[:notice] = 'a notice'
+ flash[:success] = 'a success'
+ flash[:not_user_facing] = 'a not user facing flash'
+ expect(helper.user_facing_flashes).to eq 'alert' => 'an alert',
+ 'error' => 'an error',
+ 'notice' => 'a notice',
+ 'success' => 'a success'
+ end
+ end
+end
diff --git a/spec/helpers/home_helper_spec.rb b/spec/helpers/home_helper_spec.rb
index 8c96387e68fc1841def39772d98229d5612f2d68..a3dc6f836fff92080faa6e0b77c69ad3a7dbb721 100644
--- a/spec/helpers/home_helper_spec.rb
+++ b/spec/helpers/home_helper_spec.rb
@@ -1,5 +1,9 @@
require 'rails_helper'
RSpec.describe HomeHelper, type: :helper do
-
+ describe 'default_props' do
+ it 'returns default properties according to the context' do
+ expect(helper.default_props).to eq locale: I18n.locale
+ end
+ end
end
diff --git a/spec/helpers/http_helper_spec.rb b/spec/helpers/http_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b8e31b8e6f3b69e9272bf75736713c1c63c96418
--- /dev/null
+++ b/spec/helpers/http_helper_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe HttpHelper do
+ describe 'http_client' do
+ it 'returns HTTP::Client with default options' do
+ options = helper.http_client.default_options
+ expect(options.headers['User-Agent']).to match /.+ \(Mastodon\/.+;\ \+http:\/\/cb6e6126\.ngrok\.io\/\)/
+ expect(options.timeout_options).to eq read_timeout: 10, write_timeout: 10, connect_timeout: 10
+ end
+ end
+end
diff --git a/spec/helpers/instance_helper_spec.rb b/spec/helpers/instance_helper_spec.rb
index c42ed693886e9ca65b63762a0a26a294734c36ca..c3d28544f2ddab78b69be6d10dd814fa58d7a09d 100644
--- a/spec/helpers/instance_helper_spec.rb
+++ b/spec/helpers/instance_helper_spec.rb
@@ -4,6 +4,12 @@ require 'rails_helper'
describe InstanceHelper do
describe 'site_title' do
+ around do |example|
+ site_title = Setting.site_title
+ example.run
+ Setting.site_title = site_title
+ end
+
it 'Uses the Setting.site_title value when it exists' do
Setting.site_title = 'New site title'
diff --git a/spec/helpers/stream_entries_helper_spec.rb b/spec/helpers/stream_entries_helper_spec.rb
index 1ef49a3ab5b330b3512c5dbf8638ee53ad43e249..2c0d7b2394c68f8f3a51a546022a83235fdc8702 100644
--- a/spec/helpers/stream_entries_helper_spec.rb
+++ b/spec/helpers/stream_entries_helper_spec.rb
@@ -217,7 +217,7 @@ RSpec.describe StreamEntriesHelper, type: :helper do
end
it 'is true if right to left characters are greater than 1/3 of total text' do
- expect(helper).to be_rtl 'aaݟ'
+ expect(helper).to be_rtl 'aaݟaaݟ'
end
end
end
diff --git a/spec/javascript/components/avatar.test.jsx b/spec/javascript/components/avatar.test.js
similarity index 94%
rename from spec/javascript/components/avatar.test.jsx
rename to spec/javascript/components/avatar.test.js
index f7017388022e6ff5b425c4555d1733c224b7bc52..03b71dc9d92eb716f5e22739c5e9313f5b5bff60 100644
--- a/spec/javascript/components/avatar.test.jsx
+++ b/spec/javascript/components/avatar.test.js
@@ -1,7 +1,7 @@
import { expect } from 'chai';
import { render } from 'enzyme';
-
-import Avatar from '../../../app/javascript/mastodon/components/avatar'
+import React from 'react';
+import Avatar from '../../../app/javascript/mastodon/components/avatar';
describe('', () => {
const src = '/path/to/image.jpg';
diff --git a/spec/javascript/components/button.test.jsx b/spec/javascript/components/button.test.js
similarity index 98%
rename from spec/javascript/components/button.test.jsx
rename to spec/javascript/components/button.test.js
index e08671c01329c8ef20b637554b1e1c5994d933f7..9cf8b1eed5010e7715b86dbd7aed6bf4a07707ae 100644
--- a/spec/javascript/components/button.test.jsx
+++ b/spec/javascript/components/button.test.js
@@ -1,7 +1,7 @@
import { expect } from 'chai';
import { shallow } from 'enzyme';
import sinon from 'sinon';
-
+import React from 'react';
import Button from '../../../app/javascript/mastodon/components/button';
describe('', () => {
diff --git a/spec/javascript/components/display_name.test.jsx b/spec/javascript/components/display_name.test.js
similarity index 88%
rename from spec/javascript/components/display_name.test.jsx
rename to spec/javascript/components/display_name.test.js
index 7072e653ef0cd337dc4cc1adb6116d3aa4bd3eac..d6dc7edc091ebd4d91ba715cdb922aa90d6f8f67 100644
--- a/spec/javascript/components/display_name.test.jsx
+++ b/spec/javascript/components/display_name.test.js
@@ -1,15 +1,15 @@
import { expect } from 'chai';
import { render } from 'enzyme';
import Immutable from 'immutable';
-
-import DisplayName from '../../../app/javascript/mastodon/components/display_name'
+import React from 'react';
+import DisplayName from '../../../app/javascript/mastodon/components/display_name';
describe('', () => {
it('renders display name + account name', () => {
const account = Immutable.fromJS({
username: 'bar',
acct: 'bar@baz',
- display_name: 'Foo'
+ display_name: 'Foo',
});
const wrapper = render();
expect(wrapper).to.have.text('Foo @bar@baz');
@@ -19,7 +19,7 @@ describe('', () => {
const account = Immutable.fromJS({
username: 'bar',
acct: 'bar@baz',
- display_name: ''
+ display_name: '',
});
const wrapper = render();
expect(wrapper).to.have.text('bar @bar@baz');
diff --git a/spec/javascript/components/dropdown_menu.test.jsx b/spec/javascript/components/dropdown_menu.test.js
similarity index 96%
rename from spec/javascript/components/dropdown_menu.test.jsx
rename to spec/javascript/components/dropdown_menu.test.js
index c5bbf5ad63bd243f700c74b5158e10e42351e60a..54cdcabf06cae5268f3a728c971bc221f1fefc7e 100644
--- a/spec/javascript/components/dropdown_menu.test.jsx
+++ b/spec/javascript/components/dropdown_menu.test.js
@@ -1,7 +1,7 @@
import { expect } from 'chai';
import { shallow, mount } from 'enzyme';
import sinon from 'sinon';
-
+import React from 'react';
import DropdownMenu from '../../../app/javascript/mastodon/components/dropdown_menu';
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
@@ -12,7 +12,7 @@ describe('', () => {
const items = [
{ text: 'first item', action: action, href: '/some/url' },
- { text: 'second item', action: 'noop' }
+ { text: 'second item', action: 'noop' },
];
const wrapper = shallow();
@@ -35,23 +35,23 @@ describe('', () => {
});
it('uses props.icon as icon class name', () => {
- expect(wrapper.find(DropdownTrigger).find('i')).to.have.className(`fa-${icon}`)
+ expect(wrapper.find(DropdownTrigger).find('i')).to.have.className(`fa-${icon}`);
});
it('is not expanded by default', () => {
expect(wrapper.state('expanded')).to.be.equal(false);
- })
+ });
it('does not render the list elements if not expanded', () => {
const lis = wrapper.find(DropdownContent).find('li');
expect(lis.length).to.be.equal(0);
- })
+ });
it('sets expanded to true when clicking the trigger', () => {
const wrapper = mount();
wrapper.find(DropdownTrigger).first().simulate('click');
expect(wrapper.state('expanded')).to.be.equal(true);
- })
+ });
// Error: ReactWrapper::state() can only be called on the root
/*it('sets expanded to false when clicking outside', () => {
diff --git a/spec/javascript/components/features/ui/components/column.test.jsx b/spec/javascript/components/features/ui/components/column.test.js
similarity index 84%
rename from spec/javascript/components/features/ui/components/column.test.jsx
rename to spec/javascript/components/features/ui/components/column.test.js
index 6359905e650cf486f790bd8a2491941d446aa292..4491d6e19bd50dcc063e6df868cbedd58652d30f 100644
--- a/spec/javascript/components/features/ui/components/column.test.jsx
+++ b/spec/javascript/components/features/ui/components/column.test.js
@@ -1,7 +1,7 @@
import { expect } from 'chai';
import { mount } from 'enzyme';
import sinon from 'sinon';
-
+import React from 'react';
import Column from '../../../../../../app/javascript/mastodon/features/ui/components/column';
import ColumnHeader from '../../../../../../app/javascript/mastodon/features/ui/components/column_header';
@@ -13,8 +13,8 @@ describe('', () => {
it('runs the scroll animation if the column contains scrollable content', () => {
const wrapper = mount(
-
-
+
+
);
wrapper.find(ColumnHeader).simulate('click');
@@ -22,7 +22,7 @@ describe('', () => {
});
it('does not try to scroll if there is no scrollable content', () => {
- const wrapper = mount();
+ const wrapper = mount();
wrapper.find(ColumnHeader).simulate('click');
expect(global.requestAnimationFrame.called).to.equal(false);
});
diff --git a/spec/javascript/components/loading_indicator.test.jsx b/spec/javascript/components/loading_indicator.test.js
similarity index 75%
rename from spec/javascript/components/loading_indicator.test.jsx
rename to spec/javascript/components/loading_indicator.test.js
index a372640be75d2c5f63867d4dc86e5de4af4ceb73..0859dd192d354ff749dacd6aba33f7fc0509c1a0 100644
--- a/spec/javascript/components/loading_indicator.test.jsx
+++ b/spec/javascript/components/loading_indicator.test.js
@@ -1,7 +1,7 @@
import { expect } from 'chai';
import { shallow } from 'enzyme';
-
-import LoadingIndicator from '../../../app/javascript/mastodon/components/loading_indicator'
+import React from 'react';
+import LoadingIndicator from '../../../app/javascript/mastodon/components/loading_indicator';
describe('', () => {
diff --git a/spec/javascript/setup.js b/spec/javascript/setup.js
index 0d07f74d88168440637788c27956ca07d93e4060..7d4b2866efe48ed70224a35cefd53b9db56de242 100644
--- a/spec/javascript/setup.js
+++ b/spec/javascript/setup.js
@@ -1,12 +1,8 @@
+import { jsdom } from 'jsdom/lib/old-api';
import chai from 'chai';
import chaiEnzyme from 'chai-enzyme';
chai.use(chaiEnzyme());
-/**
- * http://airbnb.io/enzyme/docs/guides/jsdom.html
- */
-var jsdom = require('jsdom').jsdom;
-
var exposedProperties = ['window', 'navigator', 'document'];
global.document = jsdom('');
@@ -19,8 +15,5 @@ Object.keys(document.defaultView).forEach((property) => {
});
global.navigator = {
- userAgent: 'node.js'
+ userAgent: 'node.js',
};
-
-var React = window.React = global.React = require('react');
-var ReactDOM = window.ReactDOM = global.ReactDOM = require('react-dom');
diff --git a/spec/lib/atom_serializer_spec.rb b/spec/lib/atom_serializer_spec.rb
index cfca82a284411a4609afc015e8bc55afb81807c2..d14fc5b40f77c56d0fe4da59735f1909cbe025a1 100644
--- a/spec/lib/atom_serializer_spec.rb
+++ b/spec/lib/atom_serializer_spec.rb
@@ -1,206 +1,1554 @@
require 'rails_helper'
RSpec.describe AtomSerializer do
- let(:author) { Fabricate(:account, username: 'Sombra', display_name: '1337 haxxor') }
- let(:receiver) { Fabricate(:account, username: 'Symmetra') }
+ shared_examples 'follow request salmon' do
+ it 'appends author element with account' do
+ account = Fabricate(:account, domain: nil, username: 'username')
+ follow_request = Fabricate(:follow_request, account: account)
- before do
- stub_request(:get, "https://cb6e6126.ngrok.io/avatars/original/missing.png").to_return(status: 404)
- stub_request(:get, "https://cb6e6126.ngrok.io/headers/original/missing.png").to_return(status: 404)
+ follow_request_salmon = serialize(follow_request)
+
+ expect(follow_request_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username'
+ end
+
+ it 'appends activity:object-type element with activity type' do
+ follow_request = Fabricate(:follow_request)
+
+ follow_request_salmon = serialize(follow_request)
+
+ object_type = follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' }
+ expect(object_type.text).to eq TagManager::TYPES[:activity]
+ end
+
+ it 'appends activity:verb element with request_friend type' do
+ follow_request = Fabricate(:follow_request)
+
+ follow_request_salmon = serialize(follow_request)
+
+ verb = follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' }
+ expect(verb.text).to eq TagManager::VERBS[:request_friend]
+ end
+
+ it 'appends activity:object with target account' do
+ target_account = Fabricate(:account, domain: 'domain', uri: 'https://domain/id')
+ follow_request = Fabricate(:follow_request, target_account: target_account)
+
+ follow_request_salmon = serialize(follow_request)
+
+ object = follow_request_salmon.nodes.find { |node| node.name == 'activity:object' }
+ expect(object.id.text).to eq 'https://domain/id'
+ end
+ end
+
+ shared_examples 'namespaces' do
+ it 'adds namespaces' do
+ element = serialize
+
+ expect(element['xmlns']).to eq TagManager::XMLNS
+ expect(element['xmlns:thr']).to eq TagManager::THR_XMLNS
+ expect(element['xmlns:activity']).to eq TagManager::AS_XMLNS
+ expect(element['xmlns:poco']).to eq TagManager::POCO_XMLNS
+ expect(element['xmlns:media']).to eq TagManager::MEDIA_XMLNS
+ expect(element['xmlns:ostatus']).to eq TagManager::OS_XMLNS
+ expect(element['xmlns:mastodon']).to eq TagManager::MTDN_XMLNS
+ end
+ end
+
+ shared_examples 'no namespaces' do
+ it 'does not add namespaces' do
+ expect(serialize['xmlns']).to eq nil
+ end
+ end
+
+ shared_examples 'status attributes' do
+ it 'appends summary element with spoiler text if present' do
+ status = Fabricate(:status, language: :ca, spoiler_text: 'spoiler text')
+
+ element = serialize(status)
+
+ summary = element.summary
+ expect(summary['xml:lang']).to eq 'ca'
+ expect(summary.text).to eq 'spoiler text'
+ end
+
+ it 'does not append summary element with spoiler text if not present' do
+ status = Fabricate(:status, spoiler_text: '')
+ element = serialize(status)
+ element.nodes.each { |node| expect(node.name).not_to eq 'summary' }
+ end
+
+ it 'appends content element with formatted status' do
+ status = Fabricate(:status, language: :ca, text: 'text')
+
+ element = serialize(status)
+
+ content = element.content
+ expect(content[:type]).to eq 'html'
+ expect(content['xml:lang']).to eq 'ca'
+ expect(content.text).to eq '
text
'
+ end
+
+ it 'appends link elements for mentioned accounts' do
+ account = Fabricate(:account, username: 'username')
+ status = Fabricate(:status)
+ Fabricate(:mention, account: account, status: status)
+
+ element = serialize(status)
+
+ mentioned = element.nodes.find do |node|
+ node.name == 'link' &&
+ node[:rel] == 'mentioned' &&
+ node['ostatus:object-type'] == TagManager::TYPES[:person]
+ end
+ expect(mentioned[:href]).to eq 'https://cb6e6126.ngrok.io/users/username'
+ end
+ end
+
+ describe 'render' do
+ it 'returns XML with emojis' do
+ element = Ox::Element.new('tag')
+ element << '💩'
+ xml = AtomSerializer.render(element)
+
+ expect(xml).to eq "\n💩\n"
+ end
+
+ it 'returns XML, stripping invalid characters like \b and \v' do
+ element = Ox::Element.new('tag')
+ element << "im l33t\b haxo\b\vr"
+ xml = AtomSerializer.render(element)
+
+ expect(xml).to eq "\nim l33t haxor\n"
+ end
+ end
+
+ describe '#author' do
+ context 'when note is present' do
+ it 'appends poco:note element with note for local account' do
+ account = Fabricate(:account, domain: nil, note: '
'
+ end
+
+ it 'appends poco:note element with tags-stripped note for remote account' do
+ account = Fabricate(:account, domain: 'remote', note: '
note
')
+
+ author = AtomSerializer.new.author(account)
+
+ note = author.nodes.find { |node| node.name == 'poco:note' }
+ expect(note.text).to eq 'note'
+ end
+
+ it 'appends summary element with type attribute and simplified note if present' do
+ account = Fabricate(:account, note: 'note')
+ author = AtomSerializer.new.author(account)
+ expect(author.summary.text).to eq '
note
'
+ expect(author.summary[:type]).to eq 'html'
+ end
+ end
+
+ context 'when note is not present' do
+ it 'does not append poco:note element' do
+ account = Fabricate(:account, note: '')
+ author = AtomSerializer.new.author(account)
+ author.nodes.each { |node| expect(node.name).not_to eq 'poco:note' }
+ end
+
+ it 'does not append summary element' do
+ account = Fabricate(:account, note: '')
+ author = AtomSerializer.new.author(account)
+ author.nodes.each { |node| expect(node.name).not_to eq 'summary' }
+ end
+ end
+
+ it 'returns author element' do
+ account = Fabricate(:account)
+ author = AtomSerializer.new.author(account)
+ expect(author.name).to eq 'author'
+ end
+
+ it 'appends activity:object-type element with person type' do
+ account = Fabricate(:account, domain: nil, username: 'username')
+
+ author = AtomSerializer.new.author(account)
+
+ object_type = author.nodes.find { |node| node.name == 'activity:object-type' }
+ expect(object_type.text).to eq TagManager::TYPES[:person]
+ end
+
+ it 'appends email element with username and domain for local account' do
+ account = Fabricate(:account, username: 'username')
+ author = AtomSerializer.new.author(account)
+ expect(author.email.text).to eq 'username@cb6e6126.ngrok.io'
+ end
+
+ it 'appends email element with username and domain for remote user' do
+ account = Fabricate(:account, domain: 'domain', username: 'username')
+ author = AtomSerializer.new.author(account)
+ expect(author.email.text).to eq 'username@domain'
+ end
+
+ it 'appends link element for an alternative' do
+ account = Fabricate(:account, domain: nil, username: 'username')
+
+ author = AtomSerializer.new.author(account)
+
+ link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' }
+ expect(link[:type]).to eq 'text/html'
+ expect(link[:rel]).to eq 'alternate'
+ expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/@username'
+ end
+
+ it 'has link element for avatar if present' do
+ account = Fabricate(:account, avatar: attachment_fixture('avatar.gif'))
+
+ author = AtomSerializer.new.author(account)
+
+ link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'avatar' }
+ expect(link[:type]).to eq 'image/gif'
+ expect(link['media:width']).to eq '120'
+ expect(link['media:height']).to eq '120'
+ expect(link[:href]).to match /^https:\/\/cb6e6126.ngrok.io\/system\/accounts\/avatars\/.+\/original\/avatar.gif/
+ end
+
+ it 'does not have link element for avatar if not present' do
+ account = Fabricate(:account, avatar: nil)
+
+ author = AtomSerializer.new.author(account)
+
+ author.nodes.each do |node|
+ expect(node[:rel]).not_to eq 'avatar' if node.name == 'link'
+ end
+ end
+
+ it 'appends link element for header if present' do
+ account = Fabricate(:account, header: attachment_fixture('avatar.gif'))
+
+ author = AtomSerializer.new.author(account)
+
+ link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'header' }
+ expect(link[:type]).to eq 'image/gif'
+ expect(link['media:width']).to eq '700'
+ expect(link['media:height']).to eq '335'
+ expect(link[:href]).to match /^https:\/\/cb6e6126.ngrok.io\/system\/accounts\/headers\/.+\/original\/avatar.gif/
+ end
+
+ it 'does not append link element for header if not present' do
+ account = Fabricate(:account, header: nil)
+
+ author = AtomSerializer.new.author(account)
+
+ author.nodes.each do |node|
+ expect(node[:rel]).not_to eq 'header' if node.name == 'link'
+ end
+ end
+
+ it 'appends poco:displayName element with display name if present' do
+ account = Fabricate(:account, display_name: 'display name')
+
+ author = AtomSerializer.new.author(account)
+
+ display_name = author.nodes.find { |node| node.name == 'poco:displayName' }
+ expect(display_name.text).to eq 'display name'
+ end
+
+ it 'does not append poco:displayName element with display name if not present' do
+ account = Fabricate(:account, display_name: '')
+ author = AtomSerializer.new.author(account)
+ author.nodes.each { |node| expect(node.name).not_to eq 'poco:displayName' }
+ end
+
+ it "appends mastodon:scope element with 'private' if locked" do
+ account = Fabricate(:account, locked: true)
+
+ author = AtomSerializer.new.author(account)
+
+ scope = author.nodes.find { |node| node.name == 'mastodon:scope' }
+ expect(scope.text).to eq 'private'
+ end
+
+ it "appends mastodon:scope element with 'public' if unlocked" do
+ account = Fabricate(:account, locked: false)
+
+ author = AtomSerializer.new.author(account)
+
+ scope = author.nodes.find { |node| node.name == 'mastodon:scope' }
+ expect(scope.text).to eq 'public'
+ end
+
+ it 'includes URI' do
+ account = Fabricate(:account, domain: nil, username: 'username')
+
+ author = AtomSerializer.new.author(account)
+
+ expect(author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username'
+ expect(author.uri.text).to eq 'https://cb6e6126.ngrok.io/users/username'
+ end
+
+ it 'includes username' do
+ account = Fabricate(:account, username: 'username')
+
+ author = AtomSerializer.new.author(account)
+
+ name = author.nodes.find { |node| node.name == 'name' }
+ username = author.nodes.find { |node| node.name == 'poco:preferredUsername' }
+ expect(name.text).to eq 'username'
+ expect(username.text).to eq 'username'
+ end
+ end
+
+ describe '#entry' do
+ shared_examples 'not root' do
+ include_examples 'no namespaces' do
+ def serialize
+ subject
+ end
+ end
+
+ it 'does not append author element' do
+ subject.nodes.each { |node| expect(node.name).not_to eq 'author' }
+ end
+ end
+
+ context 'it is root' do
+ include_examples 'namespaces' do
+ def serialize
+ stream_entry = Fabricate(:stream_entry)
+ AtomSerializer.new.entry(stream_entry, true)
+ end
+ end
+
+ it 'appends author element' do
+ account = Fabricate(:account, username: 'username')
+ status = Fabricate(:status, account: account)
+
+ entry = AtomSerializer.new.entry(status.stream_entry, true)
+
+ expect(entry.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username'
+ end
+ end
+
+ context 'if status is present' do
+ include_examples 'status attributes' do
+ def serialize(status)
+ AtomSerializer.new.entry(status.stream_entry, true)
+ end
+ end
+
+ it 'appends link element for the public collection if status is publicly visible' do
+ status = Fabricate(:status, visibility: :public)
+
+ entry = AtomSerializer.new.entry(status.stream_entry)
+
+ mentioned_person = entry.nodes.find do |node|
+ node.name == 'link' &&
+ node[:rel] == 'mentioned' &&
+ node['ostatus:object-type'] == TagManager::TYPES[:collection]
+ end
+ expect(mentioned_person[:href]).to eq TagManager::COLLECTIONS[:public]
+ end
+
+ it 'does not append link element for the public collection if status is not publicly visible' do
+ status = Fabricate(:status, visibility: :private)
+
+ entry = AtomSerializer.new.entry(status.stream_entry)
+
+ entry.nodes.each do |node|
+ if node.name == 'link' &&
+ node[:rel] == 'mentioned' &&
+ node['ostatus:object-type'] == TagManager::TYPES[:collection]
+ expect(mentioned_collection[:href]).not_to eq TagManager::COLLECTIONS[:public]
+ end
+ end
+ end
+
+ it 'appends category elements for tags' do
+ tag = Fabricate(:tag, name: 'tag')
+ status = Fabricate(:status, tags: [ tag ])
+
+ entry = AtomSerializer.new.entry(status.stream_entry)
+
+ expect(entry.category[:term]).to eq 'tag'
+ end
+
+ it 'appends category element for NSFW if status is sensitive' do
+ status = Fabricate(:status, sensitive: true)
+ entry = AtomSerializer.new.entry(status.stream_entry)
+ expect(entry.category[:term]).to eq 'nsfw'
+ end
+
+ it 'appends link elements for media attachments' do
+ file = attachment_fixture('attachment.jpg')
+ media_attachment = Fabricate(:media_attachment, file: file)
+ status = Fabricate(:status, media_attachments: [ media_attachment ])
+
+ entry = AtomSerializer.new.entry(status.stream_entry)
+
+ enclosure = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'enclosure' }
+ expect(enclosure[:type]).to eq 'image/jpeg'
+ expect(enclosure[:href]).to match /^https:\/\/cb6e6126.ngrok.io\/system\/media_attachments\/files\/.+\/original\/attachment.jpg$/
+ end
+
+ it 'appends mastodon:scope element with visibility' do
+ status = Fabricate(:status, visibility: :public)
+
+ entry = AtomSerializer.new.entry(status.stream_entry)
+
+ scope = entry.nodes.find { |node| node.name == 'mastodon:scope' }
+ expect(scope.text).to eq 'public'
+ end
+
+ it 'returns element whose rendered view triggers creation when processed' do
+ remote_account = Account.create!(username: 'username')
+ remote_status = Fabricate(:status, account: remote_account)
+ remote_status.stream_entry.update!(created_at: '2000-01-01T00:00:00Z')
+
+ entry = AtomSerializer.new.entry(remote_status.stream_entry, true)
+ xml = AtomSerializer.render(entry).gsub('cb6e6126.ngrok.io', 'remote')
+
+ remote_status.destroy!
+ remote_account.destroy!
+
+ account = Account.create!(
+ domain: 'remote',
+ username: 'username',
+ last_webfingered_at: Time.now.utc,
+ )
+
+ ProcessFeedService.new.call(xml, account)
+
+ expect(Status.find_by(uri: "tag:remote,2000-01-01:objectId=#{remote_status.id}:objectType=Status")).to be_instance_of Status
+ end
+ end
+
+ context 'if status is not present' do
+ it 'appends content element saying status is deleted' do
+ status = Fabricate(:status)
+ status.destroy!
+
+ entry = AtomSerializer.new.entry(status.stream_entry)
+
+ expect(entry.content.text).to eq 'Deleted status'
+ end
+
+ it 'appends title element saying the status is deleted' do
+ account = Fabricate(:account, username: 'username')
+ status = Fabricate(:status, account: account)
+ status.destroy!
+
+ entry = AtomSerializer.new.entry(status.stream_entry)
+
+ expect(entry.title.text).to eq 'username deleted status'
+ end
+ end
+
+ context 'it is not root' do
+ let(:stream_entry) { Fabricate(:stream_entry) }
+ subject { AtomSerializer.new.entry(stream_entry, false) }
+ include_examples 'not root'
+ end
+
+ context 'without root parameter' do
+ let(:stream_entry) { Fabricate(:stream_entry) }
+ subject { AtomSerializer.new.entry(stream_entry) }
+ include_examples 'not root'
+ end
+
+ it 'returns entry element' do
+ stream_entry = Fabricate(:stream_entry)
+ entry = AtomSerializer.new.entry(stream_entry)
+ expect(entry.name).to eq 'entry'
+ end
+
+ it 'appends id element with unique tag' do
+ status = Fabricate(:status, reblog_of_id: nil)
+ status.stream_entry.update!(created_at: '2000-01-01T00:00:00Z')
+
+ entry = AtomSerializer.new.entry(status.stream_entry)
+
+ expect(entry.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+ end
+
+ it 'appends published element with created date' do
+ stream_entry = Fabricate(:stream_entry, created_at: '2000-01-01T00:00:00Z')
+ entry = AtomSerializer.new.entry(stream_entry)
+ expect(entry.published.text).to eq '2000-01-01T00:00:00Z'
+ end
+
+ it 'appends updated element with updated date' do
+ stream_entry = Fabricate(:stream_entry, updated_at: '2000-01-01T00:00:00Z')
+ entry = AtomSerializer.new.entry(stream_entry)
+ expect(entry.updated.text).to eq '2000-01-01T00:00:00Z'
+ end
+
+ it 'appends title element with status title' do
+ account = Fabricate(:account, username: 'username')
+ status = Fabricate(:status, account: account, reblog_of_id: nil)
+ entry = AtomSerializer.new.entry(status.stream_entry)
+ expect(entry.title.text).to eq 'New status by username'
+ end
+
+ it 'appends activity:object-type element with object type' do
+ status = Fabricate(:status)
+ entry = AtomSerializer.new.entry(status.stream_entry)
+ object_type = entry.nodes.find { |node| node.name == 'activity:object-type' }
+ expect(object_type.text).to eq TagManager::TYPES[:note]
+ end
+
+ it 'appends activity:verb element with object type' do
+ status = Fabricate(:status)
+
+ entry = AtomSerializer.new.entry(status.stream_entry)
+
+ object_type = entry.nodes.find { |node| node.name == 'activity:verb' }
+ expect(object_type.text).to eq TagManager::VERBS[:post]
+ end
+
+ it 'appends activity:object element with target if present' do
+ reblogged = Fabricate(:status, created_at: '2000-01-01T00:00:00Z')
+ reblog = Fabricate(:status, reblog: reblogged)
+
+ entry = AtomSerializer.new.entry(reblog.stream_entry)
+
+ object = entry.nodes.find { |node| node.name == 'activity:object' }
+ expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{reblogged.id}:objectType=Status"
+ end
+
+ it 'does not append activity:object element if target is not present' do
+ status = Fabricate(:status, reblog_of_id: nil)
+ entry = AtomSerializer.new.entry(status.stream_entry)
+ entry.nodes.each { |node| expect(node.name).not_to eq 'activity:object' }
+ end
+
+ it 'appends link element for an alternative' do
+ account = Fabricate(:account, username: 'username')
+ status = Fabricate(:status, account: account)
+
+ entry = AtomSerializer.new.entry(status.stream_entry)
+
+ link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' }
+ expect(link[:type]).to eq 'text/html'
+ expect(link[:href]).to eq "https://cb6e6126.ngrok.io/users/username/updates/#{status.stream_entry.id}"
+ end
+
+ it 'appends link element for itself' do
+ account = Fabricate(:account, username: 'username')
+ status = Fabricate(:status, account: account)
+
+ entry = AtomSerializer.new.entry(status.stream_entry)
+
+ link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'self' }
+ expect(link[:type]).to eq 'application/atom+xml'
+ expect(link[:href]).to eq "https://cb6e6126.ngrok.io/users/username/updates/#{status.stream_entry.id}.atom"
+ end
+
+ it 'appends thr:in-reply-to element if threaded' do
+ in_reply_to_status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reblog_of_id: nil)
+ reply_status = Fabricate(:status, in_reply_to_id: in_reply_to_status.id)
+
+ entry = AtomSerializer.new.entry(reply_status.stream_entry)
+
+ in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' }
+ expect(in_reply_to[:ref]).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{in_reply_to_status.id}:objectType=Status"
+ end
+
+ it 'does not append thr:in-reply-to element if not threaded' do
+ status = Fabricate(:status)
+ entry = AtomSerializer.new.entry(status.stream_entry)
+ entry.nodes.each { |node| expect(node.name).not_to eq 'thr:in-reply-to' }
+ end
+
+ it 'appends ostatus:conversation if conversation id is present' do
+ status = Fabricate(:status)
+ status.conversation.update!(created_at: '2000-01-01T00:00:00Z')
+
+ entry = AtomSerializer.new.entry(status.stream_entry)
+
+ conversation = entry.nodes.find { |node| node.name == 'ostatus:conversation' }
+ expect(conversation[:ref]).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.conversation_id}:objectType=Conversation"
+ end
+
+ it 'does not append ostatus:conversation if conversation id is not present' do
+ status = Fabricate.build(:status, conversation_id: nil)
+ status.save!(validate: false)
+
+ entry = AtomSerializer.new.entry(status.stream_entry)
+
+ entry.nodes.each { |node| expect(node.name).not_to eq 'ostatus:conversation' }
+ end
end
- describe '#author' do
- it 'returns dumpable XML with emojis' do
- account = Fabricate(:account, display_name: '💩')
- xml = AtomSerializer.render(AtomSerializer.new.author(account))
+ describe '#feed' do
+ include_examples 'namespaces' do
+ def serialize
+ account = Fabricate(:account)
+ AtomSerializer.new.feed(account, [])
+ end
+ end
+
+ it 'returns feed element' do
+ account = Fabricate(:account)
+ feed = AtomSerializer.new.feed(account, [])
+ expect(feed.name).to eq 'feed'
+ end
+
+ it 'appends id element with account Atom URL' do
+ account = Fabricate(:account, username: 'username')
+ feed = AtomSerializer.new.feed(account, [])
+ expect(feed.id.text).to eq 'https://cb6e6126.ngrok.io/users/username.atom'
+ end
+
+ it 'appends title element with account display name if present' do
+ account = Fabricate(:account, display_name: 'display name')
+ feed = AtomSerializer.new.feed(account, [])
+ expect(feed.title.text).to eq 'display name'
+ end
+
+ it 'does not append title element with account username if account display name is not present' do
+ account = Fabricate(:account, display_name: '', username: 'username')
+ feed = AtomSerializer.new.feed(account, [])
+ expect(feed.title.text).to eq 'username'
+ end
+
+ it 'appends subtitle element with account note' do
+ account = Fabricate(:account, note: 'note')
+ feed = AtomSerializer.new.feed(account, [])
+ expect(feed.subtitle.text).to eq 'note'
+ end
+
+ it 'appends updated element with date account got updated' do
+ account = Fabricate(:account, updated_at: '2000-01-01T00:00:00Z')
+ feed = AtomSerializer.new.feed(account, [])
+ expect(feed.updated.text).to eq '2000-01-01T00:00:00Z'
+ end
+
+ it 'appends logo element with full asset URL for original account avatar' do
+ account = Fabricate(:account, avatar: attachment_fixture('avatar.gif'))
+ feed = AtomSerializer.new.feed(account, [])
+ expect(feed.logo.text).to match /^https:\/\/cb6e6126.ngrok.io\/system\/accounts\/avatars\/.+\/original\/avatar.gif/
+ end
+
+ it 'appends author element' do
+ account = Fabricate(:account, username: 'username')
+ feed = AtomSerializer.new.feed(account, [])
+ expect(feed.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username'
+ end
+
+ it 'appends link element for an alternative' do
+ account = Fabricate(:account, username: 'username')
+
+ feed = AtomSerializer.new.feed(account, [])
+
+ link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' }
+ expect(link[:type]).to eq 'text/html'
+ expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/@username'
+ end
+
+ it 'appends link element for itself' do
+ account = Fabricate(:account, username: 'username')
+
+ feed = AtomSerializer.new.feed(account, [])
+
+ link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'self' }
+ expect(link[:type]).to eq 'application/atom+xml'
+ expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/users/username.atom'
+ end
+
+ it 'appends link element for the next if it has 20 stream entries' do
+ account = Fabricate(:account, username: 'username')
+ stream_entry = Fabricate(:stream_entry)
+
+ feed = AtomSerializer.new.feed(account, Array.new(20, stream_entry))
+
+ link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'next' }
+ expect(link[:type]).to eq 'application/atom+xml'
+ expect(link[:href]).to eq "https://cb6e6126.ngrok.io/users/username.atom?max_id=#{stream_entry.id}"
+ end
+
+ it 'does not append link element for the next if it does not have 20 stream entries' do
+ account = Fabricate(:account, username: 'username')
+
+ feed = AtomSerializer.new.feed(account, [])
+
+ feed.nodes.each do |node|
+ expect(node[:rel]).not_to eq 'next' if node.name == 'link'
+ end
+ end
+
+ it 'appends link element for hub' do
+ account = Fabricate(:account, username: 'username')
+
+ feed = AtomSerializer.new.feed(account, [])
+
+ link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'hub' }
+ expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/api/push'
+ end
+
+ it 'appends link element for Salmon' do
+ account = Fabricate(:account, username: 'username')
+
+ feed = AtomSerializer.new.feed(account, [])
+
+ link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'salmon' }
+ expect(link[:href]).to start_with 'https://cb6e6126.ngrok.io/api/salmon/'
+ end
+
+ it 'appends stream entries' do
+ account = Fabricate(:account, username: 'username')
+ status = Fabricate(:status, account: account)
+
+ feed = AtomSerializer.new.feed(account, [status.stream_entry])
+
+ expect(feed.entry.title.text).to eq 'New status by username'
+ end
+ end
+
+ describe '#block_salmon' do
+ include_examples 'namespaces' do
+ def serialize
+ block = Fabricate(:block)
+ AtomSerializer.new.block_salmon(block)
+ end
+ end
+
+ it 'returns entry element' do
+ block = Fabricate(:block)
+ block_salmon = AtomSerializer.new.block_salmon(block)
+ expect(block_salmon.name).to eq 'entry'
+ end
+
+ it 'appends id element with unique tag' do
+ block = Fabricate(:block)
+
+ time_before = Time.now
+ block_salmon = AtomSerializer.new.block_salmon(block)
+ time_after = Time.now
+
+ expect(block_salmon.id.text).to(
+ eq(TagManager.instance.unique_tag(time_before.utc, block.id, 'Block'))
+ .or(eq(TagManager.instance.unique_tag(time_after.utc, block.id, 'Block')))
+ )
+ end
+
+ it 'appends title element with description' do
+ account = Fabricate(:account, domain: nil, username: 'account')
+ target_account = Fabricate(:account, domain: 'remote', username: 'target_account')
+ block = Fabricate(:block, account: account, target_account: target_account)
+
+ block_salmon = AtomSerializer.new.block_salmon(block)
+
+ expect(block_salmon.title.text).to eq 'account no longer wishes to interact with target_account@remote'
+ end
+
+ it 'appends author element with account' do
+ account = Fabricate(:account, domain: nil, username: 'account')
+ block = Fabricate(:block, account: account)
+
+ block_salmon = AtomSerializer.new.block_salmon(block)
- expect(xml).to be_a String
- expect(xml).to match(/💩<\/poco:displayName>/)
+ expect(block_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/account'
end
- it 'returns dumpable XML with invalid characters like \b and \v' do
- account = Fabricate(:account, display_name: "im l33t\b haxo\b\vr")
- xml = AtomSerializer.render(AtomSerializer.new.author(account))
+ it 'appends activity:object-type element with activity type' do
+ block = Fabricate(:block)
- expect(xml).to be_a String
- expect(xml).to match(/im l33t haxor<\/poco:displayName>/)
+ block_salmon = AtomSerializer.new.block_salmon(block)
+
+ object_type = block_salmon.nodes.find { |node| node.name == 'activity:object-type' }
+ expect(object_type.text).to eq TagManager::TYPES[:activity]
end
- end
- describe '#entry' do
- describe 'with deleted status' do
- let(:entry) do
- status = Fabricate(:status, account: author, text: 'boop')
- entry = status.stream_entry
- status.destroy
- entry
- end
+ it 'appends activity:verb element with block' do
+ block = Fabricate(:block)
- it 'returns dumpable XML' do
- xml = AtomSerializer.render(AtomSerializer.new.entry(entry, true))
- expect(xml).to be_a String
- expect(xml).to match(/#{TagManager.instance.unique_tag(entry.created_at, entry.activity_id, 'Status')}<\/id>/)
- end
+ block_salmon = AtomSerializer.new.block_salmon(block)
+
+ verb = block_salmon.nodes.find { |node| node.name == 'activity:verb' }
+ expect(verb.text).to eq TagManager::VERBS[:block]
+ end
+
+ it 'appends activity:object element with target account' do
+ target_account = Fabricate(:account, domain: 'domain', uri: 'https://domain/id')
+ block = Fabricate(:block, target_account: target_account)
+
+ block_salmon = AtomSerializer.new.block_salmon(block)
+
+ object = block_salmon.nodes.find { |node| node.name == 'activity:object' }
+ expect(object.id.text).to eq 'https://domain/id'
+ end
- it 'triggers delete when processed' do
- status = double(id: entry.activity_id)
- service = double
+ it 'returns element whose rendered view triggers block when processed' do
+ block = Fabricate(:block)
+ block_salmon = AtomSerializer.new.block_salmon(block)
+ xml = AtomSerializer.render(block_salmon)
+ envelope = OStatus2::Salmon.new.pack(xml, block.account.keypair)
+ block.destroy!
- allow(Status).to receive(:find_by).and_return(status)
- allow(RemoveStatusService).to receive(:new).and_return(service)
- allow(service).to receive(:call)
+ ProcessInteractionService.new.call(envelope, block.target_account)
- xml = AtomSerializer.render(AtomSerializer.new.entry(entry, true))
- ProcessFeedService.new.call(xml, author)
+ expect(block.account.blocking?(block.target_account)).to be true
+ end
+ end
- expect(service).to have_received(:call).with(status)
+ describe '#unblock_salmon' do
+ include_examples 'namespaces' do
+ def serialize
+ block = Fabricate(:block)
+ AtomSerializer.new.unblock_salmon(block)
end
end
- describe 'with reblog of local user' do
- it 'returns dumpable XML'
- it 'creates a reblog'
+ it 'returns entry element' do
+ block = Fabricate(:block)
+ unblock_salmon = AtomSerializer.new.unblock_salmon(block)
+ expect(unblock_salmon.name).to eq 'entry'
end
- describe 'with reblog of 3rd party user' do
- it 'returns dumpable XML'
- it 'creates a reblog with correct author'
+ it 'appends id element with unique tag' do
+ block = Fabricate(:block)
+
+ time_before = Time.now
+ unblock_salmon = AtomSerializer.new.unblock_salmon(block)
+ time_after = Time.now
+
+ expect(unblock_salmon.id.text).to(
+ eq(TagManager.instance.unique_tag(time_before.utc, block.id, 'Block'))
+ .or(eq(TagManager.instance.unique_tag(time_after.utc, block.id, 'Block')))
+ )
end
- end
- describe '#follow_salmon' do
- let(:xml) do
- follow = Fabricate(:follow, account: author, target_account: receiver)
- xml = AtomSerializer.render(AtomSerializer.new.follow_salmon(follow))
- follow.destroy
- xml
+ it 'appends title element with description' do
+ account = Fabricate(:account, domain: nil, username: 'account')
+ target_account = Fabricate(:account, domain: 'remote', username: 'target_account')
+ block = Fabricate(:block, account: account, target_account: target_account)
+
+ unblock_salmon = AtomSerializer.new.unblock_salmon(block)
+
+ expect(unblock_salmon.title.text).to eq 'account no longer blocks target_account@remote'
end
- it 'returns dumpable XML' do
- expect(xml).to be_a String
+ it 'appends author element with account' do
+ account = Fabricate(:account, domain: nil, username: 'account')
+ block = Fabricate(:block, account: account)
+
+ unblock_salmon = AtomSerializer.new.unblock_salmon(block)
+
+ expect(unblock_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/account'
end
- it 'triggers follow when processed' do
- envelope = OStatus2::Salmon.new.pack(xml, author.keypair)
- ProcessInteractionService.new.call(envelope, receiver)
- expect(author.following?(receiver)).to be true
+ it 'appends activity:object-type element with activity type' do
+ block = Fabricate(:block)
+
+ unblock_salmon = AtomSerializer.new.unblock_salmon(block)
+
+ object_type = unblock_salmon.nodes.find { |node| node.name == 'activity:object-type' }
+ expect(object_type.text).to eq TagManager::TYPES[:activity]
end
- end
- describe '#unfollow_salmon' do
- let(:xml) do
- follow = Fabricate(:follow, account: author, target_account: receiver)
- follow.destroy
- xml = AtomSerializer.render(AtomSerializer.new.unfollow_salmon(follow))
- author.follow!(receiver)
- xml
+ it 'appends activity:verb element with block' do
+ block = Fabricate(:block)
+
+ unblock_salmon = AtomSerializer.new.unblock_salmon(block)
+
+ verb = unblock_salmon.nodes.find { |node| node.name == 'activity:verb' }
+ expect(verb.text).to eq TagManager::VERBS[:unblock]
end
- it 'returns dumpable XML' do
- expect(xml).to be_a String
+ it 'appends activity:object element with target account' do
+ target_account = Fabricate(:account, domain: 'domain', uri: 'https://domain/id')
+ block = Fabricate(:block, target_account: target_account)
+
+ unblock_salmon = AtomSerializer.new.unblock_salmon(block)
+
+ object = unblock_salmon.nodes.find { |node| node.name == 'activity:object' }
+ expect(object.id.text).to eq 'https://domain/id'
end
- it 'triggers unfollow when processed' do
- envelope = OStatus2::Salmon.new.pack(xml, author.keypair)
- ProcessInteractionService.new.call(envelope, receiver)
- expect(author.following?(receiver)).to be false
+ it 'returns element whose rendered view triggers block when processed' do
+ block = Fabricate(:block)
+ unblock_salmon = AtomSerializer.new.unblock_salmon(block)
+ xml = AtomSerializer.render(unblock_salmon)
+ envelope = OStatus2::Salmon.new.pack(xml, block.account.keypair)
+
+ ProcessInteractionService.new.call(envelope, block.target_account)
+
+ expect{ block.reload }.to raise_error ActiveRecord::RecordNotFound
end
end
describe '#favourite_salmon' do
- let(:status) { Fabricate(:status, account: receiver, text: 'Everything by design.') }
+ include_examples 'namespaces' do
+ def serialize
+ favourite = Fabricate(:favourite)
+ AtomSerializer.new.favourite_salmon(favourite)
+ end
+ end
+
+ it 'returns entry element' do
+ favourite = Fabricate(:favourite)
+ favourite_salmon = AtomSerializer.new.favourite_salmon(favourite)
+ expect(favourite_salmon.name).to eq 'entry'
+ end
+
+ it 'appends id element with unique tag' do
+ favourite = Fabricate(:favourite, created_at: '2000-01-01T00:00:00Z')
+ favourite_salmon = AtomSerializer.new.favourite_salmon(favourite)
+ expect(favourite_salmon.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{favourite.id}:objectType=Favourite"
+ end
+
+ it 'appends author element with account' do
+ account = Fabricate(:account, domain: nil, username: 'username')
+ favourite = Fabricate(:favourite, account: account)
+
+ favourite_salmon = AtomSerializer.new.favourite_salmon(favourite)
+
+ expect(favourite_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username'
+ end
+
+ it 'appends activity:object-type element with activity type' do
+ favourite = Fabricate(:favourite)
+
+ favourite_salmon = AtomSerializer.new.favourite_salmon(favourite)
+
+ object_type = favourite_salmon.nodes.find { |node| node.name == 'activity:object-type' }
+ expect(object_type.text).to eq 'http://activitystrea.ms/schema/1.0/activity'
+ end
- let(:xml) do
- favourite = Fabricate(:favourite, account: author, status: status)
- xml = AtomSerializer.render(AtomSerializer.new.favourite_salmon(favourite))
- favourite.destroy
- xml
+ it 'appends activity:verb element with favorite' do
+ favourite = Fabricate(:favourite)
+
+ favourite_salmon = AtomSerializer.new.favourite_salmon(favourite)
+
+ verb = favourite_salmon.nodes.find { |node| node.name == 'activity:verb' }
+ expect(verb.text).to eq TagManager::VERBS[:favorite]
+ end
+
+ it 'appends activity:object element with status' do
+ status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z')
+ favourite = Fabricate(:favourite, status: status)
+
+ favourite_salmon = AtomSerializer.new.favourite_salmon(favourite)
+
+ object = favourite_salmon.nodes.find { |node| node.name == 'activity:object' }
+ expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+ end
+
+ it 'appends thr:in-reply-to element for status' do
+ status_account = Fabricate(:account, username: 'username')
+ status = Fabricate(:status, account: status_account, created_at: '2000-01-01T00:00:00Z')
+ favourite = Fabricate(:favourite, status: status)
+
+ favourite_salmon = AtomSerializer.new.favourite_salmon(favourite)
+
+ in_reply_to = favourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' }
+ expect(in_reply_to.ref).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+ expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
end
- it 'returns dumpable XML' do
- expect(xml).to be_a String
+ it 'includes description' do
+ account = Fabricate(:account, domain: nil, username: 'account')
+ status_account = Fabricate(:account, domain: 'remote', username: 'status_account')
+ status = Fabricate(:status, account: status_account)
+ favourite = Fabricate(:favourite, account: account, status: status)
+
+ favourite_salmon = AtomSerializer.new.favourite_salmon(favourite)
+
+ expect(favourite_salmon.title.text).to eq 'account favourited a status by status_account@remote'
+ expect(favourite_salmon.content.text).to eq 'account favourited a status by status_account@remote'
end
- it 'triggers favourite when processed' do
- envelope = OStatus2::Salmon.new.pack(xml, author.keypair)
- ProcessInteractionService.new.call(envelope, receiver)
- expect(author.favourited?(status)).to be true
+ it 'returns element whose rendered view triggers favourite when processed' do
+ favourite = Fabricate(:favourite)
+ favourite_salmon = AtomSerializer.new.favourite_salmon(favourite)
+ xml = AtomSerializer.render(favourite_salmon)
+ envelope = OStatus2::Salmon.new.pack(xml, favourite.account.keypair)
+ favourite.destroy!
+
+ ProcessInteractionService.new.call(envelope, favourite.status.account)
+ expect(favourite.account.favourited?(favourite.status)).to be true
end
end
describe '#unfavourite_salmon' do
- let(:status) { Fabricate(:status, account: receiver, text: 'Perfect harmony.') }
+ include_examples 'namespaces' do
+ def serialize
+ favourite = Fabricate(:favourite)
+ AtomSerializer.new.favourite_salmon(favourite)
+ end
+ end
+
+ it 'returns entry element' do
+ favourite = Fabricate(:favourite)
+ unfavourite_salmon = AtomSerializer.new.unfavourite_salmon(favourite)
+ expect(unfavourite_salmon.name).to eq 'entry'
+ end
+
+ it 'appends id element with unique tag' do
+ favourite = Fabricate(:favourite)
+
+ time_before = Time.now
+ unfavourite_salmon = AtomSerializer.new.unfavourite_salmon(favourite)
+ time_after = Time.now
+
+ expect(unfavourite_salmon.id.text).to(
+ eq(TagManager.instance.unique_tag(time_before.utc, favourite.id, 'Favourite'))
+ .or(eq(TagManager.instance.unique_tag(time_after.utc, favourite.id, 'Favourite')))
+ )
+ end
+
+ it 'appends author element with account' do
+ account = Fabricate(:account, domain: nil, username: 'username')
+ favourite = Fabricate(:favourite, account: account)
+
+ unfavourite_salmon = AtomSerializer.new.unfavourite_salmon(favourite)
+
+ expect(unfavourite_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username'
+ end
+
+ it 'appends activity:object-type element with activity type' do
+ favourite = Fabricate(:favourite)
+
+ unfavourite_salmon = AtomSerializer.new.unfavourite_salmon(favourite)
+
+ object_type = unfavourite_salmon.nodes.find { |node| node.name == 'activity:object-type' }
+ expect(object_type.text).to eq 'http://activitystrea.ms/schema/1.0/activity'
+ end
+
+ it 'appends activity:verb element with favorite' do
+ favourite = Fabricate(:favourite)
+
+ unfavourite_salmon = AtomSerializer.new.unfavourite_salmon(favourite)
+
+ verb = unfavourite_salmon.nodes.find { |node| node.name == 'activity:verb' }
+ expect(verb.text).to eq TagManager::VERBS[:unfavorite]
+ end
+
+ it 'appends activity:object element with status' do
+ status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z')
+ favourite = Fabricate(:favourite, status: status)
+
+ unfavourite_salmon = AtomSerializer.new.unfavourite_salmon(favourite)
+
+ object = unfavourite_salmon.nodes.find { |node| node.name == 'activity:object' }
+ expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+ end
+
+ it 'appends thr:in-reply-to element for status' do
+ status_account = Fabricate(:account, username: 'username')
+ status = Fabricate(:status, account: status_account, created_at: '2000-01-01T00:00:00Z')
+ favourite = Fabricate(:favourite, status: status)
- let(:xml) do
- favourite = Fabricate(:favourite, account: author, status: status)
- favourite.destroy
- xml = AtomSerializer.render(AtomSerializer.new.unfavourite_salmon(favourite))
- Fabricate(:favourite, account: author, status: status)
- xml
+ unfavourite_salmon = AtomSerializer.new.unfavourite_salmon(favourite)
+
+ in_reply_to = unfavourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' }
+ expect(in_reply_to.ref).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+ expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
end
- it 'returns dumpable XML' do
- expect(xml).to be_a String
+ it 'includes description' do
+ account = Fabricate(:account, domain: nil, username: 'account')
+ status_account = Fabricate(:account, domain: 'remote', username: 'status_account')
+ status = Fabricate(:status, account: status_account)
+ favourite = Fabricate(:favourite, account: account, status: status)
+
+ unfavourite_salmon = AtomSerializer.new.unfavourite_salmon(favourite)
+
+ expect(unfavourite_salmon.title.text).to eq 'account no longer favourites a status by status_account@remote'
+ expect(unfavourite_salmon.content.text).to eq 'account no longer favourites a status by status_account@remote'
end
- it 'triggers unfavourite when processed' do
- envelope = OStatus2::Salmon.new.pack(xml, author.keypair)
- ProcessInteractionService.new.call(envelope, receiver)
- expect(author.favourited?(status)).to be false
+ it 'returns element whose rendered view triggers unfavourite when processed' do
+ favourite = Fabricate(:favourite)
+ unfavourite_salmon = AtomSerializer.new.unfavourite_salmon(favourite)
+ xml = AtomSerializer.render(unfavourite_salmon)
+ envelope = OStatus2::Salmon.new.pack(xml, favourite.account.keypair)
+
+ ProcessInteractionService.new.call(envelope, favourite.status.account)
+ expect { favourite.reload }.to raise_error ActiveRecord::RecordNotFound
end
end
- describe '#block_salmon' do
- let(:xml) do
- block = Fabricate(:block, account: author, target_account: receiver)
- xml = AtomSerializer.render(AtomSerializer.new.block_salmon(block))
- block.destroy
- xml
+ describe '#follow_salmon' do
+ include_examples 'namespaces' do
+ def serialize
+ follow = Fabricate(:follow)
+ AtomSerializer.new.follow_salmon(follow)
+ end
+ end
+
+ it 'returns entry element' do
+ follow = Fabricate(:follow)
+ follow_salmon = AtomSerializer.new.follow_salmon(follow)
+ expect(follow_salmon.name).to eq 'entry'
+ end
+
+ it 'appends id element with unique tag' do
+ follow = Fabricate(:follow, created_at: '2000-01-01T00:00:00Z')
+ follow_salmon = AtomSerializer.new.follow_salmon(follow)
+ expect(follow_salmon.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{follow.id}:objectType=Follow"
+ end
+
+ it 'appends author element with account' do
+ account = Fabricate(:account, domain: nil, username: 'username')
+ follow = Fabricate(:follow, account: account)
+
+ follow_salmon = AtomSerializer.new.follow_salmon(follow)
+
+ expect(follow_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username'
+ end
+
+ it 'appends activity:object-type element with activity type' do
+ follow = Fabricate(:follow)
+
+ follow_salmon = AtomSerializer.new.follow_salmon(follow)
+
+ object_type = follow_salmon.nodes.find { |node| node.name == 'activity:object-type' }
+ expect(object_type.text).to eq TagManager::TYPES[:activity]
+ end
+
+ it 'appends activity:verb element with follow' do
+ follow = Fabricate(:follow)
+
+ follow_salmon = AtomSerializer.new.follow_salmon(follow)
+
+ verb = follow_salmon.nodes.find { |node| node.name == 'activity:verb' }
+ expect(verb.text).to eq TagManager::VERBS[:follow]
+ end
+
+ it 'appends activity:object element with target account' do
+ target_account = Fabricate(:account, domain: 'domain', uri: 'https://domain/id')
+ follow = Fabricate(:follow, target_account: target_account)
+
+ follow_salmon = AtomSerializer.new.follow_salmon(follow)
+
+ object = follow_salmon.nodes.find { |node| node.name == 'activity:object' }
+ expect(object.id.text).to eq 'https://domain/id'
end
- it 'returns dumpable XML' do
- expect(xml).to be_a String
+ it 'includes description' do
+ account = Fabricate(:account, domain: nil, username: 'account')
+ target_account = Fabricate(:account, domain: 'remote', username: 'target_account')
+ follow = Fabricate(:follow, account: account, target_account: target_account)
+
+ follow_salmon = AtomSerializer.new.follow_salmon(follow)
+
+ expect(follow_salmon.title.text).to eq 'account started following target_account@remote'
+ expect(follow_salmon.content.text).to eq 'account started following target_account@remote'
end
- it 'triggers block when processed' do
- envelope = OStatus2::Salmon.new.pack(xml, author.keypair)
- ProcessInteractionService.new.call(envelope, receiver)
- expect(author.blocking?(receiver)).to be true
+ it 'returns element whose rendered view triggers follow when processed' do
+ follow = Fabricate(:follow)
+ follow_salmon = AtomSerializer.new.follow_salmon(follow)
+ xml = AtomSerializer.render(follow_salmon)
+ follow.destroy!
+ envelope = OStatus2::Salmon.new.pack(xml, follow.account.keypair)
+
+ ProcessInteractionService.new.call(envelope, follow.target_account)
+
+ expect(follow.account.following?(follow.target_account)).to be true
end
end
- describe '#unblock_salmon' do
- let(:xml) do
- block = Fabricate(:block, account: author, target_account: receiver)
- block.destroy
- xml = AtomSerializer.render(AtomSerializer.new.unblock_salmon(block))
- author.block!(receiver)
- xml
+ describe '#unfollow_salmon' do
+ include_examples 'namespaces' do
+ def serialize
+ follow = Fabricate(:follow)
+ follow.destroy!
+ AtomSerializer.new.unfollow_salmon(follow)
+ end
+ end
+
+ it 'returns entry element' do
+ follow = Fabricate(:follow)
+ follow.destroy!
+
+ unfollow_salmon = AtomSerializer.new.unfollow_salmon(follow)
+
+ expect(unfollow_salmon.name).to eq 'entry'
+ end
+
+ it 'appends id element with unique tag' do
+ follow = Fabricate(:follow)
+ follow.destroy!
+
+ time_before = Time.now
+ unfollow_salmon = AtomSerializer.new.unfollow_salmon(follow)
+ time_after = Time.now
+
+ expect(unfollow_salmon.id.text).to(
+ eq(TagManager.instance.unique_tag(time_before.utc, follow.id, 'Follow'))
+ .or(eq(TagManager.instance.unique_tag(time_after.utc, follow.id, 'Follow')))
+ )
end
- it 'returns dumpable XML' do
- expect(xml).to be_a String
+ it 'appends title element with description' do
+ account = Fabricate(:account, domain: nil, username: 'account')
+ target_account = Fabricate(:account, domain: 'remote', username: 'target_account')
+ follow = Fabricate(:follow, account: account, target_account: target_account)
+ follow.destroy!
+
+ unfollow_salmon = AtomSerializer.new.unfollow_salmon(follow)
+
+ expect(unfollow_salmon.title.text).to eq 'account is no longer following target_account@remote'
+ end
+
+ it 'appends content element with description' do
+ account = Fabricate(:account, domain: nil, username: 'account')
+ target_account = Fabricate(:account, domain: 'remote', username: 'target_account')
+ follow = Fabricate(:follow, account: account, target_account: target_account)
+ follow.destroy!
+
+ unfollow_salmon = AtomSerializer.new.unfollow_salmon(follow)
+
+ expect(unfollow_salmon.content.text).to eq 'account is no longer following target_account@remote'
+ end
+
+ it 'appends author element with account' do
+ account = Fabricate(:account, domain: nil, username: 'username')
+ follow = Fabricate(:follow, account: account)
+ follow.destroy!
+
+ unfollow_salmon = AtomSerializer.new.unfollow_salmon(follow)
+
+ expect(unfollow_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username'
+ end
+
+ it 'appends activity:object-type element with activity type' do
+ follow = Fabricate(:follow)
+ follow.destroy!
+
+ unfollow_salmon = AtomSerializer.new.unfollow_salmon(follow)
+
+ object_type = unfollow_salmon.nodes.find { |node| node.name == 'activity:object-type' }
+ expect(object_type.text).to eq TagManager::TYPES[:activity]
+ end
+
+ it 'appends activity:verb element with follow' do
+ follow = Fabricate(:follow)
+ follow.destroy!
+
+ unfollow_salmon = AtomSerializer.new.unfollow_salmon(follow)
+
+ verb = unfollow_salmon.nodes.find { |node| node.name == 'activity:verb' }
+ expect(verb.text).to eq TagManager::VERBS[:unfollow]
+ end
+
+ it 'appends activity:object element with target account' do
+ target_account = Fabricate(:account, domain: 'domain', uri: 'https://domain/id')
+ follow = Fabricate(:follow, target_account: target_account)
+ follow.destroy!
+
+ unfollow_salmon = AtomSerializer.new.unfollow_salmon(follow)
+
+ object = unfollow_salmon.nodes.find { |node| node.name == 'activity:object' }
+ expect(object.id.text).to eq 'https://domain/id'
end
- it 'triggers unblock when processed' do
- envelope = OStatus2::Salmon.new.pack(xml, author.keypair)
- ProcessInteractionService.new.call(envelope, receiver)
- expect(author.blocking?(receiver)).to be false
+ it 'returns element whose rendered view triggers unfollow when processed' do
+ follow = Fabricate(:follow)
+ follow.destroy!
+ unfollow_salmon = AtomSerializer.new.unfollow_salmon(follow)
+ xml = AtomSerializer.render(unfollow_salmon)
+ follow.account.follow!(follow.target_account)
+ envelope = OStatus2::Salmon.new.pack(xml, follow.account.keypair)
+
+ ProcessInteractionService.new.call(envelope, follow.target_account)
+
+ expect(follow.account.following?(follow.target_account)).to be false
end
end
describe '#follow_request_salmon' do
- it 'returns dumpable XML'
- it 'triggers follow request when processed'
+ include_examples 'namespaces' do
+ def serialize
+ follow_request = Fabricate(:follow_request)
+ AtomSerializer.new.follow_request_salmon(follow_request)
+ end
+ end
+
+ context do
+ def serialize(follow_request)
+ AtomSerializer.new.follow_request_salmon(follow_request)
+ end
+
+ it_behaves_like 'follow request salmon'
+
+ it 'appends id element with unique tag' do
+ follow_request = Fabricate(:follow_request, created_at: '2000-01-01T00:00:00Z')
+ follow_request_salmon = serialize(follow_request)
+ expect(follow_request_salmon.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{follow_request.id}:objectType=FollowRequest"
+ end
+
+ it 'appends title element with description' do
+ account = Fabricate(:account, domain: nil, username: 'account')
+ target_account = Fabricate(:account, domain: 'remote', username: 'target_account')
+ follow_request = Fabricate(:follow_request, account: account, target_account: target_account)
+ follow_request_salmon = serialize(follow_request)
+ expect(follow_request_salmon.title.text).to eq 'account requested to follow target_account@remote'
+ end
+
+ it 'returns element whose rendered view triggers follow request when processed' do
+ follow_request = Fabricate(:follow_request)
+ follow_request_salmon = serialize(follow_request)
+ xml = AtomSerializer.render(follow_request_salmon)
+ envelope = OStatus2::Salmon.new.pack(xml, follow_request.account.keypair)
+ follow_request.destroy!
+
+ ProcessInteractionService.new.call(envelope, follow_request.target_account)
+
+ expect(follow_request.account.requested?(follow_request.target_account)).to eq true
+ end
+ end
end
describe '#authorize_follow_request_salmon' do
- it 'returns dumpable XML'
- it 'creates follow from follow request when processed'
+ include_examples 'namespaces' do
+ def serialize
+ follow_request = Fabricate(:follow_request)
+ AtomSerializer.new.authorize_follow_request_salmon(follow_request)
+ end
+ end
+
+ it_behaves_like 'follow request salmon' do
+ def serialize(follow_request)
+ authorize_follow_request_salmon = AtomSerializer.new.authorize_follow_request_salmon(follow_request)
+ authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:object' }
+ end
+ end
+
+ it 'appends id element with unique tag' do
+ follow_request = Fabricate(:follow_request)
+
+ time_before = Time.now
+ authorize_follow_request_salmon = AtomSerializer.new.authorize_follow_request_salmon(follow_request)
+ time_after = Time.now
+
+ expect(authorize_follow_request_salmon.id.text).to(
+ eq(TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest'))
+ .or(eq(TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest')))
+ )
+ end
+
+ it 'appends title element with description' do
+ account = Fabricate(:account, domain: 'remote', username: 'account')
+ target_account = Fabricate(:account, domain: nil, username: 'target_account')
+ follow_request = Fabricate(:follow_request, account: account, target_account: target_account)
+
+ authorize_follow_request_salmon = AtomSerializer.new.authorize_follow_request_salmon(follow_request)
+
+ expect(authorize_follow_request_salmon.title.text).to eq 'target_account authorizes follow request by account@remote'
+ end
+
+ it 'appends activity:object-type element with activity type' do
+ follow_request = Fabricate(:follow_request)
+
+ authorize_follow_request_salmon = AtomSerializer.new.authorize_follow_request_salmon(follow_request)
+
+ object_type = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' }
+ expect(object_type.text).to eq TagManager::TYPES[:activity]
+ end
+
+ it 'appends activity:verb element with authorize' do
+ follow_request = Fabricate(:follow_request)
+
+ authorize_follow_request_salmon = AtomSerializer.new.authorize_follow_request_salmon(follow_request)
+
+ verb = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' }
+ expect(verb.text).to eq TagManager::VERBS[:authorize]
+ end
+
+ it 'returns element whose rendered view creates follow from follow request when processed' do
+ follow_request = Fabricate(:follow_request)
+ authorize_follow_request_salmon = AtomSerializer.new.authorize_follow_request_salmon(follow_request)
+ xml = AtomSerializer.render(authorize_follow_request_salmon)
+ envelope = OStatus2::Salmon.new.pack(xml, follow_request.target_account.keypair)
+
+ ProcessInteractionService.new.call(envelope, follow_request.account)
+
+ expect(follow_request.account.following?(follow_request.target_account)).to eq true
+ expect { follow_request.reload }.to raise_error ActiveRecord::RecordNotFound
+ end
end
describe '#reject_follow_request_salmon' do
- it 'returns dumpable XML'
- it 'deletes follow request when processed'
+ include_examples 'namespaces' do
+ def serialize
+ follow_request = Fabricate(:follow_request)
+ AtomSerializer.new.reject_follow_request_salmon(follow_request)
+ end
+ end
+
+ it_behaves_like 'follow request salmon' do
+ def serialize(follow_request)
+ reject_follow_request_salmon = AtomSerializer.new.reject_follow_request_salmon(follow_request)
+ reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:object' }
+ end
+ end
+
+ it 'appends id element with unique tag' do
+ follow_request = Fabricate(:follow_request)
+
+ time_before = Time.now
+ reject_follow_request_salmon = AtomSerializer.new.reject_follow_request_salmon(follow_request)
+ time_after = Time.now
+
+ expect(reject_follow_request_salmon.id.text).to(
+ eq(TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest'))
+ .or(TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest'))
+ )
+ end
+
+ it 'appends title element with description' do
+ account = Fabricate(:account, domain: 'remote', username: 'account')
+ target_account = Fabricate(:account, domain: nil, username: 'target_account')
+ follow_request = Fabricate(:follow_request, account: account, target_account: target_account)
+ reject_follow_request_salmon = AtomSerializer.new.reject_follow_request_salmon(follow_request)
+ expect(reject_follow_request_salmon.title.text).to eq 'target_account rejects follow request by account@remote'
+ end
+
+ it 'appends activity:object-type element with activity type' do
+ follow_request = Fabricate(:follow_request)
+ reject_follow_request_salmon = AtomSerializer.new.reject_follow_request_salmon(follow_request)
+ object_type = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' }
+ expect(object_type.text).to eq TagManager::TYPES[:activity]
+ end
+
+ it 'appends activity:verb element with authorize' do
+ follow_request = Fabricate(:follow_request)
+ reject_follow_request_salmon = AtomSerializer.new.reject_follow_request_salmon(follow_request)
+ verb = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' }
+ expect(verb.text).to eq TagManager::VERBS[:reject]
+ end
+
+ it 'returns element whose rendered view deletes follow request when processed' do
+ follow_request = Fabricate(:follow_request)
+ reject_follow_request_salmon = AtomSerializer.new.reject_follow_request_salmon(follow_request)
+ xml = AtomSerializer.render(reject_follow_request_salmon)
+ envelope = OStatus2::Salmon.new.pack(xml, follow_request.target_account.keypair)
+
+ ProcessInteractionService.new.call(envelope, follow_request.account)
+
+ expect(follow_request.account.following?(follow_request.target_account)).to eq false
+ expect { follow_request.reload }.to raise_error ActiveRecord::RecordNotFound
+ end
+ end
+
+ describe '#object' do
+ include_examples 'status attributes' do
+ def serialize(status)
+ AtomSerializer.new.object(status)
+ end
+ end
+
+ it 'returns activity:object element' do
+ status = Fabricate(:status)
+ object = AtomSerializer.new.object(status)
+ expect(object.name).to eq 'activity:object'
+ end
+
+ it 'appends id element with URL for status' do
+ status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z')
+ object = AtomSerializer.new.object(status)
+ expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+ end
+
+ it 'appends published element with created date' do
+ status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z')
+ object = AtomSerializer.new.object(status)
+ expect(object.published.text).to eq '2000-01-01T00:00:00Z'
+ end
+
+ it 'appends updated element with updated date' do
+ status = Fabricate(:status, updated_at: '2000-01-01T00:00:00Z')
+ object = AtomSerializer.new.object(status)
+ expect(object.updated.text).to eq '2000-01-01T00:00:00Z'
+ end
+
+ it 'appends title element with title' do
+ account = Fabricate(:account, username: 'username')
+ status = Fabricate(:status, account: account)
+
+ object = AtomSerializer.new.object(status)
+
+ expect(object.title.text).to eq 'New status by username'
+ end
+
+ it 'appends author element with account' do
+ account = Fabricate(:account, username: 'username')
+ status = Fabricate(:status, account: account)
+
+ entry = AtomSerializer.new.object(status)
+
+ expect(entry.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username'
+ end
+
+ it 'appends activity:object-type element with object type' do
+ status = Fabricate(:status)
+
+ entry = AtomSerializer.new.object(status)
+
+ object_type = entry.nodes.find { |node| node.name == 'activity:object-type' }
+ expect(object_type.text).to eq TagManager::TYPES[:note]
+ end
+
+ it 'appends activity:verb element with verb' do
+ status = Fabricate(:status)
+
+ entry = AtomSerializer.new.object(status)
+
+ object_type = entry.nodes.find { |node| node.name == 'activity:verb' }
+ expect(object_type.text).to eq TagManager::VERBS[:post]
+ end
+
+ it 'appends link element for an alternative' do
+ account = Fabricate(:account, username: 'username')
+ status = Fabricate(:status, account: account)
+
+ entry = AtomSerializer.new.object(status)
+
+ link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' }
+ expect(link[:type]).to eq 'text/html'
+ expect(link[:href]).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
+ end
+
+ it 'appends thr:in-reply-to element if it is a reply and thread is not nil' do
+ account = Fabricate(:account, username: 'username')
+ thread = Fabricate(:status, account: account, created_at: '2000-01-01T00:00:00Z')
+ reply = Fabricate(:status, thread: thread)
+
+ entry = AtomSerializer.new.object(reply)
+
+ in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' }
+ expect(in_reply_to.ref).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{thread.id}:objectType=Status"
+ expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{thread.id}"
+ end
+
+ it 'does not append thr:in-reply-to element if thread is nil' do
+ status = Fabricate(:status, thread: nil)
+ entry = AtomSerializer.new.object(status)
+ entry.nodes.each { |node| expect(node.name).not_to eq 'thr:in-reply-to' }
+ end
+
+ it 'does not append ostatus:conversation element if conversation_id is nil' do
+ status = Fabricate.build(:status, conversation_id: nil)
+ status.save!(validate: false)
+
+ entry = AtomSerializer.new.object(status)
+
+ entry.nodes.each { |node| expect(node.name).not_to eq 'ostatus:conversation' }
+ end
+
+ it 'appends ostatus:conversation element if conversation_id is not nil' do
+ status = Fabricate(:status)
+ status.conversation.update!(created_at: '2000-01-01T00:00:00Z')
+
+ entry = AtomSerializer.new.object(status)
+
+ conversation = entry.nodes.find { |node| node.name == 'ostatus:conversation' }
+ expect(conversation[:ref]).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.conversation.id}:objectType=Conversation"
+ end
end
end
diff --git a/spec/lib/extractor_spec.rb b/spec/lib/extractor_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dba4bd0bbcb5c7738557e8a0a07df7b5e2db426c
--- /dev/null
+++ b/spec/lib/extractor_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Extractor do
+ describe 'extract_mentions_or_lists_with_indices' do
+ it 'returns an empty array if the given string does not have at signs' do
+ text = 'a string without at signs'
+ extracted = Extractor.extract_mentions_or_lists_with_indices(text)
+ expect(extracted).to eq []
+ end
+
+ it 'does not extract mentions which ends with particular characters' do
+ text = '@screen_name@'
+ extracted = Extractor.extract_mentions_or_lists_with_indices(text)
+ expect(extracted).to eq []
+ end
+
+ it 'returns mentions as an array' do
+ text = '@screen_name'
+ extracted = Extractor.extract_mentions_or_lists_with_indices(text)
+ expect(extracted).to eq [
+ { screen_name: 'screen_name', indices: [ 0, 12 ] }
+ ]
+ end
+
+ it 'yields mentions if a block is given' do
+ text = '@screen_name'
+ Extractor.extract_mentions_or_lists_with_indices(text) do |screen_name, start_position, end_position|
+ expect(screen_name).to eq 'screen_name'
+ expect(start_position).to eq 0
+ expect(end_position).to eq 12
+ end
+ end
+ end
+
+ describe 'extract_hashtags_with_indices' do
+ it 'returns an empty array if it does not have #' do
+ text = 'a string without hash sign'
+ extracted = Extractor.extract_hashtags_with_indices(text)
+ expect(extracted).to eq []
+ end
+
+ it 'does not exclude normal hash text before ://' do
+ text = '#hashtag://'
+ extracted = Extractor.extract_hashtags_with_indices(text)
+ expect(extracted).to eq [ { hashtag: 'hashtag', indices: [ 0, 8 ] } ]
+ end
+
+ it 'excludes http://' do
+ text = '#hashtaghttp://'
+ extracted = Extractor.extract_hashtags_with_indices(text)
+ expect(extracted).to eq [ { hashtag: 'hashtag', indices: [ 0, 8 ] } ]
+ end
+
+ it 'excludes https://' do
+ text = '#hashtaghttps://'
+ extracted = Extractor.extract_hashtags_with_indices(text)
+ expect(extracted).to eq [ { hashtag: 'hashtag', indices: [ 0, 8 ] } ]
+ end
+
+ it 'yields hashtags if a block is given' do
+ text = '#hashtag'
+ Extractor.extract_hashtags_with_indices(text) do |hashtag, start_position, end_position|
+ expect(hashtag).to eq 'hashtag'
+ expect(start_position).to eq 0
+ expect(end_position).to eq 8
+ end
+ end
+ end
+
+ describe 'extract_cashtags_with_indices' do
+ it 'returns []' do
+ text = '$cashtag'
+ extracted = Extractor.extract_cashtags_with_indices(text)
+ expect(extracted).to eq []
+ end
+ end
+end
diff --git a/spec/lib/formatter_spec.rb b/spec/lib/formatter_spec.rb
index ec61eaa43a022ef159823e51ab49504233c3f41a..dfe1d8b8fe9b2c39d32ac7bf9ba97538ac7275da 100644
--- a/spec/lib/formatter_spec.rb
+++ b/spec/lib/formatter_spec.rb
@@ -1,193 +1,293 @@
require 'rails_helper'
RSpec.describe Formatter do
- let(:account) { Fabricate(:account, username: 'alice') }
- let(:local_text) { 'Hello world http://google.com' }
- let(:local_status) { Fabricate(:status, text: local_text, account: account) }
- let(:remote_status) { Fabricate(:status, text: ' Beep boop', uri: 'beepboop', account: account) }
-
- let(:local_text_with_mention) { "@#{account.username} @#{account.username}@example.com #{local_text}?x=@#{account.username} #hashtag" }
-
- let(:local_status_with_mention) do
- Fabricate(
- :status,
- text: local_text_with_mention,
- account: account,
- mentions: [Fabricate(:mention, account: account)]
- )
- end
+ let(:local_account) { Fabricate(:account, domain: nil, username: 'alice') }
+ let(:remote_account) { Fabricate(:account, domain: 'remote', username: 'bob', url: 'https://remote/') }
- describe '#format' do
- subject { Formatter.instance.format(local_status) }
+ shared_examples 'encode and link URLs' do
+ context 'matches a stand-alone medium URL' do
+ let(:text) { 'https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4' }
- context 'with standalone status' do
- it 'returns a string' do
- expect(subject).to be_a String
+ it 'has valid URL' do
+ is_expected.to include 'href="https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4"'
end
+ end
- it 'contains plain text' do
- expect(subject).to match('Hello world')
+ context 'matches a stand-alone google URL' do
+ let(:text) { 'http://google.com' }
+
+ it 'has valid URL' do
+ is_expected.to include 'href="http://google.com/"'
end
+ end
+
+ context 'matches a stand-alone IDN URL' do
+ let(:text) { 'https://nic.みんな/' }
- it 'contains a link' do
- expect(subject).to match('http://google.com/')
+ it 'has valid URL' do
+ is_expected.to include 'href="https://nic.xn--q9jyb4c/"'
end
- it 'contains a mention' do
- result = Formatter.instance.format(local_status_with_mention)
- expect(result).to match "@#{account.username}"
- expect(result).to match %r{href=\"http://google.com/\?x=@#{account.username}}
- expect(result).not_to match "href=\"https://example.com/@#{account.username}"
+ it 'has display URL' do
+ is_expected.to include 'nic.みんな/'
end
+ end
+
+ context 'matches a URL without trailing period' do
+ let(:text) { 'http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona. ' }
- it 'contains a hashtag' do
- result = Formatter.instance.format(local_status_with_mention)
- expect(result).to match('/tags/hashtag" class="mention hashtag" rel="tag">#hashtag')
+ it 'has valid URL' do
+ is_expected.to include 'href="http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona"'
end
end
- context 'with cashtag' do
- let(:local_text) { 'Hello world $AAPL' }
+ context 'matches a URL without closing paranthesis' do
+ let(:text) { '(http://google.com/)' }
- it 'skip cashtag' do
- expect(subject).to match '
Hello world $AAPL
'
+ it 'has valid URL' do
+ is_expected.to include 'href="http://google.com/"'
end
end
- context 'with reblog' do
- let(:local_status) { Fabricate(:status, account: account, reblog: Fabricate(:status, text: 'Hello world', account: account)) }
+ context 'matches a URL without exclamation point' do
+ let(:text) { 'http://www.google.com!' }
- it 'contains credit to original author' do
- expect(subject).to include("RT @#{account.username} Hello world")
+ it 'has valid URL' do
+ is_expected.to include 'href="http://www.google.com/"'
end
end
- context 'matches a stand-alone medium URL' do
- let(:local_text) { 'https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4' }
+ context 'matches a URL without single quote' do
+ let(:text) { "http://www.google.com'" }
- it 'has valid url' do
- expect(subject).to include('href="https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4"')
+ it 'has valid URL' do
+ is_expected.to include 'href="http://www.google.com/"'
end
end
- context 'matches a stand-alone google URL' do
- let(:local_text) { 'http://google.com' }
+ context 'matches a URL without angle brackets' do
+ let(:text) { 'http://www.google.com>' }
- it 'has valid url' do
- expect(subject).to include('href="http://google.com/"')
+ it 'has valid URL' do
+ is_expected.to include 'href="http://www.google.com/"'
end
end
- context 'matches a stand-alone IDN URL' do
- let(:local_text) { 'https://nic.みんな/' }
+ context 'matches a URL with a query string' do
+ let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' }
- it 'has valid url' do
- expect(subject).to include('href="https://nic.xn--q9jyb4c/"')
+ it 'has valid URL' do
+ is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink"'
end
+ end
- it 'has display url' do
- expect(subject).to include('nic.みんな/')
+ context 'matches a URL with parenthesis in it' do
+ let(:text) { 'https://en.wikipedia.org/wiki/Diaspora_(software)' }
+
+ it 'has valid URL' do
+ is_expected.to include 'href="https://en.wikipedia.org/wiki/Diaspora_(software)"'
end
end
- context 'matches a URL without trailing period' do
- let(:local_text) { 'http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona. ' }
+ context 'contains HTML (script tag)' do
+ let(:text) { '' }
- it 'has valid url' do
- expect(subject).to include('href="http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona"')
+ it 'has escaped HTML' do
+ is_expected.to include '
<script>alert("Hello")</script>
'
end
end
- xit 'matches a URL without closing paranthesis' do
- expect(subject.match('(http://google.com/)')[0]).to eq 'http://google.com'
+ context 'contains HTML (XSS attack)' do
+ let(:text) { %q{} }
+
+ it 'has escaped HTML' do
+ is_expected.to include '
<img src="javascript:alert('XSS');">
'
+ end
end
- context 'matches a URL without exclamation point' do
- let(:local_text) { 'http://www.google.com!' }
+ context 'contains invalid URL' do
+ let(:text) { 'http://www\.google\.com' }
- it 'has valid url' do
- expect(subject).to include('href="http://www.google.com/"')
+ it 'has raw URL' do
+ is_expected.to eq '
http://www\.google\.com
'
end
end
- context 'matches a URL without single quote' do
- let(:local_text) { "http://www.google.com'" }
+ context 'contains a hashtag' do
+ let(:text) { '#hashtag' }
- it 'has valid url' do
- expect(subject).to include('href="http://www.google.com/"')
+ it 'has a link' do
+ is_expected.to include '/tags/hashtag" class="mention hashtag" rel="tag">#hashtag'
end
end
+ end
- context 'matches a URL without angle brackets' do
- let(:local_text) { 'http://www.google.com>' }
+ describe '#format' do
+ subject { Formatter.instance.format(status) }
- it 'has valid url' do
- expect(subject).to include('href="http://www.google.com/"')
+ context 'with local status' do
+ context 'with reblog' do
+ let(:reblog) { Fabricate(:status, account: local_account, text: 'Hello world', uri: nil) }
+ let(:status) { Fabricate(:status, reblog: reblog) }
+
+ it 'returns original status with credit to its author' do
+ is_expected.to include 'RT @alice Hello world'
+ end
+ end
+
+ context 'contains plain text' do
+ let(:status) { Fabricate(:status, text: 'text', uri: nil) }
+
+ it 'paragraphizes' do
+ is_expected.to eq '
text
'
+ end
+ end
+
+ context 'contains line feeds' do
+ let(:status) { Fabricate(:status, text: "line\nfeed", uri: nil) }
+
+ it 'removes line feeds' do
+ is_expected.not_to include "\n"
+ end
+ end
+
+ context 'contains linkable mentions' do
+ let(:status) { Fabricate(:status, mentions: [ Fabricate(:mention, account: local_account) ], text: '@alice') }
+
+ it 'links' do
+ is_expected.to include '@alice'
+ end
+ end
+
+ context 'contains unlinkable mentions' do
+ let(:status) { Fabricate(:status, text: '@alice', uri: nil) }
+
+ it 'does not link' do
+ is_expected.to include '@alice'
+ end
+ end
+
+ context do
+ subject do
+ status = Fabricate(:status, text: text, uri: nil)
+ Formatter.instance.format(status)
+ end
+
+ include_examples 'encode and link URLs'
end
end
- context 'matches a URL with a query string' do
- let(:local_text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' }
+ context 'with remote status' do
+ let(:status) { Fabricate(:status, text: 'Beep boop', uri: 'beepboop') }
- it 'has valid url' do
- expect(subject).to include('href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink"')
+ it 'reformats' do
+ is_expected.to eq 'Beep boop'
end
end
+ end
- context 'matches a URL with parenthesis in it' do
- let(:local_text) { 'https://en.wikipedia.org/wiki/Diaspora_(software)' }
+ describe '#reformat' do
+ subject { Formatter.instance.reformat(text) }
+
+ context 'contains plain text' do
+ let(:text) { 'Beep boop' }
- it 'has valid url' do
- expect(subject).to include('href="https://en.wikipedia.org/wiki/Diaspora_(software)"')
+ it 'contains plain text' do
+ is_expected.to include 'Beep boop'
end
end
- context 'contains html (script tag)' do
- let(:local_text) { '' }
+ context 'contains scripts' do
+ let(:text) { '' }
- it 'has valid url' do
- expect(subject).to match '
<script>alert("Hello")</script>
'
+ it 'strips scripts' do
+ is_expected.to_not include ''
end
end
- context 'contains html (xss attack)' do
- let(:local_text) { %q{} }
+ context 'contains malicious classes' do
+ let(:text) { 'Show more' }
- it 'has valid url' do
- expect(subject).to match '
<img src="javascript:alert('XSS');">
'
+ it 'strips malicious classes' do
+ is_expected.to_not include 'status__content__spoiler-link'
end
end
+ end
- context 'contains invalid URL' do
- let(:local_text) { 'http://www\.google\.com' }
+ describe '#plaintext' do
+ subject { Formatter.instance.plaintext(status) }
+
+ context 'with local status' do
+ let(:status) { Fabricate(:status, text: '
a text by a nerd who uses an HTML tag in text
', uri: nil) }
- it 'has valid url' do
- expect(subject).to eq '
http://www\.google\.com
'
+ it 'returns raw text' do
+ is_expected.to eq '
a text by a nerd who uses an HTML tag in text
'
end
end
- context 'concatenates hashtag and URL' do
- let(:local_text) { '#hashtaghttps://www.google.com' }
+ context 'with remote status' do
+ let(:status) { Fabricate(:status, text: '', uri: 'beep boop') }
- it 'has valid hashtag' do
- expect(subject).to match('/tags/hashtag" class="mention hashtag" rel="tag">#hashtag')
+ it 'returns tag-stripped text' do
+ is_expected.to eq ''
end
end
end
- describe '#reformat' do
- subject { Formatter.instance.format(remote_status) }
+ describe '#simplified_format' do
+ subject { Formatter.instance.simplified_format(account) }
+
+ context 'with local status' do
+ let(:account) { Fabricate(:account, domain: nil, note: text) }
+
+ context 'contains linkable mentions for local accounts' do
+ let(:text) { '@alice' }
+
+ before { local_account }
+
+ it 'links' do
+ is_expected.to eq '
'
+ end
+ end
+
+ context 'contains linkable mentions for remote accounts' do
+ let(:text) { '@bob@remote' }
+
+ before { remote_account }
+
+ it 'links' do
+ is_expected.to eq '
'
+ end
+ end
+
+ context 'contains unlinkable mentions' do
+ let(:text) { '@alice' }
- it 'returns a string' do
- expect(subject).to be_a String
+ it 'returns raw mention texts' do
+ is_expected.to eq '
@alice
'
+ end
+ end
+
+ include_examples 'encode and link URLs'
end
- it 'contains plain text' do
- expect(subject).to match('Beep boop')
+ context 'with remote status' do
+ let(:text) { '' }
+ let(:account) { Fabricate(:account, domain: 'remote', note: text) }
+
+ it 'reformats' do
+ is_expected.to_not include ''
+ end
end
+ end
+
+ describe '#sanitize' do
+ let(:html) { '' }
+
+ subject { Formatter.instance.sanitize(html, Sanitize::Config::MASTODON_STRICT) }
- it 'does not contain scripts' do
- expect(subject).to_not match('')
+ it 'sanitizes' do
+ is_expected.to eq 'alert("Hello")'
end
end
end
diff --git a/spec/lib/hash_object_spec.rb b/spec/lib/hash_object_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ce18065209f74c1b29b7235524cd31082456cde3
--- /dev/null
+++ b/spec/lib/hash_object_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe HashObject do
+ it 'has methods corresponding to hash properties' do
+ expect(HashObject.new(key: 'value').key).to eq 'value'
+ end
+end
diff --git a/spec/lib/inline_rabl_scope_spec.rb b/spec/lib/inline_rabl_scope_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3fff176e46cc7661d7c3b226db26b32f298e3599
--- /dev/null
+++ b/spec/lib/inline_rabl_scope_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe InlineRablScope do
+ describe '#current_account' do
+ it 'returns the given account' do
+ account = Fabricate(:account)
+ expect(InlineRablScope.new(account).current_account).to eq account
+ end
+ end
+
+ describe '#current_user' do
+ it 'returns nil if the given account is nil' do
+ expect(InlineRablScope.new(nil).current_user).to eq nil
+ end
+
+ it 'returns user of account if the given account is not nil' do
+ user = Fabricate(:user)
+ expect(InlineRablScope.new(user.account).current_user).to eq user
+ end
+ end
+end
diff --git a/spec/lib/language_detector_spec.rb b/spec/lib/language_detector_spec.rb
index e543edd49694c6bcf26f6ea8672d5080989c6cf1..ec39cb6a08c839c0cd92a94c7f96be022af11b59 100644
--- a/spec/lib/language_detector_spec.rb
+++ b/spec/lib/language_detector_spec.rb
@@ -1,11 +1,49 @@
# frozen_string_literal: true
+
require 'rails_helper'
describe LanguageDetector do
+ describe 'prepared_text' do
+ it 'returns unmodified string without special cases' do
+ string = 'just a regular string'
+ result = described_class.new(string).prepared_text
+
+ expect(result).to eq string
+ end
+
+ it 'collapses spacing in strings' do
+ string = 'The formatting in this is very odd'
+
+ result = described_class.new(string).prepared_text
+ expect(result).to eq 'The formatting in this is very odd'
+ end
+
+ it 'strips usernames from strings before detection' do
+ string = '@username Yeah, very surreal...! also @friend'
+
+ result = described_class.new(string).prepared_text
+ expect(result).to eq 'Yeah, very surreal...! also'
+ end
+
+ it 'strips URLs from strings before detection' do
+ string = 'Our website is https://example.com and also http://localhost.dev'
+
+ result = described_class.new(string).prepared_text
+ expect(result).to eq 'Our website is and also'
+ end
+
+ it 'strips #hashtags from strings before detection' do
+ string = 'Hey look at all the #animals and #fish'
+
+ result = described_class.new(string).prepared_text
+ expect(result).to eq 'Hey look at all the and'
+ end
+ end
+
describe 'to_iso_s' do
it 'detects english language for basic strings' do
strings = [
- "Hello and welcome to mastodon",
+ "Hello and welcome to mastodon how are you today?",
"I'd rather not!",
"a lot of people just want to feel righteous all the time and that's all that matters",
]
@@ -24,20 +62,20 @@ describe LanguageDetector do
end
describe 'when language can\'t be detected' do
- it 'uses default locale when sent an empty document' do
+ it 'uses nil when sent an empty document' do
result = described_class.new('').to_iso_s
- expect(result).to eq :en
+ expect(result).to eq nil
end
describe 'because of a URL' do
- it 'uses default locale when sent just a URL' do
+ it 'uses nil when sent just a URL' do
string = 'http://example.com/media/2kFTgOJLXhQf0g2nKB4'
cld_result = CLD3::NNetLanguageIdentifier.new(0, 2048).find_language(string)
expect(cld_result).not_to eq :en
result = described_class.new(string).to_iso_s
- expect(result).to eq :en
+ expect(result).to eq nil
end
end
@@ -49,20 +87,20 @@ describe LanguageDetector do
expect(result).to eq :fr
end
- it 'uses default locale when account is present but has no locale' do
+ it 'uses nil when account is present but has no locale' do
account = double(user_locale: nil)
result = described_class.new('', account).to_iso_s
- expect(result).to eq :en
+ expect(result).to eq nil
end
end
describe 'with an `en` default locale' do
- it 'uses the default locale' do
+ it 'uses nil for undetectable string' do
string = ''
result = described_class.new(string).to_iso_s
- expect(result).to eq :en
+ expect(result).to eq nil
end
end
@@ -74,11 +112,11 @@ describe LanguageDetector do
I18n.default_locale = before
end
- it 'uses the default locale' do
+ it 'uses nil for undetectable string' do
string = ''
result = described_class.new(string).to_iso_s
- expect(result).to eq :ja
+ expect(result).to eq nil
end
end
end
diff --git a/spec/lib/provider_discovery_spec.rb b/spec/lib/provider_discovery_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..12e2616c9cd04ffe97036a28d976b26b5b4e9107
--- /dev/null
+++ b/spec/lib/provider_discovery_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ProviderDiscovery do
+ describe 'discover_provider' do
+ context 'when status code is 200 and MIME type is text/html' do
+ context 'Both of JSON and XML provider are discoverable' do
+ before do
+ stub_request(:get, 'https://host/oembed.html').to_return(
+ status: 200,
+ headers: { 'Content-Type': 'text/html' },
+ body: request_fixture('oembed_json_xml.html')
+ )
+ end
+
+ it 'returns new OEmbed::Provider for JSON provider if :format option is set to :json' do
+ provider = ProviderDiscovery.discover_provider('https://host/oembed.html', format: :json)
+ expect(provider.endpoint).to eq 'https://host/provider.json'
+ expect(provider.format).to eq :json
+ end
+
+ it 'returns new OEmbed::Provider for XML provider if :format option is set to :xml' do
+ provider = ProviderDiscovery.discover_provider('https://host/oembed.html', format: :xml)
+ expect(provider.endpoint).to eq 'https://host/provider.xml'
+ expect(provider.format).to eq :xml
+ end
+ end
+
+ context 'JSON provider is discoverable while XML provider is not' do
+ before do
+ stub_request(:get, 'https://host/oembed.html').to_return(
+ status: 200,
+ headers: { 'Content-Type': 'text/html' },
+ body: request_fixture('oembed_json.html')
+ )
+ end
+
+ it 'returns new OEmbed::Provider for JSON provider' do
+ provider = ProviderDiscovery.discover_provider('https://host/oembed.html')
+ expect(provider.endpoint).to eq 'https://host/provider.json'
+ expect(provider.format).to eq :json
+ end
+ end
+
+ context 'XML provider is discoverable while JSON provider is not' do
+ before do
+ stub_request(:get, 'https://host/oembed.html').to_return(
+ status: 200,
+ headers: { 'Content-Type': 'text/html' },
+ body: request_fixture('oembed_xml.html')
+ )
+ end
+
+ it 'returns new OEmbed::Provider for XML provider' do
+ provider = ProviderDiscovery.discover_provider('https://host/oembed.html')
+ expect(provider.endpoint).to eq 'https://host/provider.xml'
+ expect(provider.format).to eq :xml
+ end
+ end
+
+ context 'Invalid XML provider is discoverable while JSON provider is not' do
+ before do
+ stub_request(:get, 'https://host/oembed.html').to_return(
+ status: 200,
+ headers: { 'Content-Type': 'text/html' },
+ body: request_fixture('oembed_invalid_xml.html')
+ )
+ end
+
+ it 'raises OEmbed::NotFound' do
+ expect { ProviderDiscovery.discover_provider('https://host/oembed.html') }.to raise_error OEmbed::NotFound
+ end
+ end
+
+ context 'Neither of JSON and XML provider is discoverable' do
+ before do
+ stub_request(:get, 'https://host/oembed.html').to_return(
+ status: 200,
+ headers: { 'Content-Type': 'text/html' },
+ body: request_fixture('oembed_undiscoverable.html')
+ )
+ end
+
+ it 'raises OEmbed::NotFound' do
+ expect { ProviderDiscovery.discover_provider('https://host/oembed.html') }.to raise_error OEmbed::NotFound
+ end
+ end
+ end
+
+ context 'when status code is not 200' do
+ before do
+ stub_request(:get, 'https://host/oembed.html').to_return(
+ status: 400,
+ headers: { 'Content-Type': 'text/html' },
+ body: request_fixture('oembed_xml.html')
+ )
+ end
+
+ it 'raises OEmbed::NotFound' do
+ expect { ProviderDiscovery.discover_provider('https://host/oembed.html') }.to raise_error OEmbed::NotFound
+ end
+ end
+
+ context 'when MIME type is not text/html' do
+ before do
+ stub_request(:get, 'https://host/oembed.html').to_return(
+ status: 200,
+ body: request_fixture('oembed_xml.html')
+ )
+ end
+
+ it 'raises OEmbed::NotFound' do
+ expect { ProviderDiscovery.discover_provider('https://host/oembed.html') }.to raise_error OEmbed::NotFound
+ end
+ end
+ end
+end
diff --git a/spec/lib/status_filter_spec.rb b/spec/lib/status_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..db2d87de204d7c5f2ca276b78dffa621b75ad011
--- /dev/null
+++ b/spec/lib/status_filter_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe StatusFilter do
+ describe '#filtered?' do
+ let(:status) { Fabricate(:status) }
+
+ context 'without an account' do
+ subject { described_class.new(status, nil) }
+
+ context 'when there are no connections' do
+ it { is_expected.not_to be_filtered }
+ end
+
+ context 'when status account is silenced' do
+ before do
+ status.account.update(silenced: true)
+ end
+
+ it { is_expected.to be_filtered }
+ end
+
+ context 'when status policy does not allow show' do
+ before do
+ expect_any_instance_of(StatusPolicy).to receive(:show?).and_return(false)
+ end
+
+ it { is_expected.to be_filtered }
+ end
+ end
+
+ context 'with real account' do
+ let(:account) { Fabricate(:account) }
+ subject { described_class.new(status, account) }
+
+ context 'when there are no connections' do
+ it { is_expected.not_to be_filtered }
+ end
+
+ context 'when status account is blocked' do
+ before do
+ Fabricate(:block, account: account, target_account: status.account)
+ end
+
+ it { is_expected.to be_filtered }
+ end
+
+ context 'when status account domain is blocked' do
+ before do
+ status.account.update(domain: 'example.com')
+ Fabricate(:account_domain_block, account: account, domain: status.account_domain)
+ end
+
+ it { is_expected.to be_filtered }
+ end
+
+ context 'when status account is muted' do
+ before do
+ Fabricate(:mute, account: account, target_account: status.account)
+ end
+
+ it { is_expected.to be_filtered }
+ end
+
+ context 'when status account is silenced' do
+ before do
+ status.account.update(silenced: true)
+ end
+
+ it { is_expected.to be_filtered }
+ end
+
+ context 'when status policy does not allow show' do
+ before do
+ expect_any_instance_of(StatusPolicy).to receive(:show?).and_return(false)
+ end
+
+ it { is_expected.to be_filtered }
+ end
+ end
+ end
+end
diff --git a/spec/lib/stream_entry_finder_spec.rb b/spec/lib/stream_entry_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..64e03c36af79d589c278f77d8da6bc69cc884465
--- /dev/null
+++ b/spec/lib/stream_entry_finder_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe StreamEntryFinder do
+ include RoutingHelper
+
+ describe '#stream_entry' do
+ context 'with a status url' do
+ let(:status) { Fabricate(:status) }
+ let(:url) { short_account_status_url(account_username: status.account.username, id: status.id) }
+ subject { described_class.new(url) }
+
+ it 'finds the stream entry' do
+ expect(subject.stream_entry).to eq(status.stream_entry)
+ end
+
+ it 'raises an error if action is not :show' do
+ recognized = Rails.application.routes.recognize_path(url)
+ expect(recognized).to receive(:[]).with(:action).and_return(:create)
+ expect(Rails.application.routes).to receive(:recognize_path).with(url).and_return(recognized)
+
+ expect { subject.stream_entry }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'with a stream entry url' do
+ let(:stream_entry) { Fabricate(:stream_entry) }
+ let(:url) { account_stream_entry_url(stream_entry.account, stream_entry) }
+ subject { described_class.new(url) }
+
+ it 'finds the stream entry' do
+ expect(subject.stream_entry).to eq(stream_entry)
+ end
+ end
+
+ context 'with a plausible url' do
+ let(:url) { 'https://example.com/users/test/updates/123/embed' }
+ subject { described_class.new(url) }
+
+ it 'raises an error' do
+ expect { subject.stream_entry }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'with an unrecognized url' do
+ let(:url) { 'https://example.com/about' }
+ subject { described_class.new(url) }
+
+ it 'raises an error' do
+ expect { subject.stream_entry }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+end
diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb
index cbb427a8c1703aca949b930ac6c9190aa14f057d..1fae6bec458643406943fbe5ae41516980b1eda4 100644
--- a/spec/lib/tag_manager_spec.rb
+++ b/spec/lib/tag_manager_spec.rb
@@ -1,23 +1,152 @@
require 'rails_helper'
RSpec.describe TagManager do
- let(:local_domain) { Rails.configuration.x.local_domain }
+ describe '#local_domain?' do
+ # The following comparisons MUST be case-insensitive.
+
+ around do |example|
+ original_local_domain = Rails.configuration.x.local_domain
+ Rails.configuration.x.local_domain = 'domain'
+
+ example.run
+
+ Rails.configuration.x.local_domain = original_local_domain
+ end
+
+ it 'returns true for nil' do
+ expect(TagManager.instance.local_domain?(nil)).to eq true
+ end
+
+ it 'returns true if the slash-stripped string equals to local domain' do
+ expect(TagManager.instance.local_domain?('DoMaIn/')).to eq true
+ end
+
+ it 'returns false for irrelevant string' do
+ expect(TagManager.instance.local_domain?('DoMaIn!')).to eq false
+ end
+ end
+
+ describe '#web_domain?' do
+ # The following comparisons MUST be case-insensitive.
+
+ around do |example|
+ original_web_domain = Rails.configuration.x.web_domain
+ Rails.configuration.x.web_domain = 'domain'
+
+ example.run
+
+ Rails.configuration.x.web_domain = original_web_domain
+ end
+
+ it 'returns true for nil' do
+ expect(TagManager.instance.web_domain?(nil)).to eq true
+ end
+
+ it 'returns true if the slash-stripped string equals to web domain' do
+ expect(TagManager.instance.web_domain?('DoMaIn/')).to eq true
+ end
+
+ it 'returns false for string with irrelevant characters' do
+ expect(TagManager.instance.web_domain?('DoMaIn!')).to eq false
+ end
+ end
+
+ describe '#normalize_domain' do
+ it 'returns nil if the given parameter is nil' do
+ expect(TagManager.instance.normalize_domain(nil)).to eq nil
+ end
+
+ it 'returns normalized domain' do
+ expect(TagManager.instance.normalize_domain('DoMaIn/')).to eq 'domain'
+ end
+ end
+
+ describe '#local_url?' do
+ around do |example|
+ original_local_domain = Rails.configuration.x.local_domain
+ example.run
+ Rails.configuration.x.local_domain = original_local_domain
+ end
+
+ it 'returns true if the normalized string with port is local URL' do
+ Rails.configuration.x.local_domain = 'domain:42'
+ expect(TagManager.instance.local_url?('https://DoMaIn:42/')).to eq true
+ end
+
+ it 'returns true if the normalized string without port is local URL' do
+ Rails.configuration.x.local_domain = 'domain'
+ expect(TagManager.instance.local_url?('https://DoMaIn/')).to eq true
+ end
+
+ it 'returns false for string with irrelevant characters' do
+ Rails.configuration.x.local_domain = 'domain'
+ expect(TagManager.instance.local_url?('https://domainn/')).to eq false
+ end
+ end
+
+ describe '#same_acct?' do
+ # The following comparisons MUST be case-insensitive.
+
+ it 'returns true if the needle has a correct username and domain for remote user' do
+ expect(TagManager.instance.same_acct?('username@domain', 'UsErNaMe@DoMaIn')).to eq true
+ end
+
+ it 'returns false if the needle is missing a domain for remote user' do
+ expect(TagManager.instance.same_acct?('username@domain', 'UsErNaMe')).to eq false
+ end
+
+ it 'returns false if the needle has an incorrect domain for remote user' do
+ expect(TagManager.instance.same_acct?('username@domain', 'UsErNaMe@incorrect')).to eq false
+ end
+
+ it 'returns false if the needle has an incorrect username for remote user' do
+ expect(TagManager.instance.same_acct?('username@domain', 'incorrect@DoMaIn')).to eq false
+ end
+
+ it 'returns true if the needle has a correct username and domain for local user' do
+ expect(TagManager.instance.same_acct?('username', 'UsErNaMe@Cb6E6126.nGrOk.Io')).to eq true
+ end
+
+ it 'returns true if the needle is missing a domain for local user' do
+ expect(TagManager.instance.same_acct?('username', 'UsErNaMe')).to eq true
+ end
+
+ it 'returns false if the needle has an incorrect username for local user' do
+ expect(TagManager.instance.same_acct?('username', 'UsErNaM@Cb6E6126.nGrOk.Io')).to eq false
+ end
+
+ it 'returns false if the needle has an incorrect domain for local user' do
+ expect(TagManager.instance.same_acct?('username', 'incorrect@Cb6E6126.nGrOk.Io')).to eq false
+ end
+ end
describe '#unique_tag' do
- it 'returns a string' do
- expect(TagManager.instance.unique_tag(Time.now, 12, 'Status')).to be_a String
+ it 'returns a unique tag' do
+ expect(TagManager.instance.unique_tag(Time.utc(2000), 12, 'Status')).to eq 'tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status'
end
end
describe '#unique_tag_to_local_id' do
it 'returns the ID part' do
- expect(TagManager.instance.unique_tag_to_local_id("tag:#{local_domain};objectId=12:objectType=Status", 'Status')).to eql '12'
+ expect(TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status', 'Status')).to eql '12'
+ end
+
+ it 'returns nil if it is not local id' do
+ expect(TagManager.instance.unique_tag_to_local_id('tag:remote,2000-01-01:objectId=12:objectType=Status', 'Status')).to eq nil
+ end
+
+ it 'returns nil if it is not expected type' do
+ expect(TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Block', 'Status')).to eq nil
+ end
+
+ it 'returns nil if it does not have object ID' do
+ expect(TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectType=Status', 'Status')).to eq nil
end
end
describe '#local_id?' do
it 'returns true for a local ID' do
- expect(TagManager.instance.local_id?("tag:#{local_domain};objectId=12:objectType=Status")).to be true
+ expect(TagManager.instance.local_id?('tag:cb6e6126.ngrok.io;objectId=12:objectType=Status')).to be true
end
it 'returns false for a foreign ID' do
@@ -26,49 +155,85 @@ RSpec.describe TagManager do
end
describe '#uri_for' do
- let(:alice) { Fabricate(:account, username: 'alice') }
- let(:bob) { Fabricate(:account, username: 'bob') }
- let(:status) { Fabricate(:status, text: 'Hello world', account: alice) }
-
subject { TagManager.instance.uri_for(target) }
- context 'Account' do
- let(:target) { alice }
+ context 'activity object' do
+ let(:target) { Fabricate(:status, reblog: Fabricate(:status)).stream_entry }
+
+ before { target.update!(created_at: '2000-01-01T00:00:00Z') }
+
+ it 'returns the unique tag for status' do
+ expect(target.object_type).to eq :activity
+ is_expected.to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{target.id}:objectType=Status"
+ end
+ end
+
+ context 'comment object' do
+ let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: true) }
+
+ it 'returns the unique tag for status' do
+ expect(target.object_type).to eq :comment
+ is_expected.to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{target.id}:objectType=Status"
+ end
+ end
+
+ context 'note object' do
+ let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: false, thread: nil) }
- it 'returns a string' do
- expect(subject).to be_a String
+ it 'returns the unique tag for status' do
+ expect(target.object_type).to eq :note
+ is_expected.to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{target.id}:objectType=Status"
end
end
- context 'Status' do
- let(:target) { status }
+ context 'person object' do
+ let(:target) { Fabricate(:account, username: 'alice') }
- it 'returns a string' do
- expect(subject).to be_a String
+ it 'returns the URL for account' do
+ expect(target.object_type).to eq :person
+ is_expected.to eq 'https://cb6e6126.ngrok.io/users/alice'
end
end
end
describe '#url_for' do
- let(:alice) { Fabricate(:account, username: 'alice') }
- let(:bob) { Fabricate(:account, username: 'bob') }
- let(:status) { Fabricate(:status, text: 'Hello world', account: alice) }
+ let(:alice) { Fabricate(:account, username: 'alice') }
subject { TagManager.instance.url_for(target) }
- context 'Account' do
- let(:target) { alice }
+ context 'activity object' do
+ let(:target) { Fabricate(:status, account: alice, reblog: Fabricate(:status)).stream_entry }
+
+ it 'returns the unique tag for status' do
+ expect(target.object_type).to eq :activity
+ is_expected.to eq "https://cb6e6126.ngrok.io/@alice/#{target.id}"
+ end
+ end
+
+ context 'comment object' do
+ let(:target) { Fabricate(:status, account: alice, reply: true) }
- it 'returns a URL' do
- expect(subject).to be_a String
+ it 'returns the unique tag for status' do
+ expect(target.object_type).to eq :comment
+ is_expected.to eq "https://cb6e6126.ngrok.io/@alice/#{target.id}"
end
end
- context 'Status' do
- let(:target) { status }
+ context 'note object' do
+ let(:target) { Fabricate(:status, account: alice, reply: false, thread: nil) }
+
+ it 'returns the unique tag for status' do
+ expect(target.object_type).to eq :note
+ is_expected.to eq "https://cb6e6126.ngrok.io/@alice/#{target.id}"
+ end
+ end
+
+ context 'person object' do
+ let(:target) { alice }
- it 'returns a URL' do
- expect(subject).to be_a String
+ it 'returns the URL for account' do
+ expect(target.object_type).to eq :person
+ is_expected.to eq 'https://cb6e6126.ngrok.io/@alice'
end
end
end
diff --git a/spec/lib/user_settings_decorator_spec.rb b/spec/lib/user_settings_decorator_spec.rb
index 466c57fa51c50e10edd9e2f625e2effe3abc1115..66e42fa0ecc5034e5667b14532afb72445be35f4 100644
--- a/spec/lib/user_settings_decorator_spec.rb
+++ b/spec/lib/user_settings_decorator_spec.rb
@@ -35,6 +35,13 @@ describe UserSettingsDecorator do
expect(user.settings['boost_modal']).to eq true
end
+ it 'updates the user settings value for delete toot modal' do
+ values = { 'setting_delete_modal' => '0' }
+
+ settings.update(values)
+ expect(user.settings['delete_modal']).to eq false
+ end
+
it 'updates the user settings value for gif auto play' do
values = { 'setting_auto_play_gif' => '0' }
diff --git a/spec/lib/webfinger_resource_spec.rb b/spec/lib/webfinger_resource_spec.rb
index dfd23062f57fa485c59fafb14c88ab5f0440f86b..287537a26102f02cd051302685f01224d08138b8 100644
--- a/spec/lib/webfinger_resource_spec.rb
+++ b/spec/lib/webfinger_resource_spec.rb
@@ -2,14 +2,16 @@ require 'rails_helper'
describe WebfingerResource do
around do |example|
- before = Rails.configuration.x.local_domain
+ before_local = Rails.configuration.x.local_domain
+ before_web = Rails.configuration.x.web_domain
example.run
- Rails.configuration.x.local_domain = before
+ Rails.configuration.x.local_domain = before_local
+ Rails.configuration.x.web_domain = before_web
end
describe '#username' do
describe 'with a URL value' do
- it 'raises with an unrecognized route' do
+ it 'raises with a route whose controller is not AccountsController' do
resource = 'https://example.com/users/alice/other'
expect {
@@ -17,6 +19,21 @@ describe WebfingerResource do
}.to raise_error(ActiveRecord::RecordNotFound)
end
+ it 'raises with a route whose action is not show' do
+ resource = 'https://example.com/users/alice'
+
+ recognized = Rails.application.routes.recognize_path(resource)
+ allow(recognized).to receive(:[]).with(:controller).and_return('accounts')
+ allow(recognized).to receive(:[]).with(:username).and_return('alice')
+ expect(recognized).to receive(:[]).with(:action).and_return('create')
+
+ expect(Rails.application.routes).to receive(:recognize_path).with(resource).and_return(recognized).at_least(:once)
+
+ expect {
+ WebfingerResource.new(resource).username
+ }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
it 'raises with a string that doesnt start with URL' do
resource = 'website for http://example.com/users/alice/other'
@@ -63,6 +80,14 @@ describe WebfingerResource do
result = WebfingerResource.new(resource).username
expect(result).to eq 'alice'
end
+
+ it 'finds username for a web domain' do
+ Rails.configuration.x.web_domain = 'example.com'
+ resource = 'alice@example.com'
+
+ result = WebfingerResource.new(resource).username
+ expect(result).to eq 'alice'
+ end
end
describe 'with an acct value' do
@@ -82,13 +107,21 @@ describe WebfingerResource do
}.to raise_error(ActiveRecord::RecordNotFound)
end
- it 'finds the username for a local account' do
+ it 'finds the username for a local account if the domain is the local one' do
Rails.configuration.x.local_domain = 'example.com'
resource = 'acct:alice@example.com'
result = WebfingerResource.new(resource).username
expect(result).to eq 'alice'
end
+
+ it 'finds the username for a local account if the domain is the Web one' do
+ Rails.configuration.x.web_domain = 'example.com'
+ resource = 'acct:alice@example.com'
+
+ result = WebfingerResource.new(resource).username
+ expect(result).to eq 'alice'
+ end
end
end
end
diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb
index faa3197c83be6e5ee807dc7e2814c62371b29afc..8114356060235d48b3fbbd9981fd553bf674379d 100644
--- a/spec/mailers/notification_mailer_spec.rb
+++ b/spec/mailers/notification_mailer_spec.rb
@@ -3,13 +3,28 @@ require "rails_helper"
RSpec.describe NotificationMailer, type: :mailer do
let(:receiver) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:sender) { Fabricate(:account, username: 'bob') }
- let(:foreign_status) { Fabricate(:status, account: sender) }
- let(:own_status) { Fabricate(:status, account: receiver.account) }
+ let(:foreign_status) { Fabricate(:status, account: sender, text: 'The body of the foreign status') }
+ let(:own_status) { Fabricate(:status, account: receiver.account, text: 'The body of the own status') }
+
+ shared_examples 'localized subject' do |*args, **kwrest|
+ it 'renders subject localized for the locale of the receiver' do
+ locale = I18n.available_locales.sample
+ receiver.update!(locale: locale)
+ expect(mail.subject).to eq I18n.t(*args, kwrest.merge(locale: locale))
+ end
+
+ it 'renders subject localized for the default locale if the locale of the receiver is unavailable' do
+ receiver.update!(locale: nil)
+ expect(mail.subject).to eq I18n.t(*args, kwrest.merge(locale: I18n.default_locale))
+ end
+ end
describe "mention" do
let(:mention) { Mention.create!(account: receiver.account, status: foreign_status) }
let(:mail) { NotificationMailer.mention(receiver.account, Notification.create!(account: receiver.account, activity: mention)) }
+ include_examples 'localized subject', 'notification_mailer.mention.subject', name: 'bob'
+
it "renders the headers" do
expect(mail.subject).to eq("You were mentioned by bob")
expect(mail.to).to eq([receiver.email])
@@ -17,6 +32,7 @@ RSpec.describe NotificationMailer, type: :mailer do
it "renders the body" do
expect(mail.body.encoded).to match("You were mentioned by bob")
+ expect(mail.body.encoded).to include 'The body of the foreign status'
end
end
@@ -24,6 +40,8 @@ RSpec.describe NotificationMailer, type: :mailer do
let(:follow) { sender.follow!(receiver.account) }
let(:mail) { NotificationMailer.follow(receiver.account, Notification.create!(account: receiver.account, activity: follow)) }
+ include_examples 'localized subject', 'notification_mailer.follow.subject', name: 'bob'
+
it "renders the headers" do
expect(mail.subject).to eq("bob is now following you")
expect(mail.to).to eq([receiver.email])
@@ -38,6 +56,8 @@ RSpec.describe NotificationMailer, type: :mailer do
let(:favourite) { Favourite.create!(account: sender, status: own_status) }
let(:mail) { NotificationMailer.favourite(own_status.account, Notification.create!(account: receiver.account, activity: favourite)) }
+ include_examples 'localized subject', 'notification_mailer.favourite.subject', name: 'bob'
+
it "renders the headers" do
expect(mail.subject).to eq("bob favourited your status")
expect(mail.to).to eq([receiver.email])
@@ -45,6 +65,7 @@ RSpec.describe NotificationMailer, type: :mailer do
it "renders the body" do
expect(mail.body.encoded).to match("Your status was favourited by bob")
+ expect(mail.body.encoded).to include 'The body of the own status'
end
end
@@ -52,6 +73,8 @@ RSpec.describe NotificationMailer, type: :mailer do
let(:reblog) { Status.create!(account: sender, reblog: own_status) }
let(:mail) { NotificationMailer.reblog(own_status.account, Notification.create!(account: receiver.account, activity: reblog)) }
+ include_examples 'localized subject', 'notification_mailer.reblog.subject', name: 'bob'
+
it "renders the headers" do
expect(mail.subject).to eq("bob boosted your status")
expect(mail.to).to eq([receiver.email])
@@ -59,6 +82,7 @@ RSpec.describe NotificationMailer, type: :mailer do
it "renders the body" do
expect(mail.body.encoded).to match("Your status was boosted by bob")
+ expect(mail.body.encoded).to include 'The body of the own status'
end
end
@@ -66,6 +90,8 @@ RSpec.describe NotificationMailer, type: :mailer do
let(:follow_request) { Fabricate(:follow_request, account: sender, target_account: receiver.account) }
let(:mail) { NotificationMailer.follow_request(receiver.account, Notification.create!(account: receiver.account, activity: follow_request)) }
+ include_examples 'localized subject', 'notification_mailer.follow_request.subject', name: 'bob'
+
it 'renders the headers' do
expect(mail.subject).to eq('Pending follower: bob')
expect(mail.to).to eq([receiver.email])
@@ -78,18 +104,44 @@ RSpec.describe NotificationMailer, type: :mailer do
describe 'digest' do
before do
- mention = Fabricate(:mention, account: receiver.account)
+ mention = Fabricate(:mention, account: receiver.account, status: foreign_status)
Fabricate(:notification, account: receiver.account, activity: mention)
+ sender.follow!(receiver.account)
end
- let(:mail) { NotificationMailer.digest(receiver.account, since: 5.days.ago) }
- it 'renders the headers' do
- expect(mail.subject).to match('notification since your last')
- expect(mail.to).to eq([receiver.email])
+ context do
+ let!(:mail) { NotificationMailer.digest(receiver.account, since: 5.days.ago) }
+
+ include_examples 'localized subject', 'notification_mailer.digest.subject', count: 1, name: 'bob'
+
+ it 'renders the headers' do
+ expect(mail.subject).to match('notification since your last')
+ expect(mail.to).to eq([receiver.email])
+ end
+
+ it 'renders the body' do
+ expect(mail.body.encoded).to match('brief summary')
+ expect(mail.body.encoded).to include 'The body of the foreign status'
+ expect(mail.body.encoded).to include sender.username
+ end
end
- it 'renders the body' do
- expect(mail.body.encoded).to match('brief summary')
+ it 'includes activities since the date specified by :since option' do
+ receiver.update!(last_emailed_at: '2000-02-01T00:00:00Z', current_sign_in_at: '2000-03-01T00:00:00Z')
+ mail = NotificationMailer.digest(receiver.account, since: Time.parse('2000-01-01T00:00:00Z'))
+ expect(mail.body.encoded).to include 'Jan 01, 2000, 00:00'
+ end
+
+ it 'includes activities since the receiver was last emailed if :since option is unavailable' do
+ receiver.update!(last_emailed_at: '2000-02-01T00:00:00Z', current_sign_in_at: '2000-03-01T00:00:00Z')
+ mail = NotificationMailer.digest(receiver.account)
+ expect(mail.body.encoded).to include 'Feb 01, 2000, 00:00'
+ end
+
+ it 'includes activities since the receiver last signed in if :since option and the last emailed date are unavailable' do
+ receiver.update!(last_emailed_at: nil, current_sign_in_at: '2000-03-01T00:00:00Z')
+ mail = NotificationMailer.digest(receiver.account)
+ expect(mail.body.encoded).to include 'Mar 01, 2000, 00:00'
end
end
end
diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1f6d4401543a931f72da7679573ac63d6ca33a5e
--- /dev/null
+++ b/spec/mailers/user_mailer_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe UserMailer, type: :mailer do
+ let(:receiver) { Fabricate(:user) }
+
+ shared_examples 'localized subject' do |*args, **kwrest|
+ it 'renders subject localized for the locale of the receiver' do
+ locale = I18n.available_locales.sample
+ receiver.update!(locale: locale)
+ expect(mail.subject).to eq I18n.t(*args, kwrest.merge(locale: locale))
+ end
+
+ it 'renders subject localized for the default locale if the locale of the receiver is unavailable' do
+ receiver.update!(locale: nil)
+ expect(mail.subject).to eq I18n.t(*args, kwrest.merge(locale: I18n.default_locale))
+ end
+ end
+
+ describe 'confirmation_instructions' do
+ let(:mail) { UserMailer.confirmation_instructions(receiver, 'spec') }
+
+ it 'renders confirmation instructions' do
+ receiver.update!(locale: nil)
+ expect(mail.body.encoded).to include receiver.email
+ expect(mail.body.encoded).to include 'spec'
+ expect(mail.body.encoded).to include Rails.configuration.x.local_domain
+ end
+
+ include_examples 'localized subject',
+ 'devise.mailer.confirmation_instructions.subject',
+ instance: Rails.configuration.x.local_domain
+ end
+
+ describe 'reset_password_instructions' do
+ let(:mail) { UserMailer.reset_password_instructions(receiver, 'spec') }
+
+ it 'renders reset password instructions' do
+ receiver.update!(locale: nil)
+ expect(mail.body.encoded).to include receiver.email
+ expect(mail.body.encoded).to include 'spec'
+ end
+
+ include_examples 'localized subject',
+ 'devise.mailer.reset_password_instructions.subject'
+ end
+
+ describe 'password_change' do
+ let(:mail) { UserMailer.password_change(receiver) }
+
+ it 'renders password change notification' do
+ receiver.update!(locale: nil)
+ expect(mail.body.encoded).to include receiver.email
+ end
+
+ include_examples 'localized subject',
+ 'devise.mailer.password_change.subject'
+ end
+end
diff --git a/spec/models/account_domain_block_spec.rb b/spec/models/account_domain_block_spec.rb
index bd64e10fb328253a12771b4bfeae6278351f7f82..469bc05cb13ffc98e51b1e9268741ee6a693dab5 100644
--- a/spec/models/account_domain_block_spec.rb
+++ b/spec/models/account_domain_block_spec.rb
@@ -1,5 +1,22 @@
require 'rails_helper'
RSpec.describe AccountDomainBlock, type: :model do
+ it 'removes blocking cache after creation' do
+ account = Fabricate(:account)
+ Rails.cache.write("exclude_domains_for:#{account.id}", 'a.domain.already.blocked')
+ AccountDomainBlock.create!(account: account, domain: 'a.domain.blocked.later')
+
+ expect(Rails.cache.exist?("exclude_domains_for:#{account.id}")).to eq false
+ end
+
+ it 'removes blocking cache after destruction' do
+ account = Fabricate(:account)
+ block = AccountDomainBlock.create!(account: account, domain: 'domain')
+ Rails.cache.write("exclude_domains_for:#{account.id}", 'domain')
+
+ block.destroy!
+
+ expect(Rails.cache.exist?("exclude_domains_for:#{account.id}")).to eq false
+ end
end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index 08098854b11f0fc26566ce2a178bf0ce64e978c9..73ae34f8e5f14d282ae2979bf0375e9f29530a43 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -301,54 +301,6 @@ RSpec.describe Account, type: :model do
end
end
- describe '.find_local' do
- before do
- Fabricate(:account, username: 'Alice')
- end
-
- it 'returns Alice for alice' do
- expect(Account.find_local('alice')).to_not be_nil
- end
-
- it 'returns Alice for Alice' do
- expect(Account.find_local('Alice')).to_not be_nil
- end
-
- it 'does not return anything for a_ice' do
- expect(Account.find_local('a_ice')).to be_nil
- end
-
- it 'does not return anything for al%' do
- expect(Account.find_local('al%')).to be_nil
- end
- end
-
- describe '.find_remote' do
- before do
- Fabricate(:account, username: 'Alice', domain: 'mastodon.social')
- end
-
- it 'returns Alice for alice@mastodon.social' do
- expect(Account.find_remote('alice', 'mastodon.social')).to_not be_nil
- end
-
- it 'returns Alice for ALICE@MASTODON.SOCIAL' do
- expect(Account.find_remote('ALICE', 'MASTODON.SOCIAL')).to_not be_nil
- end
-
- it 'does not return anything for a_ice@mastodon.social' do
- expect(Account.find_remote('a_ice', 'mastodon.social')).to be_nil
- end
-
- it 'does not return anything for alice@m_stodon.social' do
- expect(Account.find_remote('alice', 'm_stodon.social')).to be_nil
- end
-
- it 'does not return anything for alice@m%' do
- expect(Account.find_remote('alice', 'm%')).to be_nil
- end
- end
-
describe '.following_map' do
it 'returns an hash' do
expect(Account.following_map([], 1)).to be_a Hash
@@ -429,6 +381,18 @@ RSpec.describe Account, type: :model do
expect(account_2).to model_have_error_on_field(:username)
end
+ it 'is invalid if the username is reserved' do
+ account = Fabricate.build(:account, username: 'support')
+ account.valid?
+ expect(account).to model_have_error_on_field(:username)
+ end
+
+ it 'is valid when username is reserved but record has already been created' do
+ account = Fabricate.build(:account, username: 'support')
+ account.save(validate: false)
+ expect(account.valid?).to be true
+ end
+
context 'when is local' do
it 'is invalid if the username doesn\'t only contains letters, numbers and underscores' do
account = Fabricate.build(:account, username: 'the-doctor')
diff --git a/spec/models/block_spec.rb b/spec/models/block_spec.rb
index cabb41c3ea29f154f8f8c4daf32b1cf76bfbb26d..acbdc77f5bc7c0e2f4562412f86d895d06c507d5 100644
--- a/spec/models/block_spec.rb
+++ b/spec/models/block_spec.rb
@@ -19,4 +19,29 @@ RSpec.describe Block, type: :model do
expect(block).to model_have_error_on_field(:target_account)
end
end
+
+ it 'removes blocking cache after creation' do
+ account = Fabricate(:account)
+ target_account = Fabricate(:account)
+ Rails.cache.write("exclude_account_ids_for:#{account.id}", [])
+ Rails.cache.write("exclude_account_ids_for:#{target_account.id}", [])
+
+ Block.create!(account: account, target_account: target_account)
+
+ expect(Rails.cache.exist?("exclude_account_ids_for:#{account.id}")).to eq false
+ expect(Rails.cache.exist?("exclude_account_ids_for:#{target_account.id}")).to eq false
+ end
+
+ it 'removes blocking cache after destruction' do
+ account = Fabricate(:account)
+ target_account = Fabricate(:account)
+ block = Block.create!(account: account, target_account: target_account)
+ Rails.cache.write("exclude_account_ids_for:#{account.id}", [target_account.id])
+ Rails.cache.write("exclude_account_ids_for:#{target_account.id}", [account.id])
+
+ block.destroy!
+
+ expect(Rails.cache.exist?("exclude_account_ids_for:#{account.id}")).to eq false
+ expect(Rails.cache.exist?("exclude_account_ids_for:#{target_account.id}")).to eq false
+ end
end
diff --git a/spec/models/concerns/account_finder_concern_spec.rb b/spec/models/concerns/account_finder_concern_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..25f4fdec4bb7532afdb6c4fd86cbf3de034456d8
--- /dev/null
+++ b/spec/models/concerns/account_finder_concern_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe AccountFinderConcern do
+ describe 'local finders' do
+ before do
+ @account = Fabricate(:account, username: 'Alice')
+ end
+
+ describe '.find_local' do
+ it 'returns case-insensitive result' do
+ expect(Account.find_local('alice')).to eq(@account)
+ end
+
+ it 'returns correctly cased result' do
+ expect(Account.find_local('Alice')).to eq(@account)
+ end
+
+ it 'returns nil without a match' do
+ expect(Account.find_local('a_ice')).to be_nil
+ end
+
+ it 'returns nil for regex style username value' do
+ expect(Account.find_local('al%')).to be_nil
+ end
+
+ it 'returns nil for nil username value' do
+ expect(Account.find_local(nil)).to be_nil
+ end
+
+ it 'returns nil for blank username value' do
+ expect(Account.find_local('')).to be_nil
+ end
+ end
+
+ describe '.find_local!' do
+ it 'returns matching result' do
+ expect(Account.find_local!('alice')).to eq(@account)
+ end
+
+ it 'raises on non-matching result' do
+ expect { Account.find_local!('missing') }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'raises with blank username' do
+ expect { Account.find_local!('') }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'raises with nil username' do
+ expect { Account.find_local!(nil) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+
+ describe 'remote finders' do
+ before do
+ @account = Fabricate(:account, username: 'Alice', domain: 'mastodon.social')
+ end
+
+ describe '.find_remote' do
+ it 'returns exact match result' do
+ expect(Account.find_remote('alice', 'mastodon.social')).to eq(@account)
+ end
+
+ it 'returns case-insensitive result' do
+ expect(Account.find_remote('ALICE', 'MASTODON.SOCIAL')).to eq(@account)
+ end
+
+ it 'returns nil when username does not match' do
+ expect(Account.find_remote('a_ice', 'mastodon.social')).to be_nil
+ end
+
+ it 'returns nil when domain does not match' do
+ expect(Account.find_remote('alice', 'm_stodon.social')).to be_nil
+ end
+
+ it 'returns nil for regex style domain value' do
+ expect(Account.find_remote('alice', 'm%')).to be_nil
+ end
+
+ it 'returns nil for nil username value' do
+ expect(Account.find_remote(nil, 'domain')).to be_nil
+ end
+
+ it 'returns nil for blank username value' do
+ expect(Account.find_remote('', 'domain')).to be_nil
+ end
+ end
+
+ describe '.find_remote!' do
+ it 'returns matching result' do
+ expect(Account.find_remote!('alice', 'mastodon.social')).to eq(@account)
+ end
+
+ it 'raises on non-matching result' do
+ expect { Account.find_remote!('missing', 'mastodon.host') }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'raises with blank username' do
+ expect { Account.find_remote!('', '') }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'raises with nil username' do
+ expect { Account.find_remote!(nil, nil) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/status_threading_concern_spec.rb b/spec/models/concerns/status_threading_concern_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..62f5f6e317e295157f4163657bac69b29f7b3197
--- /dev/null
+++ b/spec/models/concerns/status_threading_concern_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe StatusThreadingConcern do
+ describe '#ancestors' do
+ let!(:alice) { Fabricate(:account, username: 'alice') }
+ let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') }
+ let!(:jeff) { Fabricate(:account, username: 'jeff') }
+ let!(:status) { Fabricate(:status, account: alice) }
+ let!(:reply1) { Fabricate(:status, thread: status, account: jeff) }
+ let!(:reply2) { Fabricate(:status, thread: reply1, account: bob) }
+ let!(:reply3) { Fabricate(:status, thread: reply2, account: alice) }
+ let!(:viewer) { Fabricate(:account, username: 'viewer') }
+
+ it 'returns conversation history' do
+ expect(reply3.ancestors).to include(status, reply1, reply2)
+ end
+
+ it 'does not return conversation history user is not allowed to see' do
+ reply1.update(visibility: :private)
+ status.update(visibility: :direct)
+
+ expect(reply3.ancestors(viewer)).to_not include(reply1, status)
+ end
+
+ it 'does not return conversation history from blocked users' do
+ viewer.block!(jeff)
+ expect(reply3.ancestors(viewer)).to_not include(reply1)
+ end
+
+ it 'does not return conversation history from muted users' do
+ viewer.mute!(jeff)
+ expect(reply3.ancestors(viewer)).to_not include(reply1)
+ end
+
+ it 'does not return conversation history from silenced and not followed users' do
+ jeff.update(silenced: true)
+ expect(reply3.ancestors(viewer)).to_not include(reply1)
+ end
+
+ it 'does not return conversation history from blocked domains' do
+ viewer.block_domain!('example.com')
+ expect(reply3.ancestors(viewer)).to_not include(reply2)
+ end
+
+ it 'ignores deleted records' do
+ first_status = Fabricate(:status, account: bob)
+ second_status = Fabricate(:status, thread: first_status, account: alice)
+
+ # Create cache and delete cached record
+ second_status.ancestors
+ first_status.destroy
+
+ expect(second_status.ancestors).to eq([])
+ end
+ end
+
+ describe '#descendants' do
+ let!(:alice) { Fabricate(:account, username: 'alice') }
+ let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') }
+ let!(:jeff) { Fabricate(:account, username: 'jeff') }
+ let!(:status) { Fabricate(:status, account: alice) }
+ let!(:reply1) { Fabricate(:status, thread: status, account: alice) }
+ let!(:reply2) { Fabricate(:status, thread: status, account: bob) }
+ let!(:reply3) { Fabricate(:status, thread: reply1, account: jeff) }
+ let!(:viewer) { Fabricate(:account, username: 'viewer') }
+
+ it 'returns replies' do
+ expect(status.descendants).to include(reply1, reply2, reply3)
+ end
+
+ it 'does not return replies user is not allowed to see' do
+ reply1.update(visibility: :private)
+ reply3.update(visibility: :direct)
+
+ expect(status.descendants(viewer)).to_not include(reply1, reply3)
+ end
+
+ it 'does not return replies from blocked users' do
+ viewer.block!(jeff)
+ expect(status.descendants(viewer)).to_not include(reply3)
+ end
+
+ it 'does not return replies from muted users' do
+ viewer.mute!(jeff)
+ expect(status.descendants(viewer)).to_not include(reply3)
+ end
+
+ it 'does not return replies from silenced and not followed users' do
+ jeff.update(silenced: true)
+ expect(status.descendants(viewer)).to_not include(reply3)
+ end
+
+ it 'does not return replies from blocked domains' do
+ viewer.block_domain!('example.com')
+ expect(status.descendants(viewer)).to_not include(reply2)
+ end
+ end
+end
diff --git a/spec/models/domain_block_spec.rb b/spec/models/domain_block_spec.rb
index b19c8083ec6caface90775974f652e324a3da9b5..89cadccfec2b33d00a8b9c982c444f5f17b2b8a3 100644
--- a/spec/models/domain_block_spec.rb
+++ b/spec/models/domain_block_spec.rb
@@ -13,11 +13,27 @@ RSpec.describe DomainBlock, type: :model do
expect(domain_block).to model_have_error_on_field(:domain)
end
- it 'is invalid if the domain already exists' do
- domain_block_1 = Fabricate(:domain_block, domain: 'dalek.com')
- domain_block_2 = Fabricate.build(:domain_block, domain: 'dalek.com')
+ it 'is invalid if the same normalized domain already exists' do
+ domain_block_1 = Fabricate(:domain_block, domain: 'にゃん')
+ domain_block_2 = Fabricate.build(:domain_block, domain: 'xn--r9j5b5b')
domain_block_2.valid?
expect(domain_block_2).to model_have_error_on_field(:domain)
end
end
+
+ describe 'blocked?' do
+ it 'returns true if the domain is suspended' do
+ Fabricate(:domain_block, domain: 'domain', severity: :suspend)
+ expect(DomainBlock.blocked?('domain')).to eq true
+ end
+
+ it 'returns false even if the domain is silenced' do
+ Fabricate(:domain_block, domain: 'domain', severity: :silence)
+ expect(DomainBlock.blocked?('domain')).to eq false
+ end
+
+ it 'returns false if the domain is not suspended nor silenced' do
+ expect(DomainBlock.blocked?('domain')).to eq false
+ end
+ end
end
diff --git a/spec/models/export_spec.rb b/spec/models/export_spec.rb
index 3ee042fb6321540ea2f9905228b535f6bbb95be4..6daa4614575bf570f46e7807d67dca47adc5bcd7 100644
--- a/spec/models/export_spec.rb
+++ b/spec/models/export_spec.rb
@@ -1,17 +1,16 @@
require 'rails_helper'
describe Export do
- describe 'to_csv' do
- before do
- one = Account.new(username: 'one', domain: 'local.host')
- two = Account.new(username: 'two', domain: 'local.host')
- accounts = [one, two]
-
- @account = double(blocking: accounts, muting: accounts, following: accounts)
- end
+ let(:account) { Fabricate(:account) }
+ let(:target_accounts) do
+ [ {}, { username: 'one', domain: 'local.host' } ].map(&method(:Fabricate).curry(2).call(:account))
+ end
+ describe 'to_csv' do
it 'returns a csv of the blocked accounts' do
- export = Export.new(@account).to_blocked_accounts_csv
+ target_accounts.each(&account.method(:block!))
+
+ export = Export.new(account).to_blocked_accounts_csv
results = export.strip.split
expect(results.size).to eq 2
@@ -19,7 +18,9 @@ describe Export do
end
it 'returns a csv of the muted accounts' do
- export = Export.new(@account).to_muted_accounts_csv
+ target_accounts.each(&account.method(:mute!))
+
+ export = Export.new(account).to_muted_accounts_csv
results = export.strip.split
expect(results.size).to eq 2
@@ -27,11 +28,37 @@ describe Export do
end
it 'returns a csv of the following accounts' do
- export = Export.new(@account).to_following_accounts_csv
+ target_accounts.each(&account.method(:follow!))
+
+ export = Export.new(account).to_following_accounts_csv
results = export.strip.split
expect(results.size).to eq 2
expect(results.first).to eq 'one@local.host'
end
end
+
+ describe 'total_storage' do
+ it 'returns the total size of the media attachments' do
+ media_attachment = Fabricate(:media_attachment, account: account)
+ expect(Export.new(account).total_storage).to eq media_attachment.file_file_size || 0
+ end
+ end
+
+ describe 'total_follows' do
+ it 'returns the total number of the followed accounts' do
+ target_accounts.each(&account.method(:follow!))
+ expect(Export.new(account).total_follows).to eq 2
+ end
+
+ it 'returns the total number of the blocked accounts' do
+ target_accounts.each(&account.method(:block!))
+ expect(Export.new(account).total_blocks).to eq 2
+ end
+
+ it 'returns the total number of the muted accounts' do
+ target_accounts.each(&account.method(:mute!))
+ expect(Export.new(account).total_mutes).to eq 2
+ end
+ end
end
diff --git a/spec/models/favourite_spec.rb b/spec/models/favourite_spec.rb
index 5b7126506f2074d2dac7ca40c9d144de24e6dd39..ba1410a453272e68182a9b9d0407f327cc99d988 100644
--- a/spec/models/favourite_spec.rb
+++ b/spec/models/favourite_spec.rb
@@ -1,9 +1,29 @@
require 'rails_helper'
RSpec.describe Favourite, type: :model do
- let(:alice) { Fabricate(:account, username: 'alice') }
- let(:bob) { Fabricate(:account, username: 'bob') }
- let(:status) { Fabricate(:status, account: bob) }
+ let(:account) { Fabricate(:account) }
- subject { Favourite.new(account: alice, status: status) }
+ context 'when status is a reblog' do
+ let(:reblog) { Fabricate(:status, reblog: nil) }
+ let(:status) { Fabricate(:status, reblog: reblog) }
+
+ it 'invalidates if the reblogged status is already a favourite' do
+ Favourite.create!(account: account, status: reblog)
+ expect(Favourite.new(account: account, status: status).valid?).to eq false
+ end
+
+ it 'replaces status with the reblogged one if it is a reblog' do
+ favourite = Favourite.create!(account: account, status: status)
+ expect(favourite.status).to eq reblog
+ end
+ end
+
+ context 'when status is not a reblog' do
+ let(:status) { Fabricate(:status, reblog: nil) }
+
+ it 'saves with the specified status' do
+ favourite = Favourite.create!(account: account, status: status)
+ expect(favourite.status).to eq status
+ end
+ end
end
diff --git a/spec/models/feed_spec.rb b/spec/models/feed_spec.rb
index 96c4770731494148ee019031a0b7ee42dd5879ef..15033e9ebbffc83a01a1744edf235fb08e753ea1 100644
--- a/spec/models/feed_spec.rb
+++ b/spec/models/feed_spec.rb
@@ -2,20 +2,20 @@ require 'rails_helper'
RSpec.describe Feed, type: :model do
describe '#get' do
- it "gets statuses with ids in the range, maintining the order from Redis" do
+ it 'gets statuses with ids in the range, maintining the order from Redis' do
account = Fabricate(:account)
Fabricate(:status, account: account, id: 1)
Fabricate(:status, account: account, id: 2)
Fabricate(:status, account: account, id: 3)
Fabricate(:status, account: account, id: 10)
- redis = double(zrevrangebyscore: [["val2", 2.0], ["val1", 1.0], ["val3", 3.0], ["deleted", 4.0]])
+ redis = double(zrevrangebyscore: [['val2', 2.0], ['val1', 1.0], ['val3', 3.0], ['deleted', 4.0]], exists: false)
allow(Redis).to receive(:current).and_return(redis)
- feed = Feed.new("type", account)
+ feed = Feed.new(:home, account)
results = feed.get(3)
expect(results.map(&:id)).to eq [2, 1, 3]
- expect(results.first.attributes.keys).to eq ["id", "updated_at"]
+ expect(results.first.attributes.keys).to eq %w(id updated_at)
end
end
end
diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb
index d3a66134b2096ebccc2374ee6bd7a1f82f90a03d..a3ced1abc48a1479649500bd7a6660b2ae8b14a1 100644
--- a/spec/models/status_spec.rb
+++ b/spec/models/status_spec.rb
@@ -119,161 +119,6 @@ RSpec.describe Status, type: :model do
end
end
- describe '#permitted?' do
- it 'returns true when direct and account is viewer' do
- subject.visibility = :direct
- expect(subject.permitted?(subject.account)).to be true
- end
-
- it 'returns true when direct and viewer is mentioned' do
- subject.visibility = :direct
- subject.mentions = [Fabricate(:mention, account: alice)]
-
- expect(subject.permitted?(alice)).to be true
- end
-
- it 'returns false when direct and viewer is not mentioned' do
- viewer = Fabricate(:account)
- subject.visibility = :direct
-
- expect(subject.permitted?(viewer)).to be false
- end
-
- it 'returns true when private and account is viewer' do
- subject.visibility = :direct
- expect(subject.permitted?(subject.account)).to be true
- end
-
- it 'returns true when private and account is following viewer' do
- follow = Fabricate(:follow)
- subject.visibility = :private
- subject.account = follow.target_account
-
- expect(subject.permitted?(follow.account)).to be true
- end
-
- it 'returns true when private and viewer is mentioned' do
- subject.visibility = :private
- subject.mentions = [Fabricate(:mention, account: alice)]
-
- expect(subject.permitted?(alice)).to be true
- end
-
- it 'returns false when private and viewer is not mentioned or followed' do
- viewer = Fabricate(:account)
- subject.visibility = :private
-
- expect(subject.permitted?(viewer)).to be false
- end
-
- it 'returns true when no viewer' do
- expect(subject.permitted?).to be true
- end
-
- it 'returns false when viewer is blocked' do
- block = Fabricate(:block)
- subject.visibility = :private
- subject.account = block.target_account
-
- expect(subject.permitted?(block.account)).to be false
- end
- end
-
- describe '#ancestors' do
- let!(:alice) { Fabricate(:account, username: 'alice') }
- let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') }
- let!(:jeff) { Fabricate(:account, username: 'jeff') }
- let!(:status) { Fabricate(:status, account: alice) }
- let!(:reply1) { Fabricate(:status, thread: status, account: jeff) }
- let!(:reply2) { Fabricate(:status, thread: reply1, account: bob) }
- let!(:reply3) { Fabricate(:status, thread: reply2, account: alice) }
- let!(:viewer) { Fabricate(:account, username: 'viewer') }
-
- it 'returns conversation history' do
- expect(reply3.ancestors).to include(status, reply1, reply2)
- end
-
- it 'does not return conversation history user is not allowed to see' do
- reply1.update(visibility: :private)
- status.update(visibility: :direct)
-
- expect(reply3.ancestors(viewer)).to_not include(reply1, status)
- end
-
- it 'does not return conversation history from blocked users' do
- viewer.block!(jeff)
- expect(reply3.ancestors(viewer)).to_not include(reply1)
- end
-
- it 'does not return conversation history from muted users' do
- viewer.mute!(jeff)
- expect(reply3.ancestors(viewer)).to_not include(reply1)
- end
-
- it 'does not return conversation history from silenced and not followed users' do
- jeff.update(silenced: true)
- expect(reply3.ancestors(viewer)).to_not include(reply1)
- end
-
- it 'does not return conversation history from blocked domains' do
- viewer.block_domain!('example.com')
- expect(reply3.ancestors(viewer)).to_not include(reply2)
- end
-
- it 'ignores deleted records' do
- first_status = Fabricate(:status, account: bob)
- second_status = Fabricate(:status, thread: first_status, account: alice)
-
- # Create cache and delete cached record
- second_status.ancestors
- first_status.destroy
-
- expect(second_status.ancestors).to eq([])
- end
- end
-
- describe '#descendants' do
- let!(:alice) { Fabricate(:account, username: 'alice') }
- let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') }
- let!(:jeff) { Fabricate(:account, username: 'jeff') }
- let!(:status) { Fabricate(:status, account: alice) }
- let!(:reply1) { Fabricate(:status, thread: status, account: alice) }
- let!(:reply2) { Fabricate(:status, thread: status, account: bob) }
- let!(:reply3) { Fabricate(:status, thread: reply1, account: jeff) }
- let!(:viewer) { Fabricate(:account, username: 'viewer') }
-
- it 'returns replies' do
- expect(status.descendants).to include(reply1, reply2, reply3)
- end
-
- it 'does not return replies user is not allowed to see' do
- reply1.update(visibility: :private)
- reply3.update(visibility: :direct)
-
- expect(status.descendants(viewer)).to_not include(reply1, reply3)
- end
-
- it 'does not return replies from blocked users' do
- viewer.block!(jeff)
- expect(status.descendants(viewer)).to_not include(reply3)
- end
-
- it 'does not return replies from muted users' do
- viewer.mute!(jeff)
- expect(status.descendants(viewer)).to_not include(reply3)
- end
-
- it 'does not return replies from silenced and not followed users' do
- jeff.update(silenced: true)
- expect(status.descendants(viewer)).to_not include(reply3)
- end
-
- it 'does not return replies from blocked domains' do
- viewer.block_domain!('example.com')
- expect(status.descendants(viewer)).to_not include(reply2)
- end
- end
-
describe '.mutes_map' do
let(:status) { Fabricate(:status) }
let(:account) { Fabricate(:account) }
@@ -336,15 +181,18 @@ RSpec.describe Status, type: :model do
end
describe '.as_home_timeline' do
+ let(:account) { Fabricate(:account) }
+ let(:followed) { Fabricate(:account) }
+ let(:not_followed) { Fabricate(:account) }
+
before do
- account = Fabricate(:account)
- followed = Fabricate(:account)
- not_followed = Fabricate(:account)
Fabricate(:follow, account: account, target_account: followed)
- @self_status = Fabricate(:status, account: account)
- @followed_status = Fabricate(:status, account: followed)
- @not_followed_status = Fabricate(:status, account: not_followed)
+ @self_status = Fabricate(:status, account: account, visibility: :public)
+ @self_direct_status = Fabricate(:status, account: account, visibility: :direct)
+ @followed_status = Fabricate(:status, account: followed, visibility: :public)
+ @followed_direct_status = Fabricate(:status, account: followed, visibility: :direct)
+ @not_followed_status = Fabricate(:status, account: not_followed, visibility: :public)
@results = Status.as_home_timeline(account)
end
@@ -353,10 +201,23 @@ RSpec.describe Status, type: :model do
expect(@results).to include(@self_status)
end
+ it 'includes direct statuses from self' do
+ expect(@results).to include(@self_direct_status)
+ end
+
it 'includes statuses from followed' do
expect(@results).to include(@followed_status)
end
+ it 'includes direct statuses mentioning recipient from followed' do
+ Fabricate(:mention, account: account, status: @followed_direct_status)
+ expect(@results).to include(@followed_direct_status)
+ end
+
+ it 'does not include direct statuses not mentioning recipient from followed' do
+ expect(@results).not_to include(@followed_direct_status)
+ end
+
it 'does not include statuses from non-followed' do
expect(@results).not_to include(@not_followed_status)
end
diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb
index 7a5b8ec897558e9f40a2b5b386778d5a721dd1c5..2496946cbec405249fd5343233ed97bda01a2460 100644
--- a/spec/models/tag_spec.rb
+++ b/spec/models/tag_spec.rb
@@ -22,5 +22,14 @@ RSpec.describe Tag, type: :model do
expect(results).to eq [tag]
end
+
+ it 'finds the exact matching tag as the first item' do
+ similar_tag = Fabricate(:tag, name: "matchlater")
+ tag = Fabricate(:tag, name: "match")
+
+ results = Tag.search_for("match")
+
+ expect(results).to eq [tag, similar_tag]
+ end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index c3e924fd81e146bc473618e2950dfe2b7beb0ec7..a6df3fb2621b098b3116e9f68e3929dfcc365eb1 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -4,6 +4,17 @@ require 'devise_two_factor/spec_helpers'
RSpec.describe User, type: :model do
it_behaves_like 'two_factor_backupable'
+ describe 'otp_secret' do
+ it 'is encrypted with OTP_SECRET environment variable' do
+ user = Fabricate(:user,
+ encrypted_otp_secret: "Fttsy7QAa0edaDfdfSz094rRLAxc8cJweDQ4BsWH/zozcdVA8o9GLqcKhn2b\nGi/V\n",
+ encrypted_otp_secret_iv: 'rys3THICkr60BoWC',
+ encrypted_otp_secret_salt: '_LMkAGvdg7a+sDIKjI3mR2Q==')
+
+ expect(user.otp_secret).to eq 'anotpsecretthatshouldbeencrypted'
+ end
+ end
+
describe 'validations' do
it 'is invalid without an account' do
user = Fabricate.build(:user, account: nil)
@@ -23,6 +34,12 @@ RSpec.describe User, type: :model do
expect(user).to model_have_error_on_field(:email)
end
+ it 'is valid with an invalid e-mail that has already been saved' do
+ user = Fabricate.build(:user, email: 'invalid-email')
+ user.save(validate: false)
+ expect(user.valid?).to be true
+ end
+
it 'cleans out empty string from languages' do
user = Fabricate.build(:user, filtered_languages: [''])
user.valid?
@@ -30,43 +47,12 @@ RSpec.describe User, type: :model do
end
end
- describe 'settings' do
- it 'inherits default settings from default yml' do
- expect(Setting.boost_modal).to eq false
- expect(Setting.interactions['must_be_follower']).to eq false
-
- user = User.new
- expect(user.settings.boost_modal).to eq false
- expect(user.settings.interactions['must_be_follower']).to eq false
- end
-
- it 'can update settings' do
- user = Fabricate(:user)
- expect(user.settings['interactions']['must_be_follower']).to eq false
- user.settings['interactions'] = user.settings['interactions'].merge('must_be_follower' => true)
- user.reload
-
- expect(user.settings['interactions']['must_be_follower']).to eq true
- end
-
- xit 'does not mutate defaults via the cache' do
- user = Fabricate(:user)
- user.settings['interactions']['must_be_follower'] = true
- # TODO
- # This mutates the global settings default such that future user
- # instances will inherit the incorrect starting values
-
- other = Fabricate(:user)
- expect(other.settings['interactions']['must_be_follower']).to eq false
- end
- end
-
describe 'scopes' do
describe 'recent' do
it 'returns an array of recent users ordered by id' do
user_1 = Fabricate(:user)
user_2 = Fabricate(:user)
- expect(User.recent).to match_array([user_2, user_1])
+ expect(User.recent).to eq [user_2, user_1]
end
end
@@ -85,6 +71,36 @@ RSpec.describe User, type: :model do
expect(User.confirmed).to match_array([user_2])
end
end
+
+ describe 'inactive' do
+ it 'returns a relation of inactive users' do
+ specified = Fabricate(:user, current_sign_in_at: 15.days.ago)
+ Fabricate(:user, current_sign_in_at: 13.days.ago)
+
+ expect(User.inactive).to match_array([specified])
+ end
+ end
+
+ describe 'matches_email' do
+ it 'returns a relation of users whose email starts with the given string' do
+ specified = Fabricate(:user, email: 'specified@spec')
+ Fabricate(:user, email: 'unspecified@spec')
+
+ expect(User.matches_email('specified')).to match_array([specified])
+ end
+ end
+
+ describe 'with_recent_ip_address' do
+ it 'returns a relation of users who is, or was at last time, online with the given IP address' do
+ specifieds = [
+ Fabricate(:user, current_sign_in_ip: '0.0.0.42',