mirror of
https://github.com/discourse/discourse.git
synced 2026-08-12 03:37:13 +08:00
Currently the hosts that can be used for a livestream event are hardcoded in the composer's JavaScript, and the server accepts any https location, so the list isn't really enforced... and there's no way to use hosts that would otherwise work, including custom or self-hosted sources. This adds a `livestream_allowed_hosts` site setting (defaulting to the existing hardcoded list) and validates against it both in the composer and on the server. Whether a livestream embeds a player still depends on the site's onebox settings. --------- Co-authored-by: Martin Brennan <martin@discourse.org>
1531 lines
48 KiB
Ruby
Vendored
1531 lines
48 KiB
Ruby
Vendored
# frozen_string_literal: true
|
||
|
||
describe DiscoursePostEvent::Event do
|
||
before do
|
||
freeze_time DateTime.parse("2020-04-24 14:10")
|
||
Jobs.run_immediately!
|
||
SiteSetting.calendar_enabled = true
|
||
SiteSetting.discourse_post_event_enabled = true
|
||
end
|
||
|
||
it do
|
||
is_expected.to validate_length_of(:description).is_at_most(
|
||
DiscoursePostEvent::Event::MAX_DESCRIPTION_LENGTH,
|
||
)
|
||
end
|
||
|
||
it do
|
||
is_expected.to validate_length_of(:name).is_at_least(
|
||
DiscoursePostEvent::Event::MIN_NAME_LENGTH,
|
||
).is_at_most(DiscoursePostEvent::Event::MAX_NAME_LENGTH)
|
||
end
|
||
|
||
describe "#warm_livestream_onebox" do
|
||
let(:livestream_url) { "https://www.youtube.com/live/abc123" }
|
||
|
||
fab!(:topic) { Fabricate(:topic, category: nil) }
|
||
fab!(:post) { Fabricate(:post, topic: topic) }
|
||
|
||
before { Jobs.run_later! }
|
||
|
||
it "enqueues onebox warming for a livestream URL" do
|
||
expect_enqueued_with(
|
||
job: :warm_livestream_onebox,
|
||
args: {
|
||
event_id: post.id,
|
||
url: livestream_url,
|
||
},
|
||
) { Fabricate(:event, post: post, livestream: true, location: livestream_url) }
|
||
end
|
||
|
||
it "skips onebox warming when the onebox is cached" do
|
||
Discourse.cache.write(
|
||
Oneboxer.onebox_cache_key(livestream_url),
|
||
{ onebox: "<aside>cached</aside>" },
|
||
)
|
||
|
||
expect_not_enqueued_with(job: :warm_livestream_onebox) do
|
||
Fabricate(:event, post: post, livestream: true, location: livestream_url)
|
||
end
|
||
end
|
||
|
||
it "skips onebox warming for events that are not livestreams" do
|
||
expect_not_enqueued_with(job: :warm_livestream_onebox) do
|
||
Fabricate(:event, post: post, livestream: false, location: "Room 5")
|
||
end
|
||
end
|
||
|
||
it "skips onebox warming when the livestream has no URL" do
|
||
expect_not_enqueued_with(job: :warm_livestream_onebox) do
|
||
Fabricate(:event, post: post, livestream: true)
|
||
end
|
||
end
|
||
|
||
it "warms the onebox when the URL changes" do
|
||
event = Fabricate(:event, post: post, livestream: true, location: livestream_url)
|
||
|
||
expect_enqueued_with(
|
||
job: :warm_livestream_onebox,
|
||
args: {
|
||
event_id: event.id,
|
||
url: "https://www.youtube.com/live/def456",
|
||
},
|
||
) { event.update!(location: "https://www.youtube.com/live/def456") }
|
||
end
|
||
|
||
it "does not warm the onebox when an unrelated attribute changes" do
|
||
event = Fabricate(:event, post: post, livestream: true, location: livestream_url)
|
||
|
||
expect_not_enqueued_with(job: :warm_livestream_onebox) { event.update!(name: "Renamed") }
|
||
end
|
||
end
|
||
|
||
describe "#livestream_url" do
|
||
fab!(:topic) { Fabricate(:topic, category: nil) }
|
||
fab!(:post) { Fabricate(:post, topic: topic) }
|
||
|
||
it "prefers the location over the url" do
|
||
event =
|
||
Fabricate(
|
||
:event,
|
||
post: post,
|
||
location: "https://zoom.us/j/123456789",
|
||
url: "https://example.com/fallback",
|
||
)
|
||
|
||
expect(event.livestream_url).to eq("https://zoom.us/j/123456789")
|
||
end
|
||
|
||
it "falls back to the url when there is no location" do
|
||
event = Fabricate(:event, post: post, url: "https://example.com/fallback")
|
||
|
||
expect(event.livestream_url).to eq("https://example.com/fallback")
|
||
end
|
||
|
||
it "is nil when neither is set" do
|
||
event = Fabricate(:event, post: post)
|
||
|
||
expect(event.livestream_url).to be_nil
|
||
end
|
||
end
|
||
|
||
describe "#is_zoom_livestream?" do
|
||
fab!(:topic) { Fabricate(:topic, category: nil) }
|
||
fab!(:post) { Fabricate(:post, topic: topic) }
|
||
|
||
before do
|
||
SiteSetting.livestream_zoom_enabled = true
|
||
# Creating a livestream event enqueues onebox warming, which would
|
||
# otherwise run inline and try to fetch the URL.
|
||
Jobs.run_later!
|
||
end
|
||
|
||
def event_for(location)
|
||
Fabricate(:event, post: post, livestream: true, location: location)
|
||
end
|
||
|
||
it "recognises a Zoom URL on a vanity subdomain" do
|
||
expect(event_for("https://us06web.zoom.us/j/123456789?pwd=secret")).to be_is_zoom_livestream
|
||
end
|
||
|
||
it "recognises a Zoom URL on the bare host" do
|
||
expect(event_for("https://zoom.us/j/123456789")).to be_is_zoom_livestream
|
||
end
|
||
|
||
it "recognises a Zoom webinar URL" do
|
||
expect(event_for("https://zoom.us/w/123456789")).to be_is_zoom_livestream
|
||
end
|
||
|
||
it "rejects a host that only ends in the Zoom domain" do
|
||
expect(event_for("https://notzoom.us/j/123456789")).not_to be_is_zoom_livestream
|
||
end
|
||
|
||
it "rejects a Zoom URL with no joinable meeting number" do
|
||
expect(event_for("https://zoom.us/about")).not_to be_is_zoom_livestream
|
||
end
|
||
|
||
it "rejects a non-Zoom livestream URL" do
|
||
expect(event_for("https://www.youtube.com/live/abc123")).not_to be_is_zoom_livestream
|
||
end
|
||
|
||
it "is false when the event is not a livestream" do
|
||
event = Fabricate(:event, post: post, livestream: false, url: "https://zoom.us/j/123456789")
|
||
|
||
expect(event).not_to be_is_zoom_livestream
|
||
end
|
||
|
||
it "is false when Zoom is disabled" do
|
||
SiteSetting.livestream_zoom_enabled = false
|
||
|
||
expect(event_for("https://zoom.us/j/123456789")).not_to be_is_zoom_livestream
|
||
end
|
||
end
|
||
|
||
describe "#create_livestream_chat_channel" do
|
||
fab!(:category)
|
||
fab!(:topic) { Fabricate(:topic, category: category) }
|
||
fab!(:post) { Fabricate(:post, topic: topic) }
|
||
|
||
before do
|
||
SiteSetting.chat_enabled = true
|
||
Jobs.run_later!
|
||
end
|
||
|
||
it "creates the chat channel after the livestream event is committed" do
|
||
expect {
|
||
Fabricate(
|
||
:event,
|
||
post: post,
|
||
livestream: true,
|
||
location: "https://www.youtube.com/live/abc123",
|
||
)
|
||
}.to change(DiscourseCalendar::Livestream::TopicChatChannel, :count).by(1)
|
||
|
||
expect(post.topic.topic_chat_channel.chat_channel.chatable).to eq(category)
|
||
end
|
||
|
||
it "does not create a chat channel when chat is disabled" do
|
||
SiteSetting.chat_enabled = false
|
||
|
||
expect {
|
||
Fabricate(
|
||
:event,
|
||
post: post,
|
||
livestream: true,
|
||
location: "https://www.youtube.com/live/abc123",
|
||
)
|
||
}.not_to change(DiscourseCalendar::Livestream::TopicChatChannel, :count)
|
||
end
|
||
|
||
it "does not create a chat channel for a livestream event on a reply" do
|
||
reply = Fabricate(:post, topic: topic)
|
||
|
||
expect {
|
||
Fabricate(
|
||
:event,
|
||
post: reply,
|
||
livestream: true,
|
||
location: "https://www.youtube.com/live/abc123",
|
||
)
|
||
}.not_to change(DiscourseCalendar::Livestream::TopicChatChannel, :count)
|
||
end
|
||
end
|
||
|
||
describe "#reset_invalid_livestream" do
|
||
fab!(:post)
|
||
|
||
# enqueue (don't run) the onebox-warming job so it doesn't make a real request
|
||
before { Jobs.run_later! }
|
||
|
||
it "keeps livestream enabled for an allowed host" do
|
||
event =
|
||
Fabricate(
|
||
:event,
|
||
post: post,
|
||
livestream: true,
|
||
location: "https://www.youtube.com/live/abc123",
|
||
)
|
||
|
||
expect(event.reload.livestream).to eq(true)
|
||
end
|
||
|
||
it "keeps livestream enabled for a host an admin added" do
|
||
SiteSetting.livestream_allowed_hosts = "stream.example.com"
|
||
|
||
event =
|
||
Fabricate(:event, post: post, livestream: true, location: "https://stream.example.com/live")
|
||
|
||
expect(event.reload.livestream).to eq(true)
|
||
end
|
||
|
||
it "resets livestream when the location is not an https URL" do
|
||
event = Fabricate(:event, post: post, livestream: true, location: "Room 5")
|
||
|
||
expect(event.reload.livestream).to eq(false)
|
||
|
||
event.update!(location: "http://www.youtube.com/live/abc123")
|
||
|
||
expect(event.reload.livestream).to eq(false)
|
||
end
|
||
|
||
it "falls back to the url, which is what the onebox and serializers use" do
|
||
event =
|
||
Fabricate(
|
||
:event,
|
||
post: post,
|
||
livestream: true,
|
||
location: nil,
|
||
url: "https://www.youtube.com/live/abc123",
|
||
)
|
||
|
||
expect(event.reload.livestream).to eq(true)
|
||
|
||
event.update!(url: "https://example.com/live")
|
||
|
||
expect(event.reload.livestream).to eq(false)
|
||
end
|
||
|
||
it "resets livestream when the location host is not allowed" do
|
||
event = Fabricate(:event, post: post, livestream: true, location: "https://example.com/live")
|
||
|
||
expect(event.reload.livestream).to eq(false)
|
||
end
|
||
|
||
it "resets livestream when the location only resembles an allowed host" do
|
||
event =
|
||
Fabricate(:event, post: post, livestream: true, location: "https://youtube.com.evil.com/x")
|
||
|
||
expect(event.reload.livestream).to eq(false)
|
||
end
|
||
|
||
it "resets livestream when an admin removes the host from the allowlist" do
|
||
event =
|
||
Fabricate(
|
||
:event,
|
||
post: post,
|
||
livestream: true,
|
||
location: "https://www.youtube.com/live/abc123",
|
||
)
|
||
SiteSetting.livestream_allowed_hosts = "zoom.us"
|
||
|
||
event.update!(name: "Renamed")
|
||
|
||
expect(event.reload.livestream).to eq(false)
|
||
end
|
||
|
||
it "resets livestream when there is no location" do
|
||
event = Fabricate(:event, post: post, livestream: true)
|
||
|
||
expect(event.reload.livestream).to eq(false)
|
||
end
|
||
|
||
it "resets livestream when the location is edited away from a URL" do
|
||
event =
|
||
Fabricate(
|
||
:event,
|
||
post: post,
|
||
livestream: true,
|
||
location: "https://www.youtube.com/live/abc123",
|
||
)
|
||
event.update!(location: "Room 5")
|
||
|
||
expect(event.reload.livestream).to eq(false)
|
||
end
|
||
|
||
it "resets livestream when the event is not on the first post" do
|
||
reply = Fabricate(:post, topic: post.topic)
|
||
expect(reply.is_first_post?).to be(false)
|
||
|
||
event =
|
||
Fabricate(
|
||
:event,
|
||
post: reply,
|
||
livestream: true,
|
||
location: "https://www.youtube.com/live/abc123",
|
||
)
|
||
|
||
expect(event.reload.livestream).to eq(false)
|
||
end
|
||
end
|
||
|
||
describe "#raw_invitees_are_groups" do
|
||
fab!(:user) { Fabricate(:user, admin: true, refresh_auto_groups: true) }
|
||
fab!(:topic) { Fabricate(:topic, user: user) }
|
||
fab!(:post) { Fabricate(:post, topic: topic) }
|
||
|
||
it "is invalid when an invitee matches a username that is not a group" do
|
||
Fabricate(:user, username: "not_a_group")
|
||
event =
|
||
Fabricate.build(
|
||
:event,
|
||
post: post,
|
||
original_starts_at: Time.now,
|
||
raw_invitees: ["not_a_group"],
|
||
)
|
||
|
||
expect(event).not_to be_valid
|
||
expect(event.errors[:base]).to include(
|
||
I18n.t("discourse_post_event.errors.models.event.raw_invitees.only_group"),
|
||
)
|
||
end
|
||
|
||
it "is valid when an invitee is a group whose name collides with a username" do
|
||
Fabricate(:user).update_columns(
|
||
username: DiscoursePostEvent::Event::PUBLIC_GROUP,
|
||
username_lower: DiscoursePostEvent::Event::PUBLIC_GROUP,
|
||
)
|
||
event =
|
||
Fabricate.build(
|
||
:event,
|
||
post: post,
|
||
original_starts_at: Time.now,
|
||
raw_invitees: [DiscoursePostEvent::Event::PUBLIC_GROUP],
|
||
)
|
||
|
||
expect(event).to be_valid
|
||
end
|
||
end
|
||
|
||
describe "topic custom fields callback" do
|
||
let(:user) { Fabricate(:user, admin: true, refresh_auto_groups: true) }
|
||
let!(:notified_user) { Fabricate(:user) }
|
||
let(:topic) { Fabricate(:topic, user: user) }
|
||
let!(:first_post) { Fabricate(:post, topic: topic) }
|
||
let(:second_post) { Fabricate(:post, topic: topic) }
|
||
let!(:starts_at) { Time.zone.parse("2020-04-24 14:15:00") }
|
||
let!(:ends_at) { Time.zone.parse("2020-04-24 16:15:00") }
|
||
let!(:alt_starts_at) { Time.zone.parse("2020-04-24 14:14:25") }
|
||
let!(:alt_ends_at) { Time.zone.parse("2020-04-24 19:15:25") }
|
||
let(:event) do
|
||
DiscoursePostEvent::Event.create!(
|
||
id: first_post.id,
|
||
original_starts_at: Time.now + 1.hour,
|
||
original_ends_at: Time.now + 2.hours,
|
||
)
|
||
end
|
||
let(:late_event) do
|
||
DiscoursePostEvent::Event.create!(
|
||
id: first_post.id,
|
||
original_starts_at: Time.now - 10.hours,
|
||
original_ends_at: Time.now - 8.hours,
|
||
)
|
||
end
|
||
let(:first_post_starts_at) do
|
||
Time.zone.parse(
|
||
first_post.topic.custom_fields[DiscoursePostEvent::TOPIC_POST_EVENT_STARTS_AT],
|
||
)
|
||
end
|
||
let(:first_post_ends_at) do
|
||
Time.zone.parse(first_post.topic.custom_fields[DiscoursePostEvent::TOPIC_POST_EVENT_ENDS_AT])
|
||
end
|
||
|
||
describe "#after_commit[:create, :update]" do
|
||
context "when a post event has been created" do
|
||
context "when the associated post is the OP" do
|
||
it "sets the topic custom field and creates event date" do
|
||
expect(first_post.is_first_post?).to be(true)
|
||
expect(first_post.topic.custom_fields).to be_blank
|
||
|
||
expect {
|
||
DiscoursePostEvent::Event.create!(
|
||
id: first_post.id,
|
||
original_starts_at: starts_at,
|
||
original_ends_at: ends_at,
|
||
)
|
||
}.to change { DiscoursePostEvent::EventDate.count }
|
||
first_post.topic.reload
|
||
|
||
expect(first_post_starts_at).to eq_time(starts_at)
|
||
expect(first_post_ends_at).to eq_time(ends_at)
|
||
expect(DiscoursePostEvent::EventDate.last.starts_at).to eq_time(starts_at)
|
||
expect(DiscoursePostEvent::EventDate.last.ends_at).to eq_time(ends_at)
|
||
end
|
||
end
|
||
|
||
context "when the associated post is not the OP" do
|
||
it "doesn’t set the topic custom field but still creates event date" do
|
||
expect(second_post.is_first_post?).to be(false)
|
||
expect(second_post.topic.custom_fields).to be_blank
|
||
|
||
expect {
|
||
DiscoursePostEvent::Event.create!(id: second_post.id, original_starts_at: starts_at)
|
||
}.to change { DiscoursePostEvent::EventDate.count }
|
||
second_post.topic.reload
|
||
|
||
expect(second_post.topic.custom_fields).to be_blank
|
||
end
|
||
end
|
||
describe "notify an user" do
|
||
describe "before the event starts" do
|
||
it "does notify the user" do
|
||
expect { event.create_notification!(notified_user, first_post) }.to change {
|
||
Notification.count
|
||
}.by(1)
|
||
end
|
||
end
|
||
describe "after the event starts" do
|
||
it "doesn't notify the user" do
|
||
expect { late_event.create_notification!(notified_user, first_post) }.not_to change {
|
||
Notification.count
|
||
}
|
||
end
|
||
end
|
||
|
||
describe "with private message topics" do
|
||
let(:pm_owner) { Fabricate(:user) }
|
||
let(:allowed_user) { Fabricate(:user) }
|
||
let(:disallowed_user) { Fabricate(:user) }
|
||
let(:pm_topic) do
|
||
Fabricate(:private_message_topic, user: pm_owner, recipient: allowed_user)
|
||
end
|
||
let(:pm_post) { Fabricate(:post, topic: pm_topic, user: pm_owner) }
|
||
let(:pm_event) { Fabricate(:event, post: pm_post) }
|
||
|
||
it "does not send notifications to users without access to the PM" do
|
||
expect(Guardian.new(disallowed_user).can_see?(pm_topic)).to be(false)
|
||
expect { pm_event.create_notification!(disallowed_user, pm_post) }.not_to change {
|
||
Notification.count
|
||
}
|
||
end
|
||
|
||
it "does send notifications to users with access to the PM" do
|
||
expect(Guardian.new(allowed_user).can_see?(pm_topic)).to be(true)
|
||
expect { pm_event.create_notification!(allowed_user, pm_post) }.to change {
|
||
Notification.count
|
||
}.by(1)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when a post event has been updated" do
|
||
context "when the associated post is the OP" do
|
||
let!(:post_event) do
|
||
Fabricate(
|
||
:event,
|
||
post: first_post,
|
||
original_starts_at: starts_at,
|
||
original_ends_at: ends_at,
|
||
)
|
||
end
|
||
|
||
it "sets the topic custom field" do
|
||
first_post.topic.reload
|
||
|
||
expect(first_post.is_first_post?).to be(true)
|
||
expect(first_post_starts_at).to eq_time(starts_at)
|
||
expect(first_post_ends_at).to eq_time(ends_at)
|
||
|
||
first_event_date = post_event.event_dates.last
|
||
expect(first_event_date.starts_at).to eq_time(starts_at)
|
||
expect(first_event_date.finished_at).to be nil
|
||
|
||
post_event.update_with_params!(
|
||
original_starts_at: alt_starts_at,
|
||
original_ends_at: alt_ends_at,
|
||
)
|
||
first_post.topic.reload
|
||
first_event_date.reload
|
||
|
||
second_event_date = post_event.event_dates.last
|
||
|
||
expect(
|
||
Time.zone.parse(
|
||
first_post.topic.custom_fields[DiscoursePostEvent::TOPIC_POST_EVENT_STARTS_AT],
|
||
),
|
||
).to eq(alt_starts_at)
|
||
expect(
|
||
Time.zone.parse(
|
||
first_post.topic.custom_fields[DiscoursePostEvent::TOPIC_POST_EVENT_ENDS_AT],
|
||
),
|
||
).to eq(alt_ends_at)
|
||
|
||
expect(first_event_date.finished_at).not_to be nil
|
||
expect(second_event_date.starts_at).to eq_time(alt_starts_at)
|
||
|
||
second_event_date.update_columns(finished_at: Time.current)
|
||
expect(post_event.starts_at).to eq_time(alt_starts_at)
|
||
expect(post_event.ends_at).to eq_time(alt_ends_at)
|
||
end
|
||
end
|
||
|
||
context "when the associated post is not the OP" do
|
||
let(:post_event) { Fabricate(:event, post: second_post, original_starts_at: starts_at) }
|
||
|
||
it "doesn’t set the topic custom field" do
|
||
expect(second_post.is_first_post?).to be(false)
|
||
expect(
|
||
second_post.topic.custom_fields[DiscoursePostEvent::TOPIC_POST_EVENT_STARTS_AT],
|
||
).to be_blank
|
||
|
||
post_event.update_with_params!(original_starts_at: alt_starts_at)
|
||
second_post.topic.reload
|
||
|
||
expect(
|
||
second_post.topic.custom_fields[DiscoursePostEvent::TOPIC_POST_EVENT_STARTS_AT],
|
||
).to be_blank
|
||
|
||
second_event_date = post_event.event_dates.last
|
||
expect(second_event_date.starts_at).to eq_time(alt_starts_at)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#after_commit[:destroy]" do
|
||
context "when a post event has been destroyed" do
|
||
context "when the associated post is the OP" do
|
||
let!(:post_event) do
|
||
Fabricate(
|
||
:event,
|
||
post: first_post,
|
||
original_starts_at: starts_at,
|
||
original_ends_at: ends_at,
|
||
)
|
||
end
|
||
|
||
it "sets the topic custom field" do
|
||
first_post.topic.reload
|
||
|
||
expect(first_post.is_first_post?).to be(true)
|
||
expect(first_post_starts_at).to eq_time(starts_at)
|
||
expect(first_post_ends_at).to eq_time(ends_at)
|
||
|
||
post_event.destroy!
|
||
first_post.topic.reload
|
||
|
||
expect(
|
||
first_post.topic.custom_fields[DiscoursePostEvent::TOPIC_POST_EVENT_STARTS_AT],
|
||
).to be_blank
|
||
expect(
|
||
first_post.topic.custom_fields[DiscoursePostEvent::TOPIC_POST_EVENT_ENDS_AT],
|
||
).to be_blank
|
||
end
|
||
end
|
||
|
||
context "when the associated post is not the OP" do
|
||
let!(:first_post_event) do
|
||
Fabricate(
|
||
:event,
|
||
post: first_post,
|
||
original_starts_at: starts_at,
|
||
original_ends_at: ends_at,
|
||
)
|
||
end
|
||
let!(:second_post_event) do
|
||
Fabricate(
|
||
:event,
|
||
post: second_post,
|
||
original_starts_at: starts_at,
|
||
original_ends_at: ends_at,
|
||
)
|
||
end
|
||
|
||
it "doesn’t change the topic custom field" do
|
||
second_post.topic.reload
|
||
|
||
expect(first_post.is_first_post?).to be(true)
|
||
expect(
|
||
Time.zone.parse(
|
||
second_post.topic.custom_fields[DiscoursePostEvent::TOPIC_POST_EVENT_STARTS_AT],
|
||
),
|
||
).to eq(starts_at)
|
||
expect(
|
||
Time.zone.parse(
|
||
second_post.topic.custom_fields[DiscoursePostEvent::TOPIC_POST_EVENT_ENDS_AT],
|
||
),
|
||
).to eq(ends_at)
|
||
expect(second_post.is_first_post?).to be(false)
|
||
|
||
second_post_event.destroy!
|
||
second_post.topic.reload
|
||
|
||
expect(
|
||
Time.zone.parse(
|
||
second_post.topic.custom_fields[DiscoursePostEvent::TOPIC_POST_EVENT_STARTS_AT],
|
||
),
|
||
).to eq(starts_at)
|
||
expect(
|
||
Time.zone.parse(
|
||
second_post.topic.custom_fields[DiscoursePostEvent::TOPIC_POST_EVENT_ENDS_AT],
|
||
),
|
||
).to eq(ends_at)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#ongoing?" do
|
||
let(:user) { Fabricate(:user, admin: true, refresh_auto_groups: true) }
|
||
let(:topic) { Fabricate(:topic, user: user) }
|
||
let!(:first_post) { Fabricate(:post, topic: topic) }
|
||
|
||
context "with ends_at" do
|
||
context "with starts_at < current date" do
|
||
context "with ends_at < current date" do
|
||
it "is ongoing" do
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(
|
||
original_starts_at: 2.hours.ago,
|
||
original_ends_at: 1.hour.ago,
|
||
post: first_post,
|
||
)
|
||
|
||
expect(post_event.ongoing?).to be(false)
|
||
end
|
||
end
|
||
|
||
context "with ends_at > current date" do
|
||
it "is not ongoing" do
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(
|
||
original_starts_at: 2.hours.ago,
|
||
original_ends_at: 3.hours.from_now,
|
||
post: first_post,
|
||
)
|
||
|
||
expect(post_event.ongoing?).to be(true)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when starts_at > current date" do
|
||
context "when ends_at > current date" do
|
||
it "is not ongoing" do
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(
|
||
original_starts_at: 1.hour.from_now,
|
||
original_ends_at: 2.hours.from_now,
|
||
post: first_post,
|
||
)
|
||
|
||
expect(post_event.ongoing?).to be(false)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
context "without ends_at date" do
|
||
context "when starts_at < current date" do
|
||
it "is ongoing" do
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(original_starts_at: 2.hours.ago, post: first_post)
|
||
|
||
expect(post_event.ongoing?).to be(true)
|
||
end
|
||
end
|
||
|
||
context "when starts_at == current date" do
|
||
it "is ongoing" do
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(original_starts_at: Time.now, post: first_post)
|
||
|
||
expect(post_event.ongoing?).to be(true)
|
||
end
|
||
end
|
||
|
||
context "when starts_at > current date" do
|
||
it "is not ongoing" do
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(original_starts_at: 1.hour.from_now, post: first_post)
|
||
|
||
expect(post_event.ongoing?).to be(false)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#currently_within_event_timeframe?" do
|
||
let(:user) { Fabricate(:user, admin: true, refresh_auto_groups: true) }
|
||
let(:topic) { Fabricate(:topic, user: user) }
|
||
let!(:first_post) { Fabricate(:post, topic: topic) }
|
||
|
||
def event_with(starts_at:, ends_at: nil, all_day: false)
|
||
DiscoursePostEvent::Event.create!(
|
||
original_starts_at: starts_at,
|
||
original_ends_at: ends_at,
|
||
all_day:,
|
||
post: first_post,
|
||
)
|
||
end
|
||
|
||
it "is false before the early access window opens" do
|
||
expect(event_with(starts_at: 45.minutes.from_now).currently_within_event_timeframe?).to be(
|
||
false,
|
||
)
|
||
end
|
||
|
||
it "is true within the early access window" do
|
||
expect(event_with(starts_at: 15.minutes.from_now).currently_within_event_timeframe?).to be(
|
||
true,
|
||
)
|
||
end
|
||
|
||
it "is true while the event is in progress" do
|
||
expect(
|
||
event_with(
|
||
starts_at: 1.hour.ago,
|
||
ends_at: 1.hour.from_now,
|
||
).currently_within_event_timeframe?,
|
||
).to be(true)
|
||
end
|
||
|
||
it "is true within the grace period after the end" do
|
||
expect(
|
||
event_with(
|
||
starts_at: 2.hours.ago,
|
||
ends_at: 5.minutes.ago,
|
||
).currently_within_event_timeframe?,
|
||
).to be(true)
|
||
end
|
||
|
||
it "is false once the grace period has passed" do
|
||
expect(
|
||
event_with(
|
||
starts_at: 2.hours.ago,
|
||
ends_at: 15.minutes.ago,
|
||
).currently_within_event_timeframe?,
|
||
).to be(false)
|
||
end
|
||
|
||
it "is true for a started event without an end time" do
|
||
expect(event_with(starts_at: 1.day.ago).currently_within_event_timeframe?).to be(true)
|
||
end
|
||
|
||
context "for an all day event" do
|
||
it "is true when now is between the start + end of the day of the event" do
|
||
expect(
|
||
event_with(
|
||
starts_at: Time.zone.now.beginning_of_day,
|
||
all_day: true,
|
||
).currently_within_event_timeframe?,
|
||
).to be(true)
|
||
end
|
||
|
||
it "is false when now is before the start of the day of the event" do
|
||
expect(
|
||
event_with(
|
||
starts_at: Time.zone.now.tomorrow.beginning_of_day,
|
||
all_day: true,
|
||
).currently_within_event_timeframe?,
|
||
).to be(false)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#expired?" do
|
||
let(:user) { Fabricate(:user, admin: true, refresh_auto_groups: true) }
|
||
let(:topic) { Fabricate(:topic, user: user) }
|
||
let!(:first_post) { Fabricate(:post, topic: topic) }
|
||
|
||
context "with ends_at" do
|
||
context "when starts_at < current date" do
|
||
context "when ends_at < current date" do
|
||
it "is expired" do
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(
|
||
original_starts_at: DateTime.parse("2020-04-22 14:05"),
|
||
original_ends_at: DateTime.parse("2020-04-23 14:05"),
|
||
post: first_post,
|
||
)
|
||
|
||
expect(post_event.expired?).to be(true)
|
||
end
|
||
end
|
||
|
||
context "when ends_at > current date" do
|
||
it "is not expired" do
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(
|
||
original_starts_at: DateTime.parse("2020-04-24 14:15"),
|
||
original_ends_at: DateTime.parse("2020-04-25 11:05"),
|
||
post: first_post,
|
||
)
|
||
|
||
expect(post_event.expired?).to be(false)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when starts_at > current date" do
|
||
it "is not expired" do
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(
|
||
original_starts_at: DateTime.parse("2020-04-25 14:05"),
|
||
original_ends_at: DateTime.parse("2020-04-26 14:05"),
|
||
post: first_post,
|
||
)
|
||
|
||
expect(post_event.expired?).to be(false)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "without ends_at date" do
|
||
context "when starts_at < current date" do
|
||
it "is expired" do
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(
|
||
original_starts_at: DateTime.parse("2020-04-24 14:05"),
|
||
post: first_post,
|
||
)
|
||
|
||
expect(post_event.expired?).to be(false)
|
||
end
|
||
end
|
||
|
||
context "when starts_at == current date" do
|
||
it "is expired" do
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(
|
||
original_starts_at: DateTime.parse("2020-04-24 14:10"),
|
||
post: first_post,
|
||
)
|
||
|
||
expect(post_event.expired?).to be(false)
|
||
end
|
||
end
|
||
|
||
context "when starts_at > current date" do
|
||
it "is not expired" do
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(
|
||
original_starts_at: DateTime.parse("2020-04-24 14:15"),
|
||
post: first_post,
|
||
)
|
||
|
||
expect(post_event.expired?).to be(false)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "with recurring events" do
|
||
context "with recurrence_until set" do
|
||
context "when current date is before recurrence_until" do
|
||
it "is not expired" do
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(
|
||
original_starts_at: DateTime.parse("2020-04-22 14:05"),
|
||
original_ends_at: DateTime.parse("2020-04-22 15:05"),
|
||
recurrence: "FREQ=WEEKLY",
|
||
recurrence_until: DateTime.parse("2020-05-01 00:00"),
|
||
post: first_post,
|
||
)
|
||
|
||
expect(post_event.expired?).to be(false)
|
||
end
|
||
end
|
||
|
||
context "when current date is after recurrence_until" do
|
||
it "is expired" do
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(
|
||
original_starts_at: DateTime.parse("2020-04-22 14:05"),
|
||
original_ends_at: DateTime.parse("2020-04-22 15:05"),
|
||
recurrence: "FREQ=WEEKLY",
|
||
recurrence_until: DateTime.parse("2020-04-23 00:00"),
|
||
post: first_post,
|
||
)
|
||
|
||
expect(post_event.expired?).to be(true)
|
||
end
|
||
end
|
||
|
||
context "when current date equals recurrence_until" do
|
||
it "is not expired" do
|
||
current_time = DateTime.parse("2020-04-24 14:10")
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(
|
||
original_starts_at: DateTime.parse("2020-04-22 14:05"),
|
||
original_ends_at: DateTime.parse("2020-04-22 15:05"),
|
||
recurrence: "FREQ=WEEKLY",
|
||
recurrence_until: current_time,
|
||
post: first_post,
|
||
)
|
||
|
||
expect(post_event.expired?).to be(false)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "without recurrence_until set" do
|
||
it "never expires" do
|
||
post_event =
|
||
DiscoursePostEvent::Event.create!(
|
||
original_starts_at: DateTime.parse("2020-04-22 14:05"),
|
||
original_ends_at: DateTime.parse("2020-04-22 15:05"),
|
||
recurrence: "FREQ=WEEKLY",
|
||
recurrence_until: nil,
|
||
post: first_post,
|
||
)
|
||
|
||
expect(post_event.expired?).to be(false)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#duration" do
|
||
let!(:post_1) { Fabricate(:post) }
|
||
|
||
context "when event has both starts_at and ends_at" do
|
||
it "returns duration in HH:MM:SS format" do
|
||
event =
|
||
DiscoursePostEvent::Event.create!(
|
||
id: post_1.id,
|
||
original_starts_at: "2022-01-15 10:00:00 UTC",
|
||
original_ends_at: "2022-01-15 11:30:00 UTC",
|
||
)
|
||
|
||
expect(event.duration).to eq("01:30:00")
|
||
end
|
||
end
|
||
|
||
context "when event only has starts_at" do
|
||
it "returns default duration of 1 hour" do
|
||
event =
|
||
DiscoursePostEvent::Event.create!(
|
||
id: post_1.id,
|
||
original_starts_at: "2022-01-15 10:00:00 UTC",
|
||
)
|
||
|
||
expect(event.duration).to eq("01:00:00")
|
||
end
|
||
end
|
||
|
||
context "when event spans multiple days" do
|
||
it "returns correct duration" do
|
||
event =
|
||
DiscoursePostEvent::Event.create!(
|
||
id: post_1.id,
|
||
original_starts_at: "2022-01-15 10:00:00 UTC",
|
||
original_ends_at: "2022-01-16 12:30:00 UTC",
|
||
)
|
||
|
||
expect(event.duration).to eq("26:30:00")
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#update_with_params!" do
|
||
let!(:post_1) { Fabricate(:post) }
|
||
let!(:user_1) { Fabricate(:user) }
|
||
let(:group_1) do
|
||
Fabricate(:group).tap do |g|
|
||
g.add(user_1)
|
||
g.save!
|
||
end
|
||
end
|
||
|
||
before { freeze_time }
|
||
|
||
context "with a private event" do
|
||
let!(:event_1) do
|
||
Fabricate(
|
||
:event,
|
||
post: post_1,
|
||
status: DiscoursePostEvent::Event.statuses[:private],
|
||
raw_invitees: [group_1.name],
|
||
)
|
||
end
|
||
|
||
before do
|
||
freeze_time
|
||
|
||
event_1.create_invitees([{ user_id: user_1.id, status: 0 }])
|
||
end
|
||
|
||
context "when updating the name" do
|
||
it "doesn’t clear existing invitees" do
|
||
expect(event_1.invitees.count).to eq(1)
|
||
|
||
expect { event_1.update_with_params!(name: "The event") }.not_to change {
|
||
event_1.invitees.count
|
||
}
|
||
end
|
||
end
|
||
|
||
context "when an invitee is no longer allowed" do
|
||
let(:group_2) { Fabricate(:group) }
|
||
|
||
it "resets the pruned invitee's topic tracking" do
|
||
event_1.invitees.find_by(user_id: user_1.id).update_attendance!(:going)
|
||
expect(TopicUser.find_by(user: user_1, topic: post_1.topic).notification_level).to eq(
|
||
TopicUser.notification_levels[:watching],
|
||
)
|
||
|
||
event_1.update_with_params!(raw_invitees: [group_2.name])
|
||
|
||
expect(event_1.invitees.find_by(user_id: user_1.id)).to be_nil
|
||
expect(TopicUser.find_by(user: user_1, topic: post_1.topic).notification_level).to eq(
|
||
TopicUser.notification_levels[:regular],
|
||
)
|
||
end
|
||
|
||
it "unfollows the pruned invitee from the livestream chat channel" do
|
||
channel = Fabricate(:category_channel)
|
||
Fabricate(:topic_chat_channel, topic: post_1.topic, chat_channel: channel)
|
||
membership =
|
||
Fabricate(
|
||
:user_chat_channel_membership,
|
||
user: user_1,
|
||
chat_channel: channel,
|
||
following: true,
|
||
)
|
||
|
||
event_1.update_with_params!(raw_invitees: [group_2.name])
|
||
|
||
expect(event_1.invitees.find_by(user_id: user_1.id)).to be_nil
|
||
expect(membership.reload.following).to eq(false)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "resetting topic tracking when the event is destroyed" do
|
||
fab!(:user_1, :user)
|
||
fab!(:topic_1, :topic)
|
||
fab!(:post_1) { Fabricate(:post, topic: topic_1) }
|
||
fab!(:event_1) { Fabricate(:event, post: post_1) }
|
||
|
||
before { event_1.create_invitees([{ user_id: user_1.id, status: 0 }]) }
|
||
|
||
it "resets a watching invitee back to regular, leaving the topic intact" do
|
||
event_1.invitees.find_by(user_id: user_1.id).update_attendance!(:going)
|
||
expect(TopicUser.find_by(user: user_1, topic: topic_1).notification_level).to eq(
|
||
TopicUser.notification_levels[:watching],
|
||
)
|
||
|
||
event_1.destroy!
|
||
|
||
expect(Topic.exists?(topic_1.id)).to eq(true)
|
||
expect(TopicUser.find_by(user: user_1, topic: topic_1).notification_level).to eq(
|
||
TopicUser.notification_levels[:regular],
|
||
)
|
||
end
|
||
end
|
||
|
||
describe "#missing_users" do
|
||
let!(:post_1) { Fabricate(:post) }
|
||
let!(:user_1) { Fabricate(:user) }
|
||
let!(:user_2) { Fabricate(:user) }
|
||
let!(:user_3) { Fabricate(:user) }
|
||
let!(:group_1) do
|
||
Fabricate(:group).tap do |g|
|
||
g.add(user_1)
|
||
g.add(user_2)
|
||
g.add(user_3)
|
||
g.save!
|
||
end
|
||
end
|
||
let!(:group_2) do
|
||
Fabricate(:group).tap do |g|
|
||
g.add(user_2)
|
||
g.save!
|
||
end
|
||
end
|
||
let!(:event_1) do
|
||
Fabricate(
|
||
:event,
|
||
post: post_1,
|
||
status: DiscoursePostEvent::Event.statuses[:private],
|
||
raw_invitees: [group_1.name, group_2.name],
|
||
)
|
||
end
|
||
|
||
before { DiscoursePostEvent::Invitee.create_attendance!(user_3.id, post_1.id, :going) }
|
||
|
||
it "doesn't return already attending user" do
|
||
expect(event_1.missing_users.pluck(:id)).to_not include(user_3.id)
|
||
end
|
||
|
||
it "return users from groups with no duplicates" do
|
||
expect(event_1.missing_users.pluck(:id)).to match_array([user_1.id, user_2.id])
|
||
end
|
||
|
||
context "with private event with empty raw_invitees" do
|
||
let!(:event_without_invitees) do
|
||
Fabricate(
|
||
:event,
|
||
post: Fabricate(:post),
|
||
status: DiscoursePostEvent::Event.statuses[:private],
|
||
raw_invitees: [],
|
||
)
|
||
end
|
||
|
||
it "does not return all site users" do
|
||
expect(event_without_invitees.missing_users.count).to eq(0)
|
||
expect(User.real.activated.not_silenced.not_suspended.not_staged.count).not_to eq(0)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#calculate_next_date" do
|
||
subject(:next_date) { event.calculate_next_date }
|
||
|
||
context "when the event is recurring" do
|
||
context "when the recurring ends on the next day" do
|
||
let(:event) do
|
||
Fabricate(
|
||
:event,
|
||
recurrence: "every_day",
|
||
recurrence_until: "2020-04-25 23:59",
|
||
original_starts_at: "2020-04-20 13:00",
|
||
)
|
||
end
|
||
|
||
it "returns the next occurrence within the recurrence period" do
|
||
expect(next_date).not_to be_blank
|
||
expect(next_date).to be_an(Array)
|
||
expect(next_date.length).to eq(2)
|
||
expect(next_date[0]).to eq(Time.utc(2020, 4, 25, 13, 0, 0))
|
||
expect(next_date[1]).to eq(Time.utc(2020, 4, 25, 14, 0, 0))
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#starts_at and #ends_at for expired recurring events" do
|
||
context "when recurring event has expired (past recurrence_until)" do
|
||
let(:expired_recurring_event) do
|
||
event =
|
||
Fabricate(
|
||
:event,
|
||
recurrence: "every_week",
|
||
recurrence_until: 1.day.ago,
|
||
original_starts_at: 1.week.ago,
|
||
original_ends_at: 1.week.ago + 2.hours,
|
||
)
|
||
event
|
||
end
|
||
|
||
it "returns nil for starts_at since no future dates can be computed" do
|
||
expect(expired_recurring_event.starts_at).to be_nil
|
||
end
|
||
|
||
it "returns nil for ends_at since no future dates can be computed" do
|
||
expect(expired_recurring_event.ends_at).to be_nil
|
||
end
|
||
|
||
it "serializer handles nil starts_at correctly" do
|
||
serializer =
|
||
DiscoursePostEvent::EventSerializer.new(
|
||
expired_recurring_event,
|
||
scope: Guardian.new,
|
||
root: false,
|
||
)
|
||
json = JSON.parse(serializer.to_json)
|
||
|
||
expect(json["starts_at"]).to be_nil
|
||
expect(json["ends_at"]).to be_nil
|
||
end
|
||
|
||
it "basic serializer handles expired recurring events correctly" do
|
||
serializer =
|
||
DiscoursePostEvent::BasicEventSerializer.new(
|
||
expired_recurring_event,
|
||
root: false,
|
||
scope: Guardian.new,
|
||
)
|
||
json = JSON.parse(serializer.to_json)
|
||
|
||
expect(json["starts_at"]).to be_nil
|
||
expect(json["ends_at"]).to be_nil
|
||
end
|
||
end
|
||
|
||
context "when recurring event has no recurrence_until (endless)" do
|
||
let(:endless_recurring_event) do
|
||
Fabricate(
|
||
:event,
|
||
recurrence: "every_week",
|
||
recurrence_until: nil,
|
||
original_starts_at: 1.week.ago,
|
||
original_ends_at: 1.week.ago + 2.hours,
|
||
)
|
||
end
|
||
|
||
it "still returns starts_at from event_dates" do
|
||
expect(endless_recurring_event.starts_at).not_to be_nil
|
||
end
|
||
|
||
it "still returns ends_at from event_dates" do
|
||
expect(endless_recurring_event.starts_at).not_to be_nil
|
||
end
|
||
end
|
||
|
||
context "when non-recurring event" do
|
||
let(:non_recurring_event) do
|
||
Fabricate(
|
||
:event,
|
||
recurrence: nil,
|
||
original_starts_at: 1.week.ago,
|
||
original_ends_at: 1.week.ago + 2.hours,
|
||
)
|
||
end
|
||
|
||
it "still returns starts_at from event_dates regardless of when it was" do
|
||
expect(non_recurring_event.starts_at).not_to be_nil
|
||
end
|
||
|
||
it "still returns ends_at from event_dates regardless of when it was" do
|
||
expect(non_recurring_event.ends_at).not_to be_nil
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "syncing from raw" do
|
||
fab!(:user) { Fabricate(:user, admin: true, refresh_auto_groups: true) }
|
||
fab!(:topic) { Fabricate(:topic, user: user) }
|
||
fab!(:post) { Fabricate(:post, topic: topic, user: user) }
|
||
fab!(:upload)
|
||
|
||
it "sets livestream from the bbcode attribute" do
|
||
Jobs.run_later!
|
||
|
||
post.update!(
|
||
raw:
|
||
"[event start=\"2020-04-24 14:15\" livestream=\"true\" location=\"https://www.youtube.com/live/abc123\"]\n[/event]",
|
||
)
|
||
post.rebake!
|
||
DiscoursePostEvent::Event::SyncFromPost.call(params: { post_id: post.id })
|
||
post.reload
|
||
|
||
expect(post.event.livestream).to eq(true)
|
||
end
|
||
|
||
it "defaults livestream to false when the attribute is absent" do
|
||
post.update!(raw: "[event start=\"2020-04-24 14:15\"]\n[/event]")
|
||
post.rebake!
|
||
DiscoursePostEvent::Event::SyncFromPost.call(params: { post_id: post.id })
|
||
post.reload
|
||
|
||
expect(post.event.livestream).to eq(false)
|
||
end
|
||
|
||
it "ignores livestream=true when the location is not an http(s) URL" do
|
||
post.update!(
|
||
raw: "[event start=\"2020-04-24 14:15\" livestream=\"true\" location=\"Room 5\"]\n[/event]",
|
||
)
|
||
post.rebake!
|
||
DiscoursePostEvent::Event::SyncFromPost.call(params: { post_id: post.id })
|
||
post.reload
|
||
|
||
expect(post.event.livestream).to eq(false)
|
||
end
|
||
|
||
context "with image" do
|
||
before do
|
||
post.update!(
|
||
raw: "[event start=\"2020-04-24 14:15\" image=\"#{upload.short_url}\"]\n[/event]",
|
||
)
|
||
post.rebake!
|
||
DiscoursePostEvent::Event::SyncFromPost.call(params: { post_id: post.id })
|
||
post.reload
|
||
end
|
||
|
||
it "resolves image to image_upload_id and creates UploadReference" do
|
||
expect(post.event.image_upload_id).to eq(upload.id)
|
||
expect(UploadReference.exists?(upload_id: upload.id, target: post.event)).to eq(true)
|
||
end
|
||
|
||
it "clears image_upload_id when image is removed" do
|
||
post.update!(raw: "[event start=\"2020-04-24 14:15\"]\n[/event]")
|
||
post.rebake!
|
||
DiscoursePostEvent::Event::SyncFromPost.call(params: { post_id: post.id })
|
||
post.reload
|
||
|
||
expect(post.event.image_upload_id).to be_nil
|
||
expect(UploadReference.exists?(upload_id: upload.id, target: post.event)).to eq(false)
|
||
end
|
||
end
|
||
|
||
describe "post.image_upload_id sync on post_process_cooked" do
|
||
fab!(:topic) { Fabricate(:topic, user: user) }
|
||
|
||
it "sets post.image_upload_id from the event image after post processing" do
|
||
post =
|
||
Fabricate(
|
||
:post,
|
||
topic: topic,
|
||
user: user,
|
||
raw: "[event start=\"2020-04-24 14:15\" image=\"#{upload.short_url}\"]\n[/event]",
|
||
)
|
||
post.rebake!
|
||
DiscoursePostEvent::Event::SyncFromPost.call(params: { post_id: post.id })
|
||
post.reload
|
||
CookedPostProcessor.new(post).post_process
|
||
|
||
post.reload
|
||
expect(post.image_upload_id).to eq(upload.id)
|
||
end
|
||
|
||
it "sets topic.image_upload_id when the post is the first post" do
|
||
post = topic.first_post || Fabricate(:post, topic: topic, user: user, post_number: 1)
|
||
post.update!(
|
||
raw: "[event start=\"2020-04-24 14:15\" image=\"#{upload.short_url}\"]\n[/event]",
|
||
)
|
||
post.rebake!
|
||
DiscoursePostEvent::Event::SyncFromPost.call(params: { post_id: post.id })
|
||
post.reload
|
||
CookedPostProcessor.new(post).post_process
|
||
|
||
post.reload
|
||
topic.reload
|
||
expect(topic.image_upload_id).to eq(upload.id)
|
||
end
|
||
|
||
it "does not override post.image_upload_id when event has no image" do
|
||
other_upload = Fabricate(:upload)
|
||
post =
|
||
Fabricate(
|
||
:post,
|
||
topic: topic,
|
||
user: user,
|
||
raw: "[event start=\"2020-04-24 14:15\"]\n[/event]\n",
|
||
)
|
||
post.rebake!
|
||
DiscoursePostEvent::Event::SyncFromPost.call(params: { post_id: post.id })
|
||
post.reload
|
||
CookedPostProcessor.new(post).post_process
|
||
|
||
post.reload
|
||
expect(post.image_upload_id).not_to eq(nil)
|
||
expect(post.event.image_upload_id).to be_nil
|
||
end
|
||
end
|
||
|
||
context "with an image from a different secure post" do
|
||
fab!(:private_upload_owner, :user)
|
||
fab!(:private_upload_recipient, :user)
|
||
fab!(:private_upload_post) do
|
||
Fabricate(
|
||
:private_message_post,
|
||
user: private_upload_owner,
|
||
recipient: private_upload_recipient,
|
||
)
|
||
end
|
||
fab!(:private_upload) do
|
||
Fabricate(
|
||
:secure_upload,
|
||
user: private_upload_owner,
|
||
access_control_post: private_upload_post,
|
||
)
|
||
end
|
||
|
||
before do
|
||
setup_s3
|
||
SiteSetting.secure_uploads = true
|
||
|
||
post.update!(
|
||
raw: "[event start=\"2020-04-24 14:15\" image=\"#{private_upload.url}\"]\n[/event]",
|
||
)
|
||
post.rebake!
|
||
end
|
||
|
||
it "does not associate the upload" do
|
||
DiscoursePostEvent::Event::SyncFromPost.call(params: { post_id: post.id })
|
||
post.reload
|
||
|
||
expect(post.event.image_upload_id).to be_nil
|
||
expect(UploadReference.exists?(upload_id: private_upload.id, target: post.event)).to eq(
|
||
false,
|
||
)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "post/topic image_upload_id sync on post_edited" do
|
||
fab!(:user) { Fabricate(:user, admin: true, refresh_auto_groups: true) }
|
||
fab!(:topic) { Fabricate(:topic, user: user) }
|
||
fab!(:post) { Fabricate(:post, topic: topic, user: user, post_number: 1) }
|
||
fab!(:upload)
|
||
|
||
it "syncs event image to post and topic when image is added" do
|
||
PostRevisor.new(post).revise!(
|
||
user,
|
||
raw: "[event start=\"2020-04-24 14:15\" image=\"#{upload.short_url}\"]\n[/event]",
|
||
)
|
||
post.reload
|
||
topic.reload
|
||
|
||
expect(post.image_upload_id).to eq(upload.id)
|
||
expect(topic.image_upload_id).to eq(upload.id)
|
||
end
|
||
|
||
it "does not clear post image_upload_id when event has no image but post has inline images" do
|
||
other_upload = Fabricate(:upload)
|
||
PostRevisor.new(post).revise!(
|
||
user,
|
||
raw: "[event start=\"2020-04-24 14:15\"]\n[/event]\n",
|
||
)
|
||
CookedPostProcessor.new(post).post_process
|
||
post.reload
|
||
|
||
PostRevisor.new(post).revise!(
|
||
user,
|
||
raw:
|
||
"[event start=\"2020-04-24 14:15\"]\n[/event]\n\nupdated text",
|
||
)
|
||
post.reload
|
||
|
||
expect(post.image_upload_id).to eq(other_upload.id)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe DiscoursePostEvent::Event, "#most_likely_going" do
|
||
before do
|
||
Jobs.run_immediately!
|
||
SiteSetting.calendar_enabled = true
|
||
SiteSetting.discourse_post_event_enabled = true
|
||
end
|
||
|
||
fab!(:event)
|
||
|
||
it "orders going invitees by RSVP time (created_at), not by user id" do
|
||
early_rsvp = Fabricate(:user)
|
||
late_rsvp = Fabricate(:user)
|
||
# later-created user (higher id) RSVP'd first, so should come first
|
||
expect(late_rsvp.id).to be > early_rsvp.id
|
||
|
||
Fabricate(
|
||
:post_event_invitee,
|
||
event:,
|
||
user: late_rsvp,
|
||
status: DiscoursePostEvent::Invitee.statuses[:going],
|
||
created_at: 2.hours.ago,
|
||
)
|
||
Fabricate(
|
||
:post_event_invitee,
|
||
event:,
|
||
user: early_rsvp,
|
||
status: DiscoursePostEvent::Invitee.statuses[:going],
|
||
created_at: 1.hour.ago,
|
||
)
|
||
|
||
expect(event.most_likely_going.map(&:user)).to eq([late_rsvp, early_rsvp])
|
||
end
|
||
|
||
it "groups going invitees ahead of interested ones regardless of RSVP time" do
|
||
interested = Fabricate(:user)
|
||
going = Fabricate(:user)
|
||
|
||
Fabricate(
|
||
:post_event_invitee,
|
||
event:,
|
||
user: interested,
|
||
status: DiscoursePostEvent::Invitee.statuses[:interested],
|
||
created_at: 2.hours.ago,
|
||
)
|
||
Fabricate(
|
||
:post_event_invitee,
|
||
event:,
|
||
user: going,
|
||
status: DiscoursePostEvent::Invitee.statuses[:going],
|
||
created_at: 1.hour.ago,
|
||
)
|
||
|
||
expect(event.most_likely_going.map(&:user)).to eq([going, interested])
|
||
end
|
||
end
|
||
|
||
describe DiscoursePostEvent::Event, "#capacity" do
|
||
before do
|
||
Jobs.run_immediately!
|
||
SiteSetting.calendar_enabled = true
|
||
SiteSetting.discourse_post_event_enabled = true
|
||
end
|
||
|
||
it "detects capacity when max_attendees set" do
|
||
creator = Fabricate(:user)
|
||
topic = Fabricate(:topic, user: creator)
|
||
post = Fabricate(:post, user: creator, topic: topic)
|
||
event = Fabricate(:event, post: post, max_attendees: 1)
|
||
event.create_invitees(
|
||
[{ user_id: creator.id, status: DiscoursePostEvent::Invitee.statuses[:going] }],
|
||
)
|
||
expect(event.at_capacity?).to eq(true)
|
||
end
|
||
end
|