Gab Social. All are welcome.
This commit is contained in:
64
spec/lib/activitypub/activity/accept_spec.rb
Normal file
64
spec/lib/activitypub/activity/accept_spec.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Accept do
|
||||
let(:sender) { Fabricate(:account) }
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Accept',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: {
|
||||
id: 'bar',
|
||||
type: 'Follow',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
object: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
},
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
Fabricate(:follow_request, account: recipient, target_account: sender)
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'creates a follow relationship' do
|
||||
expect(recipient.following?(sender)).to be true
|
||||
end
|
||||
|
||||
it 'removes the follow request' do
|
||||
expect(recipient.requested?(sender)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a relay' do
|
||||
let!(:relay) { Fabricate(:relay, state: :pending, follow_activity_id: 'https://abc-123/456') }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Accept',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: {
|
||||
id: 'https://abc-123/456',
|
||||
type: 'Follow',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
object: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
},
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
it 'marks the relay as accepted' do
|
||||
subject.perform
|
||||
expect(relay.reload.accepted?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
48
spec/lib/activitypub/activity/add_spec.rb
Normal file
48
spec/lib/activitypub/activity/add_spec.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Add do
|
||||
let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') }
|
||||
let(:status) { Fabricate(:status, account: sender) }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Add',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: ActivityPub::TagManager.instance.uri_for(status),
|
||||
target: sender.featured_collection_url,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
it 'creates a pin' do
|
||||
subject.perform
|
||||
expect(sender.pinned?(status)).to be true
|
||||
end
|
||||
|
||||
context 'when status was not known before' do
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Add',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: 'https://example.com/unknown',
|
||||
target: sender.featured_collection_url,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/unknown').to_return(status: 410)
|
||||
end
|
||||
|
||||
it 'fetches the status' do
|
||||
subject.perform
|
||||
expect(a_request(:get, 'https://example.com/unknown')).to have_been_made.at_least_once
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
172
spec/lib/activitypub/activity/announce_spec.rb
Normal file
172
spec/lib/activitypub/activity/announce_spec.rb
Normal file
@@ -0,0 +1,172 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Announce do
|
||||
let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', uri: 'https://example.com/actor') }
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
let(:status) { Fabricate(:status, account: recipient) }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Announce',
|
||||
actor: 'https://example.com/actor',
|
||||
object: object_json,
|
||||
to: 'http://example.com/followers',
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
let(:unknown_object_json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'https://example.com/actor/hello-world',
|
||||
type: 'Note',
|
||||
attributedTo: 'https://example.com/actor',
|
||||
content: 'Hello world',
|
||||
to: 'http://example.com/followers',
|
||||
}
|
||||
end
|
||||
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
describe '#perform' do
|
||||
context 'when sender is followed by a local account' do
|
||||
before do
|
||||
Fabricate(:account).follow!(sender)
|
||||
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json))
|
||||
subject.perform
|
||||
end
|
||||
|
||||
context 'a known status' do
|
||||
let(:object_json) do
|
||||
ActivityPub::TagManager.instance.uri_for(status)
|
||||
end
|
||||
|
||||
it 'creates a reblog by sender of status' do
|
||||
expect(sender.reblogged?(status)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'an unknown status' do
|
||||
let(:object_json) { 'https://example.com/actor/hello-world' }
|
||||
|
||||
it 'creates a reblog by sender of status' do
|
||||
reblog = sender.statuses.first
|
||||
|
||||
expect(reblog).to_not be_nil
|
||||
expect(reblog.reblog.text).to eq 'Hello world'
|
||||
end
|
||||
end
|
||||
|
||||
context 'self-repost of a previously unknown status with missing attributedTo' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: 'https://example.com/actor#bar',
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
to: 'http://example.com/followers',
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates a reblog by sender of status' do
|
||||
expect(sender.reblogged?(sender.statuses.first)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'self-repost of a previously unknown status with correct attributedTo' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: 'https://example.com/actor#bar',
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
attributedTo: 'https://example.com/actor',
|
||||
to: 'http://example.com/followers',
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates a reblog by sender of status' do
|
||||
expect(sender.reblogged?(sender.statuses.first)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status belongs to a local user' do
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
let(:object_json) do
|
||||
ActivityPub::TagManager.instance.uri_for(status)
|
||||
end
|
||||
|
||||
it 'creates a reblog by sender of status' do
|
||||
expect(sender.reblogged?(status)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the sender is relayed' do
|
||||
let!(:relay_account) { Fabricate(:account, inbox_url: 'https://relay.example.com/inbox') }
|
||||
let!(:relay) { Fabricate(:relay, inbox_url: 'https://relay.example.com/inbox') }
|
||||
|
||||
subject { described_class.new(json, sender, relayed_through_account: relay_account) }
|
||||
|
||||
context 'and the relay is enabled' do
|
||||
before do
|
||||
relay.update(state: :accepted)
|
||||
subject.perform
|
||||
end
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: 'https://example.com/actor#bar',
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
to: 'http://example.com/followers',
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates a reblog by sender of status' do
|
||||
expect(sender.statuses.count).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
context 'and the relay is disabled' do
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: 'https://example.com/actor#bar',
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
to: 'http://example.com/followers',
|
||||
}
|
||||
end
|
||||
|
||||
it 'does not create anything' do
|
||||
expect(sender.statuses.count).to eq 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the sender has no relevance to local activity' do
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: 'https://example.com/actor#bar',
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
to: 'http://example.com/followers',
|
||||
}
|
||||
end
|
||||
|
||||
it 'does not create anything' do
|
||||
expect(sender.statuses.count).to eq 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
85
spec/lib/activitypub/activity/block_spec.rb
Normal file
85
spec/lib/activitypub/activity/block_spec.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Block do
|
||||
let(:sender) { Fabricate(:account) }
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Block',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
context 'when the recipient does not follow the sender' do
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'creates a block from sender to recipient' do
|
||||
expect(sender.blocking?(recipient)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the recipient follows the sender' do
|
||||
before do
|
||||
recipient.follow!(sender)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'creates a block from sender to recipient' do
|
||||
expect(sender.blocking?(recipient)).to be true
|
||||
end
|
||||
|
||||
it 'ensures recipient is not following sender' do
|
||||
expect(recipient.following?(sender)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a matching undo has been received first' do
|
||||
let(:undo_json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'bar',
|
||||
type: 'Undo',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: json,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
before do
|
||||
recipient.follow!(sender)
|
||||
ActivityPub::Activity::Undo.new(undo_json, sender).perform
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'does not create a block from sender to recipient' do
|
||||
expect(sender.blocking?(recipient)).to be false
|
||||
end
|
||||
|
||||
it 'ensures recipient is not following sender' do
|
||||
expect(recipient.following?(sender)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
631
spec/lib/activitypub/activity/create_spec.rb
Normal file
631
spec/lib/activitypub/activity/create_spec.rb
Normal file
@@ -0,0 +1,631 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Create do
|
||||
let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor') }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join,
|
||||
type: 'Create',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: object_json,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
before do
|
||||
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
|
||||
|
||||
stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
|
||||
stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png'))
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when fetching' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
context 'unknown object type' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Banana',
|
||||
content: 'Lorem ipsum',
|
||||
}
|
||||
end
|
||||
|
||||
it 'does not create a status' do
|
||||
expect(sender.statuses.count).to be_zero
|
||||
end
|
||||
end
|
||||
|
||||
context 'standalone' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.text).to eq 'Lorem ipsum'
|
||||
end
|
||||
|
||||
it 'missing to/cc defaults to direct privacy' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.visibility).to eq 'direct'
|
||||
end
|
||||
end
|
||||
|
||||
context 'public' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
to: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.visibility).to eq 'public'
|
||||
end
|
||||
end
|
||||
|
||||
context 'unlisted' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
cc: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.visibility).to eq 'unlisted'
|
||||
end
|
||||
end
|
||||
|
||||
context 'private' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
to: 'http://example.com/followers',
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.visibility).to eq 'private'
|
||||
end
|
||||
end
|
||||
|
||||
context 'limited' do
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
to: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.visibility).to eq 'limited'
|
||||
end
|
||||
|
||||
it 'creates silent mention' do
|
||||
status = sender.statuses.first
|
||||
expect(status.mentions.first).to be_silent
|
||||
end
|
||||
end
|
||||
|
||||
context 'direct' do
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
to: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
tag: {
|
||||
type: 'Mention',
|
||||
href: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.visibility).to eq 'direct'
|
||||
end
|
||||
end
|
||||
|
||||
context 'as a reply' do
|
||||
let(:original_status) { Fabricate(:status) }
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status),
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.thread).to eq original_status
|
||||
expect(status.reply?).to be true
|
||||
expect(status.in_reply_to_account).to eq original_status.account
|
||||
expect(status.conversation).to eq original_status.conversation
|
||||
end
|
||||
end
|
||||
|
||||
context 'with mentions' do
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
tag: [
|
||||
{
|
||||
type: 'Mention',
|
||||
href: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.mentions.map(&:account)).to include(recipient)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with mentions missing href' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
tag: [
|
||||
{
|
||||
type: 'Mention',
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
expect(status).to_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with media attachments' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
attachment: [
|
||||
{
|
||||
type: 'Document',
|
||||
mediaType: 'image/png',
|
||||
url: 'http://example.com/attachment.png',
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with media attachments with focal points' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
attachment: [
|
||||
{
|
||||
type: 'Document',
|
||||
mediaType: 'image/png',
|
||||
url: 'http://example.com/attachment.png',
|
||||
focalPoint: [0.5, -0.7],
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.media_attachments.map(&:focus)).to include('0.5,-0.7')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with media attachments missing url' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
attachment: [
|
||||
{
|
||||
type: 'Document',
|
||||
mediaType: 'image/png',
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
expect(status).to_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with hashtags' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
tag: [
|
||||
{
|
||||
type: 'Hashtag',
|
||||
href: 'http://example.com/blah',
|
||||
name: '#test',
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.tags.map(&:name)).to include('test')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with hashtags missing name' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
tag: [
|
||||
{
|
||||
type: 'Hashtag',
|
||||
href: 'http://example.com/blah',
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
expect(status).to_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with emojis' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum :tinking:',
|
||||
tag: [
|
||||
{
|
||||
type: 'Emoji',
|
||||
icon: {
|
||||
url: 'http://example.com/emoji.png',
|
||||
},
|
||||
name: 'tinking',
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.emojis.map(&:shortcode)).to include('tinking')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with emojis missing name' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum :tinking:',
|
||||
tag: [
|
||||
{
|
||||
type: 'Emoji',
|
||||
icon: {
|
||||
url: 'http://example.com/emoji.png',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
expect(status).to_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with emojis missing icon' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum :tinking:',
|
||||
tag: [
|
||||
{
|
||||
type: 'Emoji',
|
||||
name: 'tinking',
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
expect(status).to_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with poll' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Question',
|
||||
content: 'Which color was the submarine?',
|
||||
oneOf: [
|
||||
{
|
||||
name: 'Yellow',
|
||||
replies: {
|
||||
type: 'Collection',
|
||||
totalItems: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Blue',
|
||||
replies: {
|
||||
type: 'Collection',
|
||||
totalItems: 3,
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
expect(status).to_not be_nil
|
||||
expect(status.poll).to_not be_nil
|
||||
end
|
||||
|
||||
it 'creates a poll' do
|
||||
poll = sender.polls.first
|
||||
expect(poll).to_not be_nil
|
||||
expect(poll.status).to_not be_nil
|
||||
expect(poll.options).to eq %w(Yellow Blue)
|
||||
expect(poll.cached_tallies).to eq [10, 3]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a vote to a local poll' do
|
||||
let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) }
|
||||
let!(:local_status) { Fabricate(:status, poll: poll) }
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
name: 'Yellow',
|
||||
inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status)
|
||||
}
|
||||
end
|
||||
|
||||
it 'adds a vote to the poll with correct uri' do
|
||||
vote = poll.votes.first
|
||||
expect(vote).to_not be_nil
|
||||
expect(vote.uri).to eq object_json[:id]
|
||||
expect(poll.reload.cached_tallies).to eq [1, 0]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a vote to an expired local poll' do
|
||||
let(:poll) do
|
||||
poll = Fabricate.build(:poll, options: %w(Yellow Blue), expires_at: 1.day.ago)
|
||||
poll.save(validate: false)
|
||||
poll
|
||||
end
|
||||
let!(:local_status) { Fabricate(:status, poll: poll) }
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
name: 'Yellow',
|
||||
inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status)
|
||||
}
|
||||
end
|
||||
|
||||
it 'does not add a vote to the poll' do
|
||||
expect(poll.votes.first).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sender is followed by local users' do
|
||||
subject { described_class.new(json, sender, delivery: true) }
|
||||
|
||||
before do
|
||||
Fabricate(:account).follow!(sender)
|
||||
subject.perform
|
||||
end
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.text).to eq 'Lorem ipsum'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sender replies to local status' do
|
||||
let!(:local_status) { Fabricate(:status) }
|
||||
|
||||
subject { described_class.new(json, sender, delivery: true) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status),
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.text).to eq 'Lorem ipsum'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sender targets a local user' do
|
||||
let!(:local_account) { Fabricate(:account) }
|
||||
|
||||
subject { described_class.new(json, sender, delivery: true) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
to: ActivityPub::TagManager.instance.uri_for(local_account),
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.text).to eq 'Lorem ipsum'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sender cc\'s a local user' do
|
||||
let!(:local_account) { Fabricate(:account) }
|
||||
|
||||
subject { described_class.new(json, sender, delivery: true) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
cc: ActivityPub::TagManager.instance.uri_for(local_account),
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.text).to eq 'Lorem ipsum'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the sender has no relevance to local activity' do
|
||||
subject { described_class.new(json, sender, delivery: true) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
}
|
||||
end
|
||||
|
||||
it 'does not create anything' do
|
||||
expect(sender.statuses.count).to eq 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
52
spec/lib/activitypub/activity/delete_spec.rb
Normal file
52
spec/lib/activitypub/activity/delete_spec.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Delete do
|
||||
let(:sender) { Fabricate(:account, domain: 'example.com') }
|
||||
let(:status) { Fabricate(:status, account: sender, uri: 'foobar') }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Delete',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: ActivityPub::TagManager.instance.uri_for(status),
|
||||
signature: 'foo',
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'deletes sender\'s status' do
|
||||
expect(Status.find_by(id: status.id)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status has been reblogged' do
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
let!(:reblogger) { Fabricate(:account) }
|
||||
let!(:follower) { Fabricate(:account, username: 'follower', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
|
||||
let!(:reblog) { Fabricate(:status, account: reblogger, reblog: status) }
|
||||
|
||||
before do
|
||||
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
|
||||
follower.follow!(reblogger)
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'deletes sender\'s status' do
|
||||
expect(Status.find_by(id: status.id)).to be_nil
|
||||
end
|
||||
|
||||
it 'sends delete activity to followers of rebloggers' do
|
||||
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
56
spec/lib/activitypub/activity/flag_spec.rb
Normal file
56
spec/lib/activitypub/activity/flag_spec.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Flag do
|
||||
let(:sender) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') }
|
||||
let(:flagged) { Fabricate(:account) }
|
||||
let(:status) { Fabricate(:status, account: flagged, uri: 'foobar') }
|
||||
let(:flag_id) { nil }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: flag_id,
|
||||
type: 'Flag',
|
||||
content: 'Boo!!',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: [
|
||||
ActivityPub::TagManager.instance.uri_for(flagged),
|
||||
ActivityPub::TagManager.instance.uri_for(status),
|
||||
],
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'creates a report' do
|
||||
report = Report.find_by(account: sender, target_account: flagged)
|
||||
|
||||
expect(report).to_not be_nil
|
||||
expect(report.comment).to eq 'Boo!!'
|
||||
expect(report.status_ids).to eq [status.id]
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform with a defined uri' do
|
||||
subject { described_class.new(json, sender) }
|
||||
let (:flag_id) { 'http://example.com/reports/1' }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'creates a report' do
|
||||
report = Report.find_by(account: sender, target_account: flagged)
|
||||
|
||||
expect(report).to_not be_nil
|
||||
expect(report.comment).to eq 'Boo!!'
|
||||
expect(report.status_ids).to eq [status.id]
|
||||
expect(report.uri).to eq flag_id
|
||||
end
|
||||
end
|
||||
end
|
||||
49
spec/lib/activitypub/activity/follow_spec.rb
Normal file
49
spec/lib/activitypub/activity/follow_spec.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Follow do
|
||||
let(:sender) { Fabricate(:account) }
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Follow',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
context 'unlocked account' do
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'creates a follow from sender to recipient' do
|
||||
expect(sender.following?(recipient)).to be true
|
||||
end
|
||||
|
||||
it 'does not create a follow request' do
|
||||
expect(sender.requested?(recipient)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'locked account' do
|
||||
before do
|
||||
recipient.update(locked: true)
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'does not create a follow from sender to recipient' do
|
||||
expect(sender.following?(recipient)).to be false
|
||||
end
|
||||
|
||||
it 'creates a follow request' do
|
||||
expect(sender.requested?(recipient)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
29
spec/lib/activitypub/activity/like_spec.rb
Normal file
29
spec/lib/activitypub/activity/like_spec.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Like do
|
||||
let(:sender) { Fabricate(:account) }
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
let(:status) { Fabricate(:status, account: recipient) }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Like',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: ActivityPub::TagManager.instance.uri_for(status),
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'creates a favourite from sender to status' do
|
||||
expect(sender.favourited?(status)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
52
spec/lib/activitypub/activity/move_spec.rb
Normal file
52
spec/lib/activitypub/activity/move_spec.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Move do
|
||||
let(:follower) { Fabricate(:account) }
|
||||
let(:old_account) { Fabricate(:account) }
|
||||
let(:new_account) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
follower.follow!(old_account)
|
||||
|
||||
old_account.update!(uri: 'https://example.org/alice', domain: 'example.org', protocol: :activitypub, inbox_url: 'https://example.org/inbox')
|
||||
new_account.update!(uri: 'https://example.com/alice', domain: 'example.com', protocol: :activitypub, inbox_url: 'https://example.com/inbox', also_known_as: [old_account.uri])
|
||||
|
||||
stub_request(:post, 'https://example.org/inbox').to_return(status: 200)
|
||||
stub_request(:post, 'https://example.com/inbox').to_return(status: 200)
|
||||
|
||||
service_stub = double
|
||||
allow(ActivityPub::FetchRemoteAccountService).to receive(:new).and_return(service_stub)
|
||||
allow(service_stub).to receive(:call).and_return(new_account)
|
||||
end
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Move',
|
||||
actor: old_account.uri,
|
||||
object: old_account.uri,
|
||||
target: new_account.uri,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, old_account) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'sets moved account on old account' do
|
||||
expect(old_account.reload.moved_to_account_id).to eq new_account.id
|
||||
end
|
||||
|
||||
it 'makes followers unfollow old account' do
|
||||
expect(follower.following?(old_account)).to be false
|
||||
end
|
||||
|
||||
it 'makes followers follow-request the new account' do
|
||||
expect(follower.requested?(new_account)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
64
spec/lib/activitypub/activity/reject_spec.rb
Normal file
64
spec/lib/activitypub/activity/reject_spec.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Reject do
|
||||
let(:sender) { Fabricate(:account) }
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Reject',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: {
|
||||
id: 'bar',
|
||||
type: 'Follow',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
object: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
},
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
Fabricate(:follow_request, account: recipient, target_account: sender)
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'does not create a follow relationship' do
|
||||
expect(recipient.following?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'removes the follow request' do
|
||||
expect(recipient.requested?(sender)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a relay' do
|
||||
let!(:relay) { Fabricate(:relay, state: :pending, follow_activity_id: 'https://abc-123/456') }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Reject',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: {
|
||||
id: 'https://abc-123/456',
|
||||
type: 'Follow',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
object: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
},
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
it 'marks the relay as rejected' do
|
||||
subject.perform
|
||||
expect(relay.reload.rejected?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
30
spec/lib/activitypub/activity/remove_spec.rb
Normal file
30
spec/lib/activitypub/activity/remove_spec.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Remove do
|
||||
let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') }
|
||||
let(:status) { Fabricate(:status, account: sender) }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Add',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: ActivityPub::TagManager.instance.uri_for(status),
|
||||
target: sender.featured_collection_url,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
StatusPin.create!(account: sender, status: status)
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'removes a pin' do
|
||||
expect(sender.pinned?(status)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
147
spec/lib/activitypub/activity/undo_spec.rb
Normal file
147
spec/lib/activitypub/activity/undo_spec.rb
Normal file
@@ -0,0 +1,147 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Undo do
|
||||
let(:sender) { Fabricate(:account, domain: 'example.com') }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Undo',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: object_json,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
describe '#perform' do
|
||||
context 'with Announce' do
|
||||
let(:status) { Fabricate(:status) }
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: 'bar',
|
||||
type: 'Announce',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: ActivityPub::TagManager.instance.uri_for(status),
|
||||
atomUri: 'barbar',
|
||||
}
|
||||
end
|
||||
|
||||
context do
|
||||
before do
|
||||
Fabricate(:status, reblog: status, account: sender, uri: 'bar')
|
||||
end
|
||||
|
||||
it 'deletes the reblog' do
|
||||
subject.perform
|
||||
expect(sender.reblogged?(status)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'with atomUri' do
|
||||
before do
|
||||
Fabricate(:status, reblog: status, account: sender, uri: 'barbar')
|
||||
end
|
||||
|
||||
it 'deletes the reblog by atomUri' do
|
||||
subject.perform
|
||||
expect(sender.reblogged?(status)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with Accept' do
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
let(:object_json) do
|
||||
{
|
||||
id: 'bar',
|
||||
type: 'Accept',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: 'follow-to-revoke',
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
recipient.follow!(sender, uri: 'follow-to-revoke')
|
||||
end
|
||||
|
||||
it 'deletes follow from recipient to sender' do
|
||||
subject.perform
|
||||
expect(recipient.following?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'creates a follow request from recipient to sender' do
|
||||
subject.perform
|
||||
expect(recipient.requested?(sender)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'with Block' do
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: 'bar',
|
||||
type: 'Block',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
sender.block!(recipient)
|
||||
end
|
||||
|
||||
it 'deletes block from sender to recipient' do
|
||||
subject.perform
|
||||
expect(sender.blocking?(recipient)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'with Follow' do
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: 'bar',
|
||||
type: 'Follow',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
sender.follow!(recipient)
|
||||
end
|
||||
|
||||
it 'deletes follow from sender to recipient' do
|
||||
subject.perform
|
||||
expect(sender.following?(recipient)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'with Like' do
|
||||
let(:status) { Fabricate(:status) }
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: 'bar',
|
||||
type: 'Like',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: ActivityPub::TagManager.instance.uri_for(status),
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
Fabricate(:favourite, account: sender, status: status)
|
||||
end
|
||||
|
||||
it 'deletes favourite from sender to status' do
|
||||
subject.perform
|
||||
expect(sender.favourited?(status)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
46
spec/lib/activitypub/activity/update_spec.rb
Normal file
46
spec/lib/activitypub/activity/update_spec.rb
Normal file
@@ -0,0 +1,46 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Update do
|
||||
let!(:sender) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
stub_request(:get, actor_json[:outbox]).to_return(status: 404)
|
||||
stub_request(:get, actor_json[:followers]).to_return(status: 404)
|
||||
stub_request(:get, actor_json[:following]).to_return(status: 404)
|
||||
stub_request(:get, actor_json[:featured]).to_return(status: 404)
|
||||
|
||||
sender.update!(uri: ActivityPub::TagManager.instance.uri_for(sender))
|
||||
end
|
||||
|
||||
let(:modified_sender) do
|
||||
sender.dup.tap do |modified_sender|
|
||||
modified_sender.display_name = 'Totally modified now'
|
||||
end
|
||||
end
|
||||
|
||||
let(:actor_json) do
|
||||
ActiveModelSerializers::SerializableResource.new(modified_sender, serializer: ActivityPub::ActorSerializer, key_transform: :camel_lower).as_json
|
||||
end
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Update',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: actor_json,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'updates profile' do
|
||||
expect(sender.reload.display_name).to eq 'Totally modified now'
|
||||
end
|
||||
end
|
||||
end
|
||||
88
spec/lib/activitypub/adapter_spec.rb
Normal file
88
spec/lib/activitypub/adapter_spec.rb
Normal file
@@ -0,0 +1,88 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Adapter do
|
||||
class TestObject < ActiveModelSerializers::Model
|
||||
attributes :foo
|
||||
end
|
||||
|
||||
class TestWithBasicContextSerializer < ActivityPub::Serializer
|
||||
attributes :foo
|
||||
end
|
||||
|
||||
class TestWithNamedContextSerializer < ActivityPub::Serializer
|
||||
context :security
|
||||
attributes :foo
|
||||
end
|
||||
|
||||
class TestWithNestedNamedContextSerializer < ActivityPub::Serializer
|
||||
attributes :foo
|
||||
|
||||
has_one :virtual_object, key: :baz, serializer: TestWithNamedContextSerializer
|
||||
|
||||
def virtual_object
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
class TestWithContextExtensionSerializer < ActivityPub::Serializer
|
||||
context_extensions :sensitive
|
||||
attributes :foo
|
||||
end
|
||||
|
||||
class TestWithNestedContextExtensionSerializer < ActivityPub::Serializer
|
||||
context_extensions :manually_approves_followers
|
||||
attributes :foo
|
||||
|
||||
has_one :virtual_object, key: :baz, serializer: TestWithContextExtensionSerializer
|
||||
|
||||
def virtual_object
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
describe '#serializable_hash' do
|
||||
let(:serializer_class) {}
|
||||
|
||||
subject { ActiveModelSerializers::SerializableResource.new(TestObject.new(foo: 'bar'), serializer: serializer_class, adapter: described_class).as_json }
|
||||
|
||||
context 'when serializer defines no context' do
|
||||
let(:serializer_class) { TestWithBasicContextSerializer }
|
||||
|
||||
it 'renders a basic @context' do
|
||||
expect(subject).to include({ '@context' => 'https://www.w3.org/ns/activitystreams' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when serializer defines a named context' do
|
||||
let(:serializer_class) { TestWithNamedContextSerializer }
|
||||
|
||||
it 'renders a @context with both items' do
|
||||
expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when serializer has children that define a named context' do
|
||||
let(:serializer_class) { TestWithNestedNamedContextSerializer }
|
||||
|
||||
it 'renders a @context with both items' do
|
||||
expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when serializer defines context extensions' do
|
||||
let(:serializer_class) { TestWithContextExtensionSerializer }
|
||||
|
||||
it 'renders a @context with the extension' do
|
||||
expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', { 'sensitive' => 'as:sensitive' }] })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when serializer has children that define context extensions' do
|
||||
let(:serializer_class) { TestWithNestedContextExtensionSerializer }
|
||||
|
||||
it 'renders a @context with both extensions' do
|
||||
expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'sensitive' => 'as:sensitive' }] })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
86
spec/lib/activitypub/linked_data_signature_spec.rb
Normal file
86
spec/lib/activitypub/linked_data_signature_spec.rb
Normal file
@@ -0,0 +1,86 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::LinkedDataSignature do
|
||||
include JsonLdHelper
|
||||
|
||||
let!(:sender) { Fabricate(:account, uri: 'http://example.com/alice') }
|
||||
|
||||
let(:raw_json) do
|
||||
{
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => 'http://example.com/hello-world',
|
||||
}
|
||||
end
|
||||
|
||||
let(:json) { raw_json.merge('signature' => signature) }
|
||||
|
||||
subject { described_class.new(json) }
|
||||
|
||||
before do
|
||||
stub_jsonld_contexts!
|
||||
end
|
||||
|
||||
describe '#verify_account!' do
|
||||
context 'when signature matches' do
|
||||
let(:raw_signature) do
|
||||
{
|
||||
'creator' => 'http://example.com/alice',
|
||||
'created' => '2017-09-23T20:21:34Z',
|
||||
}
|
||||
end
|
||||
|
||||
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
|
||||
|
||||
it 'returns creator' do
|
||||
expect(subject.verify_account!).to eq sender
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signature is missing' do
|
||||
let(:signature) { nil }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.verify_account!).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signature is tampered' do
|
||||
let(:raw_signature) do
|
||||
{
|
||||
'creator' => 'http://example.com/alice',
|
||||
'created' => '2017-09-23T20:21:34Z',
|
||||
}
|
||||
end
|
||||
|
||||
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => 's69F3mfddd99dGjmvjdjjs81e12jn121Gkm1') }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.verify_account!).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sign!' do
|
||||
subject { described_class.new(raw_json).sign!(sender) }
|
||||
|
||||
it 'returns a hash' do
|
||||
expect(subject).to be_a Hash
|
||||
end
|
||||
|
||||
it 'contains signature' do
|
||||
expect(subject['signature']).to be_a Hash
|
||||
expect(subject['signature']['signatureValue']).to be_present
|
||||
end
|
||||
|
||||
it 'can be verified again' do
|
||||
expect(described_class.new(subject).verify_account!).to eq sender
|
||||
end
|
||||
end
|
||||
|
||||
def sign(from_account, options, document)
|
||||
options_hash = Digest::SHA256.hexdigest(canonicalize(options.merge('@context' => ActivityPub::LinkedDataSignature::CONTEXT)))
|
||||
document_hash = Digest::SHA256.hexdigest(canonicalize(document))
|
||||
to_be_verified = options_hash + document_hash
|
||||
Base64.strict_encode64(from_account.keypair.sign(OpenSSL::Digest::SHA256.new, to_be_verified))
|
||||
end
|
||||
end
|
||||
157
spec/lib/activitypub/tag_manager_spec.rb
Normal file
157
spec/lib/activitypub/tag_manager_spec.rb
Normal file
@@ -0,0 +1,157 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::TagManager do
|
||||
include RoutingHelper
|
||||
|
||||
subject { described_class.instance }
|
||||
|
||||
describe '#url_for' do
|
||||
it 'returns a string' do
|
||||
account = Fabricate(:account)
|
||||
expect(subject.url_for(account)).to be_a String
|
||||
end
|
||||
end
|
||||
|
||||
describe '#uri_for' do
|
||||
it 'returns a string' do
|
||||
account = Fabricate(:account)
|
||||
expect(subject.uri_for(account)).to be_a String
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to' do
|
||||
it 'returns public collection for public status' do
|
||||
status = Fabricate(:status, visibility: :public)
|
||||
expect(subject.to(status)).to eq ['https://www.w3.org/ns/activitystreams#Public']
|
||||
end
|
||||
|
||||
it 'returns followers collection for unlisted status' do
|
||||
status = Fabricate(:status, visibility: :unlisted)
|
||||
expect(subject.to(status)).to eq [account_followers_url(status.account)]
|
||||
end
|
||||
|
||||
it 'returns followers collection for private status' do
|
||||
status = Fabricate(:status, visibility: :private)
|
||||
expect(subject.to(status)).to eq [account_followers_url(status.account)]
|
||||
end
|
||||
|
||||
it 'returns URIs of mentions for direct status' do
|
||||
status = Fabricate(:status, visibility: :direct)
|
||||
mentioned = Fabricate(:account)
|
||||
status.mentions.create(account: mentioned)
|
||||
expect(subject.to(status)).to eq [subject.uri_for(mentioned)]
|
||||
end
|
||||
|
||||
it "returns URIs of mentions for direct silenced author's status only if they are followers or requesting to be" do
|
||||
bob = Fabricate(:account, username: 'bob')
|
||||
alice = Fabricate(:account, username: 'alice')
|
||||
foo = Fabricate(:account)
|
||||
author = Fabricate(:account, username: 'author', silenced: true)
|
||||
status = Fabricate(:status, visibility: :direct, account: author)
|
||||
bob.follow!(author)
|
||||
FollowRequest.create!(account: foo, target_account: author)
|
||||
status.mentions.create(account: alice)
|
||||
status.mentions.create(account: bob)
|
||||
status.mentions.create(account: foo)
|
||||
expect(subject.to(status)).to include(subject.uri_for(bob))
|
||||
expect(subject.to(status)).to include(subject.uri_for(foo))
|
||||
expect(subject.to(status)).to_not include(subject.uri_for(alice))
|
||||
end
|
||||
end
|
||||
|
||||
describe '#cc' do
|
||||
it 'returns followers collection for public status' do
|
||||
status = Fabricate(:status, visibility: :public)
|
||||
expect(subject.cc(status)).to eq [account_followers_url(status.account)]
|
||||
end
|
||||
|
||||
it 'returns public collection for unlisted status' do
|
||||
status = Fabricate(:status, visibility: :unlisted)
|
||||
expect(subject.cc(status)).to eq ['https://www.w3.org/ns/activitystreams#Public']
|
||||
end
|
||||
|
||||
it 'returns empty array for private status' do
|
||||
status = Fabricate(:status, visibility: :private)
|
||||
expect(subject.cc(status)).to eq []
|
||||
end
|
||||
|
||||
it 'returns empty array for direct status' do
|
||||
status = Fabricate(:status, visibility: :direct)
|
||||
expect(subject.cc(status)).to eq []
|
||||
end
|
||||
|
||||
it 'returns URIs of mentions for non-direct status' do
|
||||
status = Fabricate(:status, visibility: :public)
|
||||
mentioned = Fabricate(:account)
|
||||
status.mentions.create(account: mentioned)
|
||||
expect(subject.cc(status)).to include(subject.uri_for(mentioned))
|
||||
end
|
||||
|
||||
it "returns URIs of mentions for silenced author's non-direct status only if they are followers or requesting to be" do
|
||||
bob = Fabricate(:account, username: 'bob')
|
||||
alice = Fabricate(:account, username: 'alice')
|
||||
foo = Fabricate(:account)
|
||||
author = Fabricate(:account, username: 'author', silenced: true)
|
||||
status = Fabricate(:status, visibility: :public, account: author)
|
||||
bob.follow!(author)
|
||||
FollowRequest.create!(account: foo, target_account: author)
|
||||
status.mentions.create(account: alice)
|
||||
status.mentions.create(account: bob)
|
||||
status.mentions.create(account: foo)
|
||||
expect(subject.cc(status)).to include(subject.uri_for(bob))
|
||||
expect(subject.cc(status)).to include(subject.uri_for(foo))
|
||||
expect(subject.cc(status)).to_not include(subject.uri_for(alice))
|
||||
end
|
||||
end
|
||||
|
||||
describe '#local_uri?' do
|
||||
it 'returns false for non-local URI' do
|
||||
expect(subject.local_uri?('http://example.com/123')).to be false
|
||||
end
|
||||
|
||||
it 'returns true for local URIs' do
|
||||
account = Fabricate(:account)
|
||||
expect(subject.local_uri?(subject.uri_for(account))).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#uri_to_local_id' do
|
||||
it 'returns the local ID' do
|
||||
account = Fabricate(:account)
|
||||
expect(subject.uri_to_local_id(subject.uri_for(account), :username)).to eq account.username
|
||||
end
|
||||
end
|
||||
|
||||
describe '#uri_to_resource' do
|
||||
it 'returns the local account' do
|
||||
account = Fabricate(:account)
|
||||
expect(subject.uri_to_resource(subject.uri_for(account), Account)).to eq account
|
||||
end
|
||||
|
||||
it 'returns the remote account by matching URI without fragment part' do
|
||||
account = Fabricate(:account, uri: 'https://example.com/123')
|
||||
expect(subject.uri_to_resource('https://example.com/123#456', Account)).to eq account
|
||||
end
|
||||
|
||||
it 'returns the local status for ActivityPub URI' do
|
||||
status = Fabricate(:status)
|
||||
expect(subject.uri_to_resource(subject.uri_for(status), Status)).to eq status
|
||||
end
|
||||
|
||||
it 'returns the local status for OStatus tag: URI' do
|
||||
status = Fabricate(:status)
|
||||
expect(subject.uri_to_resource(OStatus::TagManager.instance.uri_for(status), Status)).to eq status
|
||||
end
|
||||
|
||||
it 'returns the local status for OStatus StreamEntry URL' do
|
||||
status = Fabricate(:status)
|
||||
stream_entry_url = account_stream_entry_url(status.account, status.stream_entry)
|
||||
expect(subject.uri_to_resource(stream_entry_url, Status)).to eq status
|
||||
end
|
||||
|
||||
it 'returns the remote status by matching URI without fragment part' do
|
||||
status = Fabricate(:status, uri: 'https://example.com/123')
|
||||
expect(subject.uri_to_resource('https://example.com/123#456', Status)).to eq status
|
||||
end
|
||||
end
|
||||
end
|
||||
71
spec/lib/delivery_failure_tracker_spec.rb
Normal file
71
spec/lib/delivery_failure_tracker_spec.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe DeliveryFailureTracker do
|
||||
subject { described_class.new('http://example.com/inbox') }
|
||||
|
||||
describe '#track_success!' do
|
||||
before do
|
||||
subject.track_failure!
|
||||
subject.track_success!
|
||||
end
|
||||
|
||||
it 'marks URL as available again' do
|
||||
expect(described_class.available?('http://example.com/inbox')).to be true
|
||||
end
|
||||
|
||||
it 'resets days to 0' do
|
||||
expect(subject.days).to be_zero
|
||||
end
|
||||
end
|
||||
|
||||
describe '#track_failure!' do
|
||||
it 'marks URL as unavailable after 7 days of being called' do
|
||||
6.times { |i| Redis.current.sadd('exhausted_deliveries:http://example.com/inbox', i) }
|
||||
subject.track_failure!
|
||||
|
||||
expect(subject.days).to eq 7
|
||||
expect(described_class.unavailable?('http://example.com/inbox')).to be true
|
||||
end
|
||||
|
||||
it 'repeated calls on the same day do not count' do
|
||||
subject.track_failure!
|
||||
subject.track_failure!
|
||||
|
||||
expect(subject.days).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
describe '.filter' do
|
||||
before do
|
||||
Redis.current.sadd('unavailable_inboxes', 'http://example.com/unavailable/inbox')
|
||||
end
|
||||
|
||||
it 'removes URLs that are unavailable' do
|
||||
result = described_class.filter(['http://example.com/good/inbox', 'http://example.com/unavailable/inbox'])
|
||||
|
||||
expect(result).to include('http://example.com/good/inbox')
|
||||
expect(result).to_not include('http://example.com/unavailable/inbox')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.track_inverse_success!' do
|
||||
let(:from_account) { Fabricate(:account, inbox_url: 'http://example.com/inbox', shared_inbox_url: 'http://example.com/shared/inbox') }
|
||||
|
||||
before do
|
||||
Redis.current.sadd('unavailable_inboxes', 'http://example.com/inbox')
|
||||
Redis.current.sadd('unavailable_inboxes', 'http://example.com/shared/inbox')
|
||||
|
||||
described_class.track_inverse_success!(from_account)
|
||||
end
|
||||
|
||||
it 'marks inbox URL as available again' do
|
||||
expect(described_class.available?('http://example.com/inbox')).to be true
|
||||
end
|
||||
|
||||
it 'marks shared inbox URL as available again' do
|
||||
expect(described_class.available?('http://example.com/shared/inbox')).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
79
spec/lib/extractor_spec.rb
Normal file
79
spec/lib/extractor_spec.rb
Normal file
@@ -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
|
||||
407
spec/lib/feed_manager_spec.rb
Normal file
407
spec/lib/feed_manager_spec.rb
Normal file
@@ -0,0 +1,407 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe FeedManager do
|
||||
before do |example|
|
||||
unless example.metadata[:skip_stub]
|
||||
stub_const 'FeedManager::MAX_ITEMS', 10
|
||||
stub_const 'FeedManager::REBLOG_FALLOFF', 4
|
||||
end
|
||||
end
|
||||
|
||||
it 'tracks at least as many statuses as reblogs', skip_stub: true do
|
||||
expect(FeedManager::REBLOG_FALLOFF).to be <= FeedManager::MAX_ITEMS
|
||||
end
|
||||
|
||||
describe '#key' do
|
||||
subject { FeedManager.instance.key(:home, 1) }
|
||||
|
||||
it 'returns a string' do
|
||||
expect(subject).to be_a String
|
||||
end
|
||||
end
|
||||
|
||||
describe '#filter?' do
|
||||
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||
let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') }
|
||||
let(:jeff) { Fabricate(:account, username: 'jeff') }
|
||||
|
||||
context 'for home feed' do
|
||||
it 'returns false for followee\'s status' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:home, status, bob.id)).to be false
|
||||
end
|
||||
|
||||
it 'returns false for reblog by followee' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be false
|
||||
end
|
||||
|
||||
it 'returns true for reblog by followee of blocked account' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
bob.block!(jeff)
|
||||
expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for reblog by followee of muted account' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
bob.mute!(jeff)
|
||||
expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for reblog by followee of someone who is blocking recipient' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
jeff.block!(bob)
|
||||
expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for reblog from account with reblogs disabled' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||
bob.follow!(alice, reblogs: false)
|
||||
expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns false for reply by followee to another followee' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
bob.follow!(jeff)
|
||||
expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be false
|
||||
end
|
||||
|
||||
it 'returns false for reply by followee to recipient' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be false
|
||||
end
|
||||
|
||||
it 'returns false for reply by followee to self' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be false
|
||||
end
|
||||
|
||||
it 'returns true for reply by followee to non-followed account' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for the second reply by followee to a non-federated status' do
|
||||
reply = Fabricate(:status, text: 'Reply 1', reply: true, account: alice)
|
||||
second_reply = Fabricate(:status, text: 'Reply 2', thread: reply, account: alice)
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:home, second_reply, bob.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns false for status by followee mentioning another account' do
|
||||
bob.follow!(alice)
|
||||
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
||||
expect(FeedManager.instance.filter?(:home, status, bob.id)).to be false
|
||||
end
|
||||
|
||||
it 'returns true for status by followee mentioning blocked account' do
|
||||
bob.block!(jeff)
|
||||
bob.follow!(alice)
|
||||
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
||||
expect(FeedManager.instance.filter?(:home, status, bob.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for reblog of a personally blocked domain' do
|
||||
alice.block_domain!('example.com')
|
||||
alice.follow!(jeff)
|
||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||
reblog = Fabricate(:status, reblog: status, account: jeff)
|
||||
expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
|
||||
end
|
||||
|
||||
context 'for irreversibly muted phrases' do
|
||||
it 'considers word boundaries when matching' do
|
||||
alice.custom_filters.create!(phrase: 'bob', context: %w(home), irreversible: true)
|
||||
alice.follow!(jeff)
|
||||
status = Fabricate(:status, text: 'bobcats', account: jeff)
|
||||
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be_falsy
|
||||
end
|
||||
|
||||
it 'returns true if phrase is contained' do
|
||||
alice.custom_filters.create!(phrase: 'farts', context: %w(home public), irreversible: true)
|
||||
alice.custom_filters.create!(phrase: 'pop tarts', context: %w(home), irreversible: true)
|
||||
alice.follow!(jeff)
|
||||
status = Fabricate(:status, text: 'i sure like POP TARts', account: jeff)
|
||||
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
|
||||
end
|
||||
|
||||
it 'matches substrings if whole_word is false' do
|
||||
alice.custom_filters.create!(phrase: 'take', context: %w(home), whole_word: false, irreversible: true)
|
||||
alice.follow!(jeff)
|
||||
status = Fabricate(:status, text: 'shiitake', account: jeff)
|
||||
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for mentions feed' do
|
||||
it 'returns true for status that mentions blocked account' do
|
||||
bob.block!(jeff)
|
||||
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
||||
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for status that replies to a blocked account' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||
bob.block!(jeff)
|
||||
expect(FeedManager.instance.filter?(:mentions, reply, bob.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for status by silenced account who recipient is not following' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
alice.silence!
|
||||
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns false for status by followed silenced account' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
alice.silence!
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#push_to_home' do
|
||||
it 'trims timelines if they will have more than FeedManager::MAX_ITEMS' do
|
||||
account = Fabricate(:account)
|
||||
status = Fabricate(:status)
|
||||
members = FeedManager::MAX_ITEMS.times.map { |count| [count, count] }
|
||||
Redis.current.zadd("feed:home:#{account.id}", members)
|
||||
|
||||
FeedManager.instance.push_to_home(account, status)
|
||||
|
||||
expect(Redis.current.zcard("feed:home:#{account.id}")).to eq FeedManager::MAX_ITEMS
|
||||
end
|
||||
|
||||
context 'reblogs' do
|
||||
it 'saves reblogs of unseen statuses' do
|
||||
account = Fabricate(:account)
|
||||
reblogged = Fabricate(:status)
|
||||
reblog = Fabricate(:status, reblog: reblogged)
|
||||
|
||||
expect(FeedManager.instance.push_to_home(account, reblog)).to be true
|
||||
end
|
||||
|
||||
it 'does not save a new reblog of a recent status' do
|
||||
account = Fabricate(:account)
|
||||
reblogged = Fabricate(:status)
|
||||
reblog = Fabricate(:status, reblog: reblogged)
|
||||
|
||||
FeedManager.instance.push_to_home(account, reblogged)
|
||||
|
||||
expect(FeedManager.instance.push_to_home(account, reblog)).to be false
|
||||
end
|
||||
|
||||
it 'saves a new reblog of an old status' do
|
||||
account = Fabricate(:account)
|
||||
reblogged = Fabricate(:status)
|
||||
reblog = Fabricate(:status, reblog: reblogged)
|
||||
|
||||
FeedManager.instance.push_to_home(account, reblogged)
|
||||
|
||||
# Fill the feed with intervening statuses
|
||||
FeedManager::REBLOG_FALLOFF.times do
|
||||
FeedManager.instance.push_to_home(account, Fabricate(:status))
|
||||
end
|
||||
|
||||
expect(FeedManager.instance.push_to_home(account, reblog)).to be true
|
||||
end
|
||||
|
||||
it 'does not save a new reblog of a recently-reblogged status' do
|
||||
account = Fabricate(:account)
|
||||
reblogged = Fabricate(:status)
|
||||
reblogs = 2.times.map { Fabricate(:status, reblog: reblogged) }
|
||||
|
||||
# The first reblog will be accepted
|
||||
FeedManager.instance.push_to_home(account, reblogs.first)
|
||||
|
||||
# The second reblog should be ignored
|
||||
expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be false
|
||||
end
|
||||
|
||||
it 'does not save a new reblog of a multiply-reblogged-then-unreblogged status' do
|
||||
account = Fabricate(:account)
|
||||
reblogged = Fabricate(:status)
|
||||
reblogs = 3.times.map { Fabricate(:status, reblog: reblogged) }
|
||||
|
||||
# Accept the reblogs
|
||||
FeedManager.instance.push_to_home(account, reblogs[0])
|
||||
FeedManager.instance.push_to_home(account, reblogs[1])
|
||||
|
||||
# Unreblog the first one
|
||||
FeedManager.instance.unpush_from_home(account, reblogs[0])
|
||||
|
||||
# The last reblog should still be ignored
|
||||
expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be false
|
||||
end
|
||||
|
||||
it 'saves a new reblog of a long-ago-reblogged status' do
|
||||
account = Fabricate(:account)
|
||||
reblogged = Fabricate(:status)
|
||||
reblogs = 2.times.map { Fabricate(:status, reblog: reblogged) }
|
||||
|
||||
# The first reblog will be accepted
|
||||
FeedManager.instance.push_to_home(account, reblogs.first)
|
||||
|
||||
# Fill the feed with intervening statuses
|
||||
FeedManager::REBLOG_FALLOFF.times do
|
||||
FeedManager.instance.push_to_home(account, Fabricate(:status))
|
||||
end
|
||||
|
||||
# The second reblog should also be accepted
|
||||
expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
it "does not push when the given status's reblog is already inserted" do
|
||||
account = Fabricate(:account)
|
||||
reblog = Fabricate(:status)
|
||||
status = Fabricate(:status, reblog: reblog)
|
||||
FeedManager.instance.push_to_home(account, status)
|
||||
|
||||
expect(FeedManager.instance.push_to_home(account, reblog)).to eq false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#push_to_list' do
|
||||
it "does not push when the given status's reblog is already inserted" do
|
||||
list = Fabricate(:list)
|
||||
reblog = Fabricate(:status)
|
||||
status = Fabricate(:status, reblog: reblog)
|
||||
FeedManager.instance.push_to_list(list, status)
|
||||
|
||||
expect(FeedManager.instance.push_to_list(list, reblog)).to eq false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#merge_into_timeline' do
|
||||
it "does not push source account's statuses whose reblogs are already inserted" do
|
||||
account = Fabricate(:account, id: 0)
|
||||
reblog = Fabricate(:status)
|
||||
status = Fabricate(:status, reblog: reblog)
|
||||
FeedManager.instance.push_to_home(account, status)
|
||||
|
||||
FeedManager.instance.merge_into_timeline(account, reblog.account)
|
||||
|
||||
expect(Redis.current.zscore("feed:home:0", reblog.id)).to eq nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#trim' do
|
||||
let(:receiver) { Fabricate(:account) }
|
||||
|
||||
it 'cleans up reblog tracking keys' do
|
||||
reblogged = Fabricate(:status)
|
||||
status = Fabricate(:status, reblog: reblogged)
|
||||
another_status = Fabricate(:status, reblog: reblogged)
|
||||
reblogs_key = FeedManager.instance.key('home', receiver.id, 'reblogs')
|
||||
reblog_set_key = FeedManager.instance.key('home', receiver.id, "reblogs:#{reblogged.id}")
|
||||
|
||||
FeedManager.instance.push_to_home(receiver, status)
|
||||
FeedManager.instance.push_to_home(receiver, another_status)
|
||||
|
||||
# We should have a tracking set and an entry in reblogs.
|
||||
expect(Redis.current.exists(reblog_set_key)).to be true
|
||||
expect(Redis.current.zrange(reblogs_key, 0, -1)).to eq [reblogged.id.to_s]
|
||||
|
||||
# Push everything off the end of the feed.
|
||||
FeedManager::MAX_ITEMS.times do
|
||||
FeedManager.instance.push_to_home(receiver, Fabricate(:status))
|
||||
end
|
||||
|
||||
# `trim` should be called automatically, but do it anyway, as
|
||||
# we're testing `trim`, not side effects of `push`.
|
||||
FeedManager.instance.trim('home', receiver.id)
|
||||
|
||||
# We should not have any reblog tracking data.
|
||||
expect(Redis.current.exists(reblog_set_key)).to be false
|
||||
expect(Redis.current.zrange(reblogs_key, 0, -1)).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#unpush' do
|
||||
let(:receiver) { Fabricate(:account) }
|
||||
|
||||
it 'leaves a reblogged status if original was on feed' do
|
||||
reblogged = Fabricate(:status)
|
||||
status = Fabricate(:status, reblog: reblogged)
|
||||
|
||||
FeedManager.instance.push_to_home(receiver, reblogged)
|
||||
FeedManager::REBLOG_FALLOFF.times { FeedManager.instance.push_to_home(receiver, Fabricate(:status)) }
|
||||
FeedManager.instance.push_to_home(receiver, status)
|
||||
|
||||
# The reblogging status should show up under normal conditions.
|
||||
expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
|
||||
|
||||
FeedManager.instance.unpush_from_home(receiver, status)
|
||||
|
||||
# Restore original status
|
||||
expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s)
|
||||
expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to include(reblogged.id.to_s)
|
||||
end
|
||||
|
||||
it 'removes a reblogged status if it was only reblogged once' do
|
||||
reblogged = Fabricate(:status)
|
||||
status = Fabricate(:status, reblog: reblogged)
|
||||
|
||||
FeedManager.instance.push_to_home(receiver, status)
|
||||
|
||||
# The reblogging status should show up under normal conditions.
|
||||
expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [status.id.to_s]
|
||||
|
||||
FeedManager.instance.unpush_from_home(receiver, status)
|
||||
|
||||
expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to be_empty
|
||||
end
|
||||
|
||||
it 'leaves a multiply-reblogged status if another reblog was in feed' do
|
||||
reblogged = Fabricate(:status)
|
||||
reblogs = 3.times.map { Fabricate(:status, reblog: reblogged) }
|
||||
|
||||
reblogs.each do |reblog|
|
||||
FeedManager.instance.push_to_home(receiver, reblog)
|
||||
end
|
||||
|
||||
# The reblogging status should show up under normal conditions.
|
||||
expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.first.id.to_s]
|
||||
|
||||
reblogs[0...-1].each do |reblog|
|
||||
FeedManager.instance.unpush_from_home(receiver, reblog)
|
||||
end
|
||||
|
||||
expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.last.id.to_s]
|
||||
end
|
||||
|
||||
it 'sends push updates' do
|
||||
status = Fabricate(:status)
|
||||
|
||||
FeedManager.instance.push_to_home(receiver, status)
|
||||
|
||||
allow(Redis.current).to receive_messages(publish: nil)
|
||||
FeedManager.instance.unpush_from_home(receiver, status)
|
||||
|
||||
deletion = Oj.dump(event: :delete, payload: status.id.to_s)
|
||||
expect(Redis.current).to have_received(:publish).with("timeline:#{receiver.id}", deletion)
|
||||
end
|
||||
end
|
||||
end
|
||||
594
spec/lib/formatter_spec.rb
Normal file
594
spec/lib/formatter_spec.rb
Normal file
@@ -0,0 +1,594 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Formatter do
|
||||
let(:local_account) { Fabricate(:account, domain: nil, username: 'alice') }
|
||||
let(:remote_account) { Fabricate(:account, domain: 'remote.test', username: 'bob', url: 'https://remote.test/') }
|
||||
|
||||
shared_examples 'encode and link URLs' do
|
||||
context 'given a stand-alone medium URL' do
|
||||
let(:text) { 'https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4' }
|
||||
|
||||
it 'matches the full URL' do
|
||||
is_expected.to include 'href="https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a stand-alone google URL' do
|
||||
let(:text) { 'http://google.com' }
|
||||
|
||||
it 'matches the full URL' do
|
||||
is_expected.to include 'href="http://google.com"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a stand-alone IDN URL' do
|
||||
let(:text) { 'https://nic.みんな/' }
|
||||
|
||||
it 'matches the full URL' do
|
||||
is_expected.to include 'href="https://nic.みんな/"'
|
||||
end
|
||||
|
||||
it 'has display URL' do
|
||||
is_expected.to include '<span class="">nic.みんな/</span>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL with a trailing period' do
|
||||
let(:text) { 'http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona. ' }
|
||||
|
||||
it 'matches the full URL but not the period' do
|
||||
is_expected.to include 'href="http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL enclosed with parentheses' do
|
||||
let(:text) { '(http://google.com/)' }
|
||||
|
||||
it 'matches the full URL but not the parentheses' do
|
||||
is_expected.to include 'href="http://google.com/"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL with a trailing exclamation point' do
|
||||
let(:text) { 'http://www.google.com!' }
|
||||
|
||||
it 'matches the full URL but not the exclamation point' do
|
||||
is_expected.to include 'href="http://www.google.com"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL with a trailing single quote' do
|
||||
let(:text) { "http://www.google.com'" }
|
||||
|
||||
it 'matches the full URL but not the single quote' do
|
||||
is_expected.to include 'href="http://www.google.com"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL with a trailing angle bracket' do
|
||||
let(:text) { 'http://www.google.com>' }
|
||||
|
||||
it 'matches the full URL but not the angle bracket' do
|
||||
is_expected.to include 'href="http://www.google.com"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL with a query string' do
|
||||
context 'with escaped unicode character' do
|
||||
let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' }
|
||||
|
||||
it 'matches the full URL' do
|
||||
is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unicode character' do
|
||||
let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓&q=autolink' }
|
||||
|
||||
it 'matches the full URL' do
|
||||
is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓&q=autolink"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unicode character at the end' do
|
||||
let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓' }
|
||||
|
||||
it 'matches the full URL' do
|
||||
is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with escaped and not escaped unicode characters' do
|
||||
let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=✓&q=autolink' }
|
||||
|
||||
it 'preserves escaped unicode characters' do
|
||||
is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=✓&q=autolink"'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL with parentheses in it' do
|
||||
let(:text) { 'https://en.wikipedia.org/wiki/Diaspora_(software)' }
|
||||
|
||||
it 'matches the full URL' do
|
||||
is_expected.to include 'href="https://en.wikipedia.org/wiki/Diaspora_(software)"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL in quotation marks' do
|
||||
let(:text) { '"https://example.com/"' }
|
||||
|
||||
it 'does not match the quotation marks' do
|
||||
is_expected.to include 'href="https://example.com/"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL in angle brackets' do
|
||||
let(:text) { '<https://example.com/>' }
|
||||
|
||||
it 'does not match the angle brackets' do
|
||||
is_expected.to include 'href="https://example.com/"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL with Japanese path string' do
|
||||
let(:text) { 'https://ja.wikipedia.org/wiki/日本' }
|
||||
|
||||
it 'matches the full URL' do
|
||||
is_expected.to include 'href="https://ja.wikipedia.org/wiki/日本"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL with Korean path string' do
|
||||
let(:text) { 'https://ko.wikipedia.org/wiki/대한민국' }
|
||||
|
||||
it 'matches the full URL' do
|
||||
is_expected.to include 'href="https://ko.wikipedia.org/wiki/대한민국"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL with a full-width space' do
|
||||
let(:text) { 'https://example.com/ abc123' }
|
||||
|
||||
it 'does not match the full-width space' do
|
||||
is_expected.to include 'href="https://example.com/"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL in Japanese quotation marks' do
|
||||
let(:text) { '「[https://example.org/」' }
|
||||
|
||||
it 'does not match the quotation marks' do
|
||||
is_expected.to include 'href="https://example.org/"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL with Simplified Chinese path string' do
|
||||
let(:text) { 'https://baike.baidu.com/item/中华人民共和国' }
|
||||
|
||||
it 'matches the full URL' do
|
||||
is_expected.to include 'href="https://baike.baidu.com/item/中华人民共和国"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL with Traditional Chinese path string' do
|
||||
let(:text) { 'https://zh.wikipedia.org/wiki/臺灣' }
|
||||
|
||||
it 'matches the full URL' do
|
||||
is_expected.to include 'href="https://zh.wikipedia.org/wiki/臺灣"'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL containing unsafe code (XSS attack, visible part)' do
|
||||
let(:text) { %q{http://example.com/b<del>b</del>} }
|
||||
|
||||
it 'does not include the HTML in the URL' do
|
||||
is_expected.to include '"http://example.com/b"'
|
||||
end
|
||||
|
||||
it 'escapes the HTML' do
|
||||
is_expected.to include '<del>b</del>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a URL containing unsafe code (XSS attack, invisible part)' do
|
||||
let(:text) { %q{http://example.com/blahblahblahblah/a<script>alert("Hello")</script>} }
|
||||
|
||||
it 'does not include the HTML in the URL' do
|
||||
is_expected.to include '"http://example.com/blahblahblahblah/a"'
|
||||
end
|
||||
|
||||
it 'escapes the HTML' do
|
||||
is_expected.to include '<script>alert("Hello")</script>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given text containing HTML code (script tag)' do
|
||||
let(:text) { '<script>alert("Hello")</script>' }
|
||||
|
||||
it 'escapes the HTML' do
|
||||
is_expected.to include '<p><script>alert("Hello")</script></p>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given text containing HTML (XSS attack)' do
|
||||
let(:text) { %q{<img src="javascript:alert('XSS');">} }
|
||||
|
||||
it 'escapes the HTML' do
|
||||
is_expected.to include '<p><img src="javascript:alert('XSS');"></p>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given an invalid URL' do
|
||||
let(:text) { 'http://www\.google\.com' }
|
||||
|
||||
it 'outputs the raw URL' do
|
||||
is_expected.to eq '<p>http://www\.google\.com</p>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given text containing a hashtag' do
|
||||
let(:text) { '#hashtag' }
|
||||
|
||||
it 'creates a hashtag link' do
|
||||
is_expected.to include '/tags/hashtag" class="mention hashtag" rel="tag">#<span>hashtag</span></a>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given text containing a hashtag with Unicode chars' do
|
||||
let(:text) { '#hashtagタグ' }
|
||||
|
||||
it 'creates a hashtag link' do
|
||||
is_expected.to include '/tags/hashtag%E3%82%BF%E3%82%B0" class="mention hashtag" rel="tag">#<span>hashtagタグ</span></a>'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#format_spoiler' do
|
||||
subject { Formatter.instance.format_spoiler(status) }
|
||||
|
||||
context 'given a post containing plain text' do
|
||||
let(:status) { Fabricate(:status, text: 'text', spoiler_text: 'Secret!', uri: nil) }
|
||||
|
||||
it 'Returns the spoiler text' do
|
||||
is_expected.to eq 'Secret!'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with an emoji shortcode at the start' do
|
||||
let!(:emoji) { Fabricate(:custom_emoji) }
|
||||
let(:status) { Fabricate(:status, text: 'text', spoiler_text: ':coolcat: Secret!', uri: nil) }
|
||||
let(:text) { ':coolcat: Beep boop' }
|
||||
|
||||
it 'converts the shortcode to an image tag' do
|
||||
is_expected.to match(/<img draggable="false" class="emojione" alt=":coolcat:"/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#format' do
|
||||
subject { Formatter.instance.format(status) }
|
||||
|
||||
context 'given a post with local status' do
|
||||
context 'given a reblogged post' 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 <span class="h-card"><a href="https://cb6e6126.ngrok.io/@alice" class="u-url mention">@<span>alice</span></a></span> Hello world'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post containing plain text' do
|
||||
let(:status) { Fabricate(:status, text: 'text', uri: nil) }
|
||||
|
||||
it 'paragraphizes the text' do
|
||||
is_expected.to eq '<p>text</p>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post containing 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 'given a post containing linkable mentions' do
|
||||
let(:status) { Fabricate(:status, mentions: [ Fabricate(:mention, account: local_account) ], text: '@alice') }
|
||||
|
||||
it 'creates a mention link' do
|
||||
is_expected.to include '<a href="https://cb6e6126.ngrok.io/@alice" class="u-url mention">@<span>alice</span></a></span>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post containing unlinkable mentions' do
|
||||
let(:status) { Fabricate(:status, text: '@alice', uri: nil) }
|
||||
|
||||
it 'does not create a mention 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
|
||||
|
||||
context 'given a post with custom_emojify option' do
|
||||
let!(:emoji) { Fabricate(:custom_emoji) }
|
||||
let(:status) { Fabricate(:status, account: local_account, text: text) }
|
||||
|
||||
subject { Formatter.instance.format(status, custom_emojify: true) }
|
||||
|
||||
context 'given a post with an emoji shortcode at the start' do
|
||||
let(:text) { ':coolcat: Beep boop' }
|
||||
|
||||
it 'converts the shortcode to an image tag' do
|
||||
is_expected.to match(/<p><img draggable="false" class="emojione" alt=":coolcat:"/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with an emoji shortcode in the middle' do
|
||||
let(:text) { 'Beep :coolcat: boop' }
|
||||
|
||||
it 'converts the shortcode to an image tag' do
|
||||
is_expected.to match(/Beep <img draggable="false" class="emojione" alt=":coolcat:"/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with concatenated emoji shortcodes' do
|
||||
let(:text) { ':coolcat::coolcat:' }
|
||||
|
||||
it 'does not touch the shortcodes' do
|
||||
is_expected.to match(/:coolcat::coolcat:/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with an emoji shortcode at the end' do
|
||||
let(:text) { 'Beep boop :coolcat:' }
|
||||
|
||||
it 'converts the shortcode to an image tag' do
|
||||
is_expected.to match(/boop <img draggable="false" class="emojione" alt=":coolcat:"/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with remote status' do
|
||||
let(:status) { Fabricate(:status, account: remote_account, text: 'Beep boop') }
|
||||
|
||||
it 'reformats the post' do
|
||||
is_expected.to eq 'Beep boop'
|
||||
end
|
||||
|
||||
context 'given a post with custom_emojify option' do
|
||||
let!(:emoji) { Fabricate(:custom_emoji, domain: remote_account.domain) }
|
||||
let(:status) { Fabricate(:status, account: remote_account, text: text) }
|
||||
|
||||
subject { Formatter.instance.format(status, custom_emojify: true) }
|
||||
|
||||
context 'given a post with an emoji shortcode at the start' do
|
||||
let(:text) { '<p>:coolcat: Beep boop<br />' }
|
||||
|
||||
it 'converts the shortcode to an image tag' do
|
||||
is_expected.to match(/<p><img draggable="false" class="emojione" alt=":coolcat:"/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with an emoji shortcode in the middle' do
|
||||
let(:text) { '<p>Beep :coolcat: boop</p>' }
|
||||
|
||||
it 'converts the shortcode to an image tag' do
|
||||
is_expected.to match(/Beep <img draggable="false" class="emojione" alt=":coolcat:"/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with concatenated emoji' do
|
||||
let(:text) { '<p>:coolcat::coolcat:</p>' }
|
||||
|
||||
it 'does not touch the shortcodes' do
|
||||
is_expected.to match(/<p>:coolcat::coolcat:<\/p>/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with an emoji shortcode at the end' do
|
||||
let(:text) { '<p>Beep boop<br />:coolcat:</p>' }
|
||||
|
||||
it 'converts the shortcode to an image tag' do
|
||||
is_expected.to match(/<br><img draggable="false" class="emojione" alt=":coolcat:"/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#reformat' do
|
||||
subject { Formatter.instance.reformat(text) }
|
||||
|
||||
context 'given a post containing plain text' do
|
||||
let(:text) { 'Beep boop' }
|
||||
|
||||
it 'keeps the plain text' do
|
||||
is_expected.to include 'Beep boop'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post containing script tags' do
|
||||
let(:text) { '<script>alert("Hello")</script>' }
|
||||
|
||||
it 'strips the scripts' do
|
||||
is_expected.to_not include '<script>alert("Hello")</script>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post containing malicious classes' do
|
||||
let(:text) { '<span class="mention status__content__spoiler-link">Show more</span>' }
|
||||
|
||||
it 'strips the malicious classes' do
|
||||
is_expected.to_not include 'status__content__spoiler-link'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#plaintext' do
|
||||
subject { Formatter.instance.plaintext(status) }
|
||||
|
||||
context 'given a post with local status' do
|
||||
let(:status) { Fabricate(:status, text: '<p>a text by a nerd who uses an HTML tag in text</p>', uri: nil) }
|
||||
|
||||
it 'returns the raw text' do
|
||||
is_expected.to eq '<p>a text by a nerd who uses an HTML tag in text</p>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with remote status' do
|
||||
let(:status) { Fabricate(:status, account: remote_account, text: '<script>alert("Hello")</script>') }
|
||||
|
||||
it 'returns tag-stripped text' do
|
||||
is_expected.to eq ''
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#simplified_format' do
|
||||
subject { Formatter.instance.simplified_format(account) }
|
||||
|
||||
context 'given a post with local status' do
|
||||
let(:account) { Fabricate(:account, domain: nil, note: text) }
|
||||
|
||||
context 'given a post containing linkable mentions for local accounts' do
|
||||
let(:text) { '@alice' }
|
||||
|
||||
before { local_account }
|
||||
|
||||
it 'creates a mention link' do
|
||||
is_expected.to eq '<p><span class="h-card"><a href="https://cb6e6126.ngrok.io/@alice" class="u-url mention">@<span>alice</span></a></span></p>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post containing linkable mentions for remote accounts' do
|
||||
let(:text) { '@bob@remote.test' }
|
||||
|
||||
before { remote_account }
|
||||
|
||||
it 'creates a mention link' do
|
||||
is_expected.to eq '<p><span class="h-card"><a href="https://remote.test/" class="u-url mention">@<span>bob</span></a></span></p>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post containing unlinkable mentions' do
|
||||
let(:text) { '@alice' }
|
||||
|
||||
it 'does not create a mention link' do
|
||||
is_expected.to eq '<p>@alice</p>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with custom_emojify option' do
|
||||
let!(:emoji) { Fabricate(:custom_emoji) }
|
||||
|
||||
before { account.note = text }
|
||||
subject { Formatter.instance.simplified_format(account, custom_emojify: true) }
|
||||
|
||||
context 'given a post with an emoji shortcode at the start' do
|
||||
let(:text) { ':coolcat: Beep boop' }
|
||||
|
||||
it 'converts the shortcode to an image tag' do
|
||||
is_expected.to match(/<p><img draggable="false" class="emojione" alt=":coolcat:"/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with an emoji shortcode in the middle' do
|
||||
let(:text) { 'Beep :coolcat: boop' }
|
||||
|
||||
it 'converts the shortcode to an image tag' do
|
||||
is_expected.to match(/Beep <img draggable="false" class="emojione" alt=":coolcat:"/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with concatenated emoji shortcodes' do
|
||||
let(:text) { ':coolcat::coolcat:' }
|
||||
|
||||
it 'does not touch the shortcodes' do
|
||||
is_expected.to match(/:coolcat::coolcat:/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with an emoji shortcode at the end' do
|
||||
let(:text) { 'Beep boop :coolcat:' }
|
||||
|
||||
it 'converts the shortcode to an image tag' do
|
||||
is_expected.to match(/boop <img draggable="false" class="emojione" alt=":coolcat:"/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include_examples 'encode and link URLs'
|
||||
end
|
||||
|
||||
context 'given a post with remote status' do
|
||||
let(:text) { '<script>alert("Hello")</script>' }
|
||||
let(:account) { Fabricate(:account, domain: 'remote', note: text) }
|
||||
|
||||
it 'reformats' do
|
||||
is_expected.to_not include '<script>alert("Hello")</script>'
|
||||
end
|
||||
|
||||
context 'with custom_emojify option' do
|
||||
let!(:emoji) { Fabricate(:custom_emoji, domain: remote_account.domain) }
|
||||
|
||||
before { remote_account.note = text }
|
||||
|
||||
subject { Formatter.instance.simplified_format(remote_account, custom_emojify: true) }
|
||||
|
||||
context 'given a post with an emoji shortcode at the start' do
|
||||
let(:text) { '<p>:coolcat: Beep boop<br />' }
|
||||
|
||||
it 'converts shortcode to image tag' do
|
||||
is_expected.to match(/<p><img draggable="false" class="emojione" alt=":coolcat:"/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with an emoji shortcode in the middle' do
|
||||
let(:text) { '<p>Beep :coolcat: boop</p>' }
|
||||
|
||||
it 'converts shortcode to image tag' do
|
||||
is_expected.to match(/Beep <img draggable="false" class="emojione" alt=":coolcat:"/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with concatenated emoji shortcodes' do
|
||||
let(:text) { '<p>:coolcat::coolcat:</p>' }
|
||||
|
||||
it 'does not touch the shortcodes' do
|
||||
is_expected.to match(/<p>:coolcat::coolcat:<\/p>/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a post with an emoji shortcode at the end' do
|
||||
let(:text) { '<p>Beep boop<br />:coolcat:</p>' }
|
||||
|
||||
it 'converts shortcode to image tag' do
|
||||
is_expected.to match(/<br><img draggable="false" class="emojione" alt=":coolcat:"/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sanitize' do
|
||||
let(:html) { '<script>alert("Hello")</script>' }
|
||||
|
||||
subject { Formatter.instance.sanitize(html, Sanitize::Config::GABSOCIAL_STRICT) }
|
||||
|
||||
it 'sanitizes' do
|
||||
is_expected.to eq ''
|
||||
end
|
||||
end
|
||||
end
|
||||
9
spec/lib/hash_object_spec.rb
Normal file
9
spec/lib/hash_object_spec.rb
Normal file
@@ -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
|
||||
134
spec/lib/language_detector_spec.rb
Normal file
134
spec/lib/language_detector_spec.rb
Normal file
@@ -0,0 +1,134 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe LanguageDetector do
|
||||
describe 'prepare_text' do
|
||||
it 'returns unmodified string without special cases' do
|
||||
string = 'just a regular string'
|
||||
result = described_class.instance.send(:prepare_text, string)
|
||||
|
||||
expect(result).to eq string
|
||||
end
|
||||
|
||||
it 'collapses spacing in strings' do
|
||||
string = 'The formatting in this is very odd'
|
||||
|
||||
result = described_class.instance.send(:prepare_text, string)
|
||||
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.instance.send(:prepare_text, string)
|
||||
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.instance.send(:prepare_text, string)
|
||||
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.instance.send(:prepare_text, string)
|
||||
expect(result).to eq 'Hey look at all the and'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'detect' do
|
||||
let(:account_without_user_locale) { Fabricate(:user, locale: nil).account }
|
||||
let(:account_remote) { Fabricate(:account, domain: 'gab.com') }
|
||||
|
||||
it 'detects english language for basic strings' do
|
||||
strings = [
|
||||
"Hello and welcome to Gab Social 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",
|
||||
]
|
||||
strings.each do |string|
|
||||
result = described_class.instance.detect(string, account_without_user_locale)
|
||||
|
||||
expect(result).to eq(:en), string
|
||||
end
|
||||
end
|
||||
|
||||
it 'detects spanish language' do
|
||||
string = 'Obtener un Hola y bienvenidos a Gab Social. Obtener un Hola y bienvenidos a Gab Social. Obtener un Hola y bienvenidos a Gab Social. Obtener un Hola y bienvenidos a Gab Social'
|
||||
result = described_class.instance.detect(string, account_without_user_locale)
|
||||
|
||||
expect(result).to eq :es
|
||||
end
|
||||
|
||||
describe 'when language can\'t be detected' do
|
||||
it 'uses nil when sent an empty document' do
|
||||
result = described_class.instance.detect('', account_without_user_locale)
|
||||
expect(result).to eq nil
|
||||
end
|
||||
|
||||
describe 'because of 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.instance.detect(string, account_without_user_locale)
|
||||
|
||||
expect(result).to eq nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with an account' do
|
||||
it 'uses the account locale when present' do
|
||||
account = double(user_locale: 'fr')
|
||||
result = described_class.instance.detect('', account)
|
||||
|
||||
expect(result).to eq nil
|
||||
end
|
||||
|
||||
it 'uses nil when account is present but has no locale' do
|
||||
result = described_class.instance.detect('', account_without_user_locale)
|
||||
|
||||
expect(result).to eq nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with an `en` default locale' do
|
||||
it 'uses nil for undetectable string' do
|
||||
result = described_class.instance.detect('', account_without_user_locale)
|
||||
|
||||
expect(result).to eq nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remote user' do
|
||||
it 'detects Korean language' do
|
||||
string = '안녕하세요'
|
||||
result = described_class.instance.detect(string, account_remote)
|
||||
|
||||
expect(result).to eq :ko
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with a non-`en` default locale' do
|
||||
around(:each) do |example|
|
||||
before = I18n.default_locale
|
||||
I18n.default_locale = :ja
|
||||
example.run
|
||||
I18n.default_locale = before
|
||||
end
|
||||
|
||||
it 'uses nil for undetectable string' do
|
||||
string = ''
|
||||
result = described_class.instance.detect(string, account_without_user_locale)
|
||||
|
||||
expect(result).to eq nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
1560
spec/lib/ostatus/atom_serializer_spec.rb
Normal file
1560
spec/lib/ostatus/atom_serializer_spec.rb
Normal file
File diff suppressed because it is too large
Load Diff
70
spec/lib/ostatus/tag_manager_spec.rb
Normal file
70
spec/lib/ostatus/tag_manager_spec.rb
Normal file
@@ -0,0 +1,70 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe OStatus::TagManager do
|
||||
describe '#unique_tag' do
|
||||
it 'returns a unique tag' do
|
||||
expect(OStatus::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(OStatus::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(OStatus::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(OStatus::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(OStatus::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(OStatus::TagManager.instance.local_id?('tag:cb6e6126.ngrok.io;objectId=12:objectType=Status')).to be true
|
||||
end
|
||||
|
||||
it 'returns false for a foreign ID' do
|
||||
expect(OStatus::TagManager.instance.local_id?('tag:foreign.tld;objectId=12:objectType=Status')).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#uri_for' do
|
||||
subject { OStatus::TagManager.instance.uri_for(target) }
|
||||
|
||||
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 target.uri
|
||||
end
|
||||
end
|
||||
|
||||
context 'note object' do
|
||||
let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: false, thread: nil) }
|
||||
|
||||
it 'returns the unique tag for status' do
|
||||
expect(target.object_type).to eq :note
|
||||
is_expected.to eq target.uri
|
||||
end
|
||||
end
|
||||
|
||||
context 'person object' do
|
||||
let(:target) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
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
|
||||
end
|
||||
82
spec/lib/proof_provider/keybase/verifier_spec.rb
Normal file
82
spec/lib/proof_provider/keybase/verifier_spec.rb
Normal file
@@ -0,0 +1,82 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ProofProvider::Keybase::Verifier do
|
||||
let(:my_domain) { Rails.configuration.x.local_domain }
|
||||
|
||||
let(:keybase_proof) do
|
||||
local_proof = AccountIdentityProof.new(
|
||||
provider: 'Keybase',
|
||||
provider_username: 'cryptoalice',
|
||||
token: '11111111111111111111111111'
|
||||
)
|
||||
|
||||
described_class.new('alice', 'cryptoalice', '11111111111111111111111111', my_domain)
|
||||
end
|
||||
|
||||
let(:query_params) do
|
||||
"domain=#{my_domain}&kb_username=cryptoalice&sig_hash=11111111111111111111111111&username=alice"
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
let(:base_url) { 'https://keybase.io/_/api/1.0/sig/proof_valid.json' }
|
||||
|
||||
context 'when valid' do
|
||||
before do
|
||||
json_response_body = '{"status":{"code":0,"name":"OK"},"proof_valid":true}'
|
||||
stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body)
|
||||
end
|
||||
|
||||
it 'calls out to keybase and returns true' do
|
||||
expect(keybase_proof.valid?).to eq true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invalid' do
|
||||
before do
|
||||
json_response_body = '{"status":{"code":0,"name":"OK"},"proof_valid":false}'
|
||||
stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body)
|
||||
end
|
||||
|
||||
it 'calls out to keybase and returns false' do
|
||||
expect(keybase_proof.valid?).to eq false
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unexpected api response' do
|
||||
before do
|
||||
json_response_body = '{"status":{"code":100,"desc":"wrong size hex_id","fields":{"sig_hash":"wrong size hex_id"},"name":"INPUT_ERROR"}}'
|
||||
stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body)
|
||||
end
|
||||
|
||||
it 'swallows the error and returns false' do
|
||||
expect(keybase_proof.valid?).to eq false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#status' do
|
||||
let(:base_url) { 'https://keybase.io/_/api/1.0/sig/proof_live.json' }
|
||||
|
||||
context 'with a normal response' do
|
||||
before do
|
||||
json_response_body = '{"status":{"code":0,"name":"OK"},"proof_live":false,"proof_valid":true}'
|
||||
stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body)
|
||||
end
|
||||
|
||||
it 'calls out to keybase and returns the status fields as proof_valid and proof_live' do
|
||||
expect(keybase_proof.status).to include({ 'proof_valid' => true, 'proof_live' => false })
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unexpected keybase response' do
|
||||
before do
|
||||
json_response_body = '{"status":{"code":100,"desc":"missing non-optional field sig_hash","fields":{"sig_hash":"missing non-optional field sig_hash"},"name":"INPUT_ERROR"}}'
|
||||
stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body)
|
||||
end
|
||||
|
||||
it 'raises a ProofProvider::Keybase::UnexpectedResponseError' do
|
||||
expect { keybase_proof.status }.to raise_error ProofProvider::Keybase::UnexpectedResponseError
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
138
spec/lib/request_spec.rb
Normal file
138
spec/lib/request_spec.rb
Normal file
@@ -0,0 +1,138 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
require 'securerandom'
|
||||
|
||||
describe Request do
|
||||
subject { Request.new(:get, 'http://example.com') }
|
||||
|
||||
describe '#headers' do
|
||||
it 'returns user agent' do
|
||||
expect(subject.headers['User-Agent']).to be_present
|
||||
end
|
||||
|
||||
it 'returns the date header' do
|
||||
expect(subject.headers['Date']).to be_present
|
||||
end
|
||||
|
||||
it 'returns the host header' do
|
||||
expect(subject.headers['Host']).to be_present
|
||||
end
|
||||
|
||||
it 'does not return virtual request-target header' do
|
||||
expect(subject.headers['(request-target)']).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#on_behalf_of' do
|
||||
it 'when used, adds signature header' do
|
||||
subject.on_behalf_of(Fabricate(:account))
|
||||
expect(subject.headers['Signature']).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe '#add_headers' do
|
||||
it 'adds headers to the request' do
|
||||
subject.add_headers('Test' => 'Foo')
|
||||
expect(subject.headers['Test']).to eq 'Foo'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'with valid host' do
|
||||
before { stub_request(:get, 'http://example.com') }
|
||||
|
||||
it 'executes a HTTP request' do
|
||||
expect { |block| subject.perform &block }.to yield_control
|
||||
expect(a_request(:get, 'http://example.com')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'executes a HTTP request when the first address is private' do
|
||||
resolver = double
|
||||
|
||||
allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:4860:4860::8844))
|
||||
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||
|
||||
expect { |block| subject.perform &block }.to yield_control
|
||||
expect(a_request(:get, 'http://example.com')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'sets headers' do
|
||||
expect { |block| subject.perform &block }.to yield_control
|
||||
expect(a_request(:get, 'http://example.com').with(headers: subject.headers)).to have_been_made
|
||||
end
|
||||
|
||||
it 'closes underlaying connection' do
|
||||
expect_any_instance_of(HTTP::Client).to receive(:close)
|
||||
expect { |block| subject.perform &block }.to yield_control
|
||||
end
|
||||
|
||||
it 'returns response which implements body_with_limit' do
|
||||
subject.perform do |response|
|
||||
expect(response).to respond_to :body_with_limit
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with private host' do
|
||||
around do |example|
|
||||
WebMock.disable!
|
||||
example.run
|
||||
WebMock.enable!
|
||||
end
|
||||
|
||||
it 'raises GabSocial::ValidationError' do
|
||||
resolver = double
|
||||
|
||||
allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:db8::face))
|
||||
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||
|
||||
expect { subject.perform }.to raise_error GabSocial::ValidationError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "response's body_with_limit method" do
|
||||
it 'rejects body more than 1 megabyte by default' do
|
||||
stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes))
|
||||
expect { subject.perform { |response| response.body_with_limit } }.to raise_error GabSocial::LengthValidationError
|
||||
end
|
||||
|
||||
it 'accepts body less than 1 megabyte by default' do
|
||||
stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.kilobytes))
|
||||
expect { subject.perform { |response| response.body_with_limit } }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'rejects body by given size' do
|
||||
stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.kilobytes))
|
||||
expect { subject.perform { |response| response.body_with_limit(1.kilobyte) } }.to raise_error GabSocial::LengthValidationError
|
||||
end
|
||||
|
||||
it 'rejects too large chunked body' do
|
||||
stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes), headers: { 'Transfer-Encoding' => 'chunked' })
|
||||
expect { subject.perform { |response| response.body_with_limit } }.to raise_error GabSocial::LengthValidationError
|
||||
end
|
||||
|
||||
it 'rejects too large monolithic body' do
|
||||
stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes), headers: { 'Content-Length' => 2.megabytes })
|
||||
expect { subject.perform { |response| response.body_with_limit } }.to raise_error GabSocial::LengthValidationError
|
||||
end
|
||||
|
||||
it 'uses binary encoding if Content-Type does not tell encoding' do
|
||||
stub_request(:any, 'http://example.com').to_return(body: '', headers: { 'Content-Type' => 'text/html' })
|
||||
expect(subject.perform { |response| response.body_with_limit.encoding }).to eq Encoding::BINARY
|
||||
end
|
||||
|
||||
it 'uses binary encoding if Content-Type tells unknown encoding' do
|
||||
stub_request(:any, 'http://example.com').to_return(body: '', headers: { 'Content-Type' => 'text/html; charset=unknown' })
|
||||
expect(subject.perform { |response| response.body_with_limit.encoding }).to eq Encoding::BINARY
|
||||
end
|
||||
|
||||
it 'uses encoding specified by Content-Type' do
|
||||
stub_request(:any, 'http://example.com').to_return(body: '', headers: { 'Content-Type' => 'text/html; charset=UTF-8' })
|
||||
expect(subject.perform { |response| response.body_with_limit.encoding }).to eq Encoding::UTF_8
|
||||
end
|
||||
end
|
||||
end
|
||||
16
spec/lib/settings/extend_spec.rb
Normal file
16
spec/lib/settings/extend_spec.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Settings::Extend do
|
||||
class User
|
||||
include Settings::Extend
|
||||
end
|
||||
|
||||
describe '#settings' do
|
||||
it 'sets @settings as an instance of Settings::ScopedSettings' do
|
||||
user = Fabricate(:user)
|
||||
expect(user.settings).to be_kind_of Settings::ScopedSettings
|
||||
end
|
||||
end
|
||||
end
|
||||
35
spec/lib/settings/scoped_settings_spec.rb
Normal file
35
spec/lib/settings/scoped_settings_spec.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Settings::ScopedSettings do
|
||||
let(:object) { Fabricate(:user) }
|
||||
let(:scoped_setting) { described_class.new(object) }
|
||||
let(:val) { 'whatever' }
|
||||
let(:methods) { %i(auto_play_gif default_sensitive unfollow_modal boost_modal delete_modal reduce_motion system_font_ui noindex theme) }
|
||||
|
||||
describe '.initialize' do
|
||||
it 'sets @object' do
|
||||
scoped_setting = described_class.new(object)
|
||||
expect(scoped_setting.instance_variable_get(:@object)).to be object
|
||||
end
|
||||
end
|
||||
|
||||
describe '#method_missing' do
|
||||
it 'sets scoped_setting.method_name = val' do
|
||||
methods.each do |key|
|
||||
scoped_setting.send("#{key}=", val)
|
||||
expect(scoped_setting.send(key)).to eq val
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#[]= and #[]' do
|
||||
it 'sets [key] = val' do
|
||||
methods.each do |key|
|
||||
scoped_setting[key] = val
|
||||
expect(scoped_setting[key]).to eq val
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
83
spec/lib/status_filter_spec.rb
Normal file
83
spec/lib/status_filter_spec.rb
Normal file
@@ -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.silence!
|
||||
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.silence!
|
||||
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
|
||||
62
spec/lib/status_finder_spec.rb
Normal file
62
spec/lib/status_finder_spec.rb
Normal file
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe StatusFinder do
|
||||
include RoutingHelper
|
||||
|
||||
describe '#status' do
|
||||
subject { described_class.new(url) }
|
||||
|
||||
context 'with a status url' do
|
||||
let(:status) { Fabricate(:status) }
|
||||
let(:url) { short_account_status_url(account_username: status.account.username, id: status.id) }
|
||||
|
||||
it 'finds the stream entry' do
|
||||
expect(subject.status).to eq(status)
|
||||
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.status }.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) }
|
||||
|
||||
it 'finds the stream entry' do
|
||||
expect(subject.status).to eq(stream_entry.status)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a remote url even if id exists on local' do
|
||||
let(:status) { Fabricate(:status) }
|
||||
let(:url) { "https://example.com/users/test/statuses/#{status.id}" }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a plausible url' do
|
||||
let(:url) { 'https://example.com/users/test/updates/123/embed' }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unrecognized url' do
|
||||
let(:url) { 'https://example.com/about' }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
164
spec/lib/tag_manager_spec.rb
Normal file
164
spec/lib/tag_manager_spec.rb
Normal file
@@ -0,0 +1,164 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe TagManager do
|
||||
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.test'
|
||||
|
||||
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.Test/')).to eq true
|
||||
end
|
||||
|
||||
it 'returns false for irrelevant string' do
|
||||
expect(TagManager.instance.local_domain?('DoMaIn.Test!')).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.test'
|
||||
|
||||
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.Test/')).to eq true
|
||||
end
|
||||
|
||||
it 'returns false for string with irrelevant characters' do
|
||||
expect(TagManager.instance.web_domain?('DoMaIn.Test!')).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.Test/')).to eq 'domain.test'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#local_url?' do
|
||||
around do |example|
|
||||
original_web_domain = Rails.configuration.x.web_domain
|
||||
example.run
|
||||
Rails.configuration.x.web_domain = original_web_domain
|
||||
end
|
||||
|
||||
it 'returns true if the normalized string with port is local URL' do
|
||||
Rails.configuration.x.web_domain = 'domain.test:42'
|
||||
expect(TagManager.instance.local_url?('https://DoMaIn.Test:42/')).to eq true
|
||||
end
|
||||
|
||||
it 'returns true if the normalized string without port is local URL' do
|
||||
Rails.configuration.x.web_domain = 'domain.test'
|
||||
expect(TagManager.instance.local_url?('https://DoMaIn.Test/')).to eq true
|
||||
end
|
||||
|
||||
it 'returns false for string with irrelevant characters' do
|
||||
Rails.configuration.x.web_domain = 'domain.test'
|
||||
expect(TagManager.instance.local_url?('https://domainn.test/')).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.test', 'UsErNaMe@DoMaIn.Test')).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.test', '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.test', 'UsErNaMe@incorrect.test')).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.test', 'incorrect@DoMaIn.test')).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 '#url_for' do
|
||||
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
subject { TagManager.instance.url_for(target) }
|
||||
|
||||
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 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 '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 the URL for account' do
|
||||
expect(target.object_type).to eq :person
|
||||
is_expected.to eq 'https://cb6e6126.ngrok.io/@alice'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
84
spec/lib/user_settings_decorator_spec.rb
Normal file
84
spec/lib/user_settings_decorator_spec.rb
Normal file
@@ -0,0 +1,84 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe UserSettingsDecorator do
|
||||
describe 'update' do
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:settings) { described_class.new(user) }
|
||||
|
||||
it 'updates the user settings value for email notifications' do
|
||||
values = { 'notification_emails' => { 'follow' => '1' } }
|
||||
|
||||
settings.update(values)
|
||||
expect(user.settings['notification_emails']['follow']).to eq true
|
||||
end
|
||||
|
||||
it 'updates the user settings value for interactions' do
|
||||
values = { 'interactions' => { 'must_be_follower' => '0' } }
|
||||
|
||||
settings.update(values)
|
||||
expect(user.settings['interactions']['must_be_follower']).to eq false
|
||||
end
|
||||
|
||||
it 'updates the user settings value for privacy' do
|
||||
values = { 'setting_default_privacy' => 'public' }
|
||||
|
||||
settings.update(values)
|
||||
expect(user.settings['default_privacy']).to eq 'public'
|
||||
end
|
||||
|
||||
it 'updates the user settings value for sensitive' do
|
||||
values = { 'setting_default_sensitive' => '1' }
|
||||
|
||||
settings.update(values)
|
||||
expect(user.settings['default_sensitive']).to eq true
|
||||
end
|
||||
|
||||
it 'updates the user settings value for unfollow modal' do
|
||||
values = { 'setting_unfollow_modal' => '0' }
|
||||
|
||||
settings.update(values)
|
||||
expect(user.settings['unfollow_modal']).to eq false
|
||||
end
|
||||
|
||||
it 'updates the user settings value for repost modal' do
|
||||
values = { 'setting_boost_modal' => '1' }
|
||||
|
||||
settings.update(values)
|
||||
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' }
|
||||
|
||||
settings.update(values)
|
||||
expect(user.settings['auto_play_gif']).to eq false
|
||||
end
|
||||
|
||||
it 'updates the user settings value for system font in UI' do
|
||||
values = { 'setting_system_font_ui' => '0' }
|
||||
|
||||
settings.update(values)
|
||||
expect(user.settings['system_font_ui']).to eq false
|
||||
end
|
||||
|
||||
it 'decoerces setting values before applying' do
|
||||
values = {
|
||||
'setting_delete_modal' => 'false',
|
||||
'setting_boost_modal' => 'true',
|
||||
}
|
||||
|
||||
settings.update(values)
|
||||
expect(user.settings['delete_modal']).to eq false
|
||||
expect(user.settings['boost_modal']).to eq true
|
||||
end
|
||||
end
|
||||
end
|
||||
127
spec/lib/webfinger_resource_spec.rb
Normal file
127
spec/lib/webfinger_resource_spec.rb
Normal file
@@ -0,0 +1,127 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe WebfingerResource do
|
||||
around do |example|
|
||||
before_local = Rails.configuration.x.local_domain
|
||||
before_web = Rails.configuration.x.web_domain
|
||||
example.run
|
||||
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 a route whose controller is not AccountsController' do
|
||||
resource = 'https://example.com/users/alice/other'
|
||||
|
||||
expect {
|
||||
WebfingerResource.new(resource).username
|
||||
}.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'
|
||||
|
||||
expect {
|
||||
WebfingerResource.new(resource).username
|
||||
}.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'finds the username in a valid https route' do
|
||||
resource = 'https://example.com/users/alice'
|
||||
|
||||
result = WebfingerResource.new(resource).username
|
||||
expect(result).to eq 'alice'
|
||||
end
|
||||
|
||||
it 'finds the username in a mixed case http route' do
|
||||
resource = 'HTTp://exAMPLEe.com/users/alice'
|
||||
|
||||
result = WebfingerResource.new(resource).username
|
||||
expect(result).to eq 'alice'
|
||||
end
|
||||
|
||||
it 'finds the username in a valid http route' do
|
||||
resource = 'http://example.com/users/alice'
|
||||
|
||||
result = WebfingerResource.new(resource).username
|
||||
expect(result).to eq 'alice'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with a username and hostname value' do
|
||||
it 'raises on a non-local domain' do
|
||||
resource = 'user@remote-host.com'
|
||||
|
||||
expect {
|
||||
WebfingerResource.new(resource).username
|
||||
}.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'finds username for a local domain' do
|
||||
Rails.configuration.x.local_domain = 'example.com'
|
||||
resource = 'alice@example.com'
|
||||
|
||||
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
|
||||
it 'raises on a non-local domain' do
|
||||
resource = 'acct:user@remote-host.com'
|
||||
|
||||
expect {
|
||||
WebfingerResource.new(resource).username
|
||||
}.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'raises on a nonsense domain' do
|
||||
resource = 'acct:user@remote-host@remote-hostess.remote.local@remote'
|
||||
|
||||
expect {
|
||||
WebfingerResource.new(resource).username
|
||||
}.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user