0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-11 02:59:07 +08:00
discourse/plugins/discourse-calendar/spec/requests/events_controller_spec.rb
Régis Hanol 5a926782fe
FIX: Single-occurrence RSVPs filled the "My events" calendar (#40813)
Addresses the **My events** calendar issue reported in [meta
t/335780](https://meta.discourse.org/t/repeating-event-going-gets-not-deleted-anymore/335780),
and improves how the event popup loads.

## FIX: Single-occurrence RSVPs filled the "My events" calendar

When a member RSVPs "going" to only the next occurrence of a recurring
event (not the whole series), the personal **My events** calendar — and
the personal ICS feed — listed the event on _every_ occurrence instead
of just the one they joined.

`EventFinder` matches an event for `attending_user` whenever the member
is a "going" invitee, ignoring the invitee's `recurring` flag, and the
events endpoint then expands the full recurrence series. The
per-occurrence invitee list already filtered single RSVPs out of future
occurrences, but the calendar/feed query did not.

`ExpandOccurrences` gains a `current_occurrence_only` option that
returns only the event's current occurrence (when it falls inside the
requested window). `EventsController#index` sets it for the attending
member's non-recurring "going" RSVPs, in both the JSON and ICS
responses. RSVPs to every occurrence still expand fully; the
non-personal calendars (all events / category) are unaffected.

This is a targeted fix — properly modelling per-occurrence RSVPs
(storing each date instead of only the recurrence formula) remains a
larger follow-up.

## UX: Render event popup details immediately instead of a spinner

Clicking an event opened a popup that showed only a loading spinner
until a full event fetch resolved — slow on poor connections. The
component wrapped its whole body in `DAsyncContent`, blocking every
field on the request even though the calendar already provides each
event's core fields (`BasicEventSerializer`) and the inline post widget
provides the complete event (`EventSerializer`).

It now renders the data already on the client immediately (title, date,
recurrence) and fills in the rest (creator, attendees, RSVP buttons)
when the background request returns. Inline event widgets in a topic
render fully with no fetch at all (and no longer fire a redundant
request). Still-loading sections use core's `.placeholder-animation`
shimmer.

## Testing

- Request spec — single-occurrence RSVP returns one occurrence;
all-occurrences RSVP returns the series.
- Service spec — `current_occurrence_only` returns the current
occurrence only when it is inside the window.
- System spec — a daily recurring event RSVPed once shows a single tile
on `/upcoming-events/mine`.
- Existing popup integration + system specs cover the immediate-render
behaviour.
2026-07-01 21:04:25 +02:00

1011 lines
36 KiB
Ruby
Vendored
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# frozen_string_literal: true
module DiscoursePostEvent
describe EventsController do
before do
Jobs.run_immediately!
SiteSetting.calendar_enabled = true
SiteSetting.discourse_post_event_enabled = true
SiteSetting.displayed_invitees_limit = 3
end
describe "#index" do
it "should not result in N+1 queries problem when multiple events are returned" do
# Warmup
get "/discourse-post-event/events.json"
# Test with 1 event
Fabricate(:event, original_starts_at: 1.day.from_now)
queries_with_1_event = track_sql_queries { get "/discourse-post-event/events.json" }
expect(response.status).to eq(200)
expect(response.parsed_body["events"].length).to eq(1)
# Test with 3 events
Fabricate(:event, original_starts_at: 2.days.from_now)
Fabricate(:event, original_starts_at: 3.days.from_now)
queries_with_3_events = track_sql_queries { get "/discourse-post-event/events.json" }
expect(response.status).to eq(200)
expect(response.parsed_body["events"].length).to eq(3)
expect(queries_with_3_events.count).to eq(queries_with_1_event.count)
end
it "should not show deleted events" do
active_event1 = Fabricate(:event, original_starts_at: 1.day.from_now)
active_event2 = Fabricate(:event, original_starts_at: 2.days.from_now)
deleted_event =
Fabricate(:event, original_starts_at: 3.days.from_now, deleted_at: 1.hour.ago)
get "/discourse-post-event/events.json"
expect(response.status).to eq(200)
events = response.parsed_body["events"]
event_ids = events.map { |e| e["id"] }
expect(event_ids).to match_array([active_event1.id, active_event2.id])
end
it "should not show closed events" do
active_event1 = Fabricate(:event, original_starts_at: 1.day.from_now)
active_event2 = Fabricate(:event, original_starts_at: 2.days.from_now)
closed_event = Fabricate(:event, original_starts_at: 3.days.from_now, closed: true)
get "/discourse-post-event/events.json"
expect(response.status).to eq(200)
events = response.parsed_body["events"]
event_ids = events.map { |e| e["id"] }
expect(event_ids).to match_array([active_event1.id, active_event2.id])
end
it "includes linkified multiline description HTML in detailed JSON" do
event =
Fabricate(
:event,
original_starts_at: 1.day.from_now,
description: "Visit https://example.com\n\nBring snacks",
)
get "/discourse-post-event/events.json", params: { include_details: "true" }
expect(response.status).to eq(200)
description_html =
response.parsed_body["events"].find { |event_json| event_json["id"] == event.id }[
"description_html"
]
expect(description_html).to include('<a href="https://example.com"')
expect(description_html).to include("<br>")
end
it "should return events in ics format" do
event1 = Fabricate(:event, original_starts_at: 1.day.from_now, name: "Test Event 1")
event2 = Fabricate(:event, original_starts_at: 2.days.from_now, name: "Test Event 2")
get "/discourse-post-event/events.ics"
expect(response.status).to eq(200)
expect(response.content_type).to include("text/calendar")
event_ids_sorted = [event1.id, event2.id].sort.join("-")
expected_filename = "events-#{Digest::SHA1.hexdigest(event_ids_sorted)}.ics"
expect(response.headers["Content-Disposition"]).to eq(
"attachment; filename=\"#{expected_filename}\"",
)
body = response.body
calendar_name =
I18n.t(
"discourse_calendar.calendar_subscriptions.all_events_feed_name",
site_title: SiteSetting.title,
)
expect(body).to include("BEGIN:VCALENDAR")
expect(body).to include("END:VCALENDAR")
expect(body).to include("X-WR-CALNAME:#{IcalEncoder.encode(calendar_name)}")
expect(body).to include("BEGIN:VEVENT")
expect(body).to include("END:VEVENT")
expect(body).to include("SUMMARY:Test Event 1")
expect(body).to include("SUMMARY:Test Event 2")
end
it "should include location and description in ics format" do
event =
Fabricate(
:event,
original_starts_at: 1.day.from_now,
name: "Tech Conference",
location: "https://meet.google.com/abc-defg-hij",
description: "Bring your laptop and questions!",
url: "https://example.com/event-info",
)
get "/discourse-post-event/events.ics"
expect(response.status).to eq(200)
expect(response.content_type).to include("text/calendar")
body = response.body
expect(body).to include("SUMMARY:Tech Conference")
expect(body).to include("LOCATION:https://meet.google.com/abc-defg-hij")
expect(body).to include("DESCRIPTION:Bring your laptop and questions!")
expect(body).to include("URL:https://example.com/event-info")
end
it "should not HTML-encode ampersands in ics format" do
Fabricate(
:event,
original_starts_at: 1.day.from_now,
name: "Tom & Jerry",
location: "Room A & B",
description: "Q & A session",
url: "https://example.com/event?a=1&b=2",
)
get "/discourse-post-event/events.ics"
expect(response.body).to include("SUMMARY:Tom & Jerry")
expect(response.body).not_to include("&amp;")
end
it "should not HTML-encode topic title used as ics summary" do
event = Fabricate(:event, original_starts_at: 1.day.from_now)
event.post.topic.update!(title: "Rock & Roll Music Festival 2026")
get "/discourse-post-event/events.ics"
expect(response.body).to include("SUMMARY:Rock & Roll Music Festival 2026")
end
it "should handle events without location and description in ics format" do
event = Fabricate(:event, original_starts_at: 1.day.from_now, name: "Simple Event")
get "/discourse-post-event/events.ics"
expect(response.status).to eq(200)
body = response.body
expect(body).to include("SUMMARY:Simple Event")
# Should still generate valid ICS even without LOCATION/DESCRIPTION
expect(body).to include("BEGIN:VEVENT")
expect(body).to include("END:VEVENT")
end
it "should include post url in description when no custom description is set" do
event =
Fabricate(:event, original_starts_at: 1.day.from_now, name: "Event Without Description")
get "/discourse-post-event/events.ics"
expect(response.status).to eq(200)
body = response.body
description_line = body.lines.find { |l| l.start_with?("DESCRIPTION:") }
post_url = "#{Discourse.base_url}#{event.post.url}"
expect(description_line).to include(post_url)
end
it "excludes events older than 3 months from ics feed by default" do
recent_event = Fabricate(:event, original_starts_at: 1.day.from_now, name: "Future Event")
old_event = Fabricate(:event, original_starts_at: 4.months.ago, name: "Old Event")
get "/discourse-post-event/events.ics"
expect(response.status).to eq(200)
body = response.body
expect(body).to include("SUMMARY:Future Event")
expect(body).not_to include("SUMMARY:Old Event")
end
it "expands recurring events into multiple occurrences" do
Fabricate(
:event,
original_starts_at: 1.day.from_now,
original_ends_at: 1.day.from_now + 1.hour,
recurrence: "every_week",
name: "Weekly Standup",
)
get "/discourse-post-event/events.ics"
expect(response.status).to eq(200)
body = response.body
occurrences = body.scan("SUMMARY:Weekly Standup").size
expect(occurrences).to be > 1
expect(occurrences).to be <= 52
end
it "gives recurring and non-recurring events unique UIDs" do
Fabricate(
:event,
original_starts_at: 1.day.from_now,
recurrence: "every_week",
name: "Recurring",
)
Fabricate(:event, original_starts_at: 2.days.from_now, name: "One-off")
get "/discourse-post-event/events.ics"
body = response.body
uids = body.scan(/^UID:(.+)$/).flatten
expect(uids.size).to eq(uids.uniq.size)
end
it "skips events with nil starts_at in ics feed" do
event = Fabricate(:event, original_starts_at: 1.day.from_now, name: "Valid Event")
get "/discourse-post-event/events.ics"
expect(response.status).to eq(200)
expect(response.body).to include("SUMMARY:Valid Event")
end
describe "with all-day events" do
before { freeze_time Time.utc(2026, 3, 1, 12) }
it "emits all-day events as DATE values with an exclusive end date" do
Fabricate(
:event,
original_starts_at: Time.utc(2026, 3, 12),
original_ends_at: Time.utc(2026, 3, 14).end_of_day,
all_day: true,
name: "All Day Conference",
)
get "/discourse-post-event/events.ics"
expect(response.status).to eq(200)
body = response.body
expect(body).to include("SUMMARY:All Day Conference")
expect(body).to include("DTSTART;VALUE=DATE:20260312")
expect(body).to include("DTEND;VALUE=DATE:20260315")
expect(body).not_to include("DTSTART:20260312T000000Z")
end
it "emits a single-day all-day event spanning one day" do
Fabricate(
:event,
original_starts_at: Time.utc(2026, 3, 12),
original_ends_at: nil,
all_day: true,
name: "All Day Holiday",
)
get "/discourse-post-event/events.ics"
expect(response.status).to eq(200)
body = response.body
expect(body).to include("DTSTART;VALUE=DATE:20260312")
expect(body).to include("DTEND;VALUE=DATE:20260313")
end
it "emits recurring all-day occurrences as DATE values" do
Fabricate(
:event,
original_starts_at: Time.utc(2026, 3, 12),
all_day: true,
recurrence: "every_week",
name: "Weekly All Day",
)
get "/discourse-post-event/events.ics"
expect(response.status).to eq(200)
body = response.body
expect(body.scan("DTSTART;VALUE=DATE:").size).to be > 1
expect(body).not_to match(/DTSTART:\d{8}T\d{6}Z/)
expect(body).to include("DTSTART;VALUE=DATE:20260312")
expect(body).to include("DTEND;VALUE=DATE:20260313")
end
end
context "when include_interested is requested for an attending user" do
fab!(:target_user, :user)
fab!(:going_event) do
Fabricate(:event, original_starts_at: 1.day.from_now, name: "Going Event")
end
fab!(:interested_event) do
Fabricate(:event, original_starts_at: 2.days.from_now, name: "Interested Event")
end
let(:events_params) { { attending_user: target_user.username, include_interested: true } }
before do
going_event.create_invitees(
[{ user_id: target_user.id, status: Invitee.statuses[:going] }],
)
interested_event.create_invitees(
[{ user_id: target_user.id, status: Invitee.statuses[:interested] }],
)
end
it "does not expose interested events to anonymous requests" do
get "/discourse-post-event/events.json", params: events_params
expect(response.status).to eq(200)
expect(response.parsed_body["events"].pluck("id")).to contain_exactly(going_event.id)
end
it "includes interested events for the same authenticated user" do
sign_in(target_user)
get "/discourse-post-event/events.json", params: events_params
expect(response.status).to eq(200)
expect(response.parsed_body["events"].pluck("id")).to contain_exactly(
going_event.id,
interested_event.id,
)
end
it "includes interested events for the owner's user api key feed" do
user_api_key =
Fabricate(
:user_api_key,
user: target_user,
scopes: [
Fabricate.build(:user_api_key_scope, name: "discourse-calendar:events_calendar"),
],
)
get "/discourse-post-event/events.ics",
params: events_params.merge(user_api_key: user_api_key.key)
expect(response.status).to eq(200)
calendar_name =
I18n.t(
"discourse_calendar.calendar_subscriptions.my_events_feed_name",
site_title: SiteSetting.title,
)
expect(response.body).to include("X-WR-CALNAME:#{IcalEncoder.encode(calendar_name)}")
expect(response.body).to include("SUMMARY:Going Event")
expect(response.body).to include("SUMMARY:Interested Event")
end
end
context "when the attending user RSVPed to a recurring event" do
fab!(:target_user, :user)
fab!(:recurring_event) do
Fabricate(
:event,
original_starts_at: 1.day.from_now,
original_ends_at: 1.day.from_now + 1.hour,
recurrence: "every_week",
name: "Weekly Standup",
)
end
before { sign_in(target_user) }
it "returns only the current occurrence when they RSVPed to a single occurrence" do
Invitee.create_attendance!(target_user.id, recurring_event.id, :going, recurring: false)
get "/discourse-post-event/events.json", params: { attending_user: target_user.username }
expect(response.status).to eq(200)
event_json = response.parsed_body["events"].find { |e| e["id"] == recurring_event.id }
expect(event_json["occurrences"].size).to eq(1)
expect(event_json["occurrences"].first["starts_at"]).to eq(event_json["starts_at"])
end
it "returns the whole series when they RSVPed to every occurrence" do
Invitee.create_attendance!(target_user.id, recurring_event.id, :going, recurring: true)
get "/discourse-post-event/events.json", params: { attending_user: target_user.username }
expect(response.status).to eq(200)
event_json = response.parsed_body["events"].find { |e| e["id"] == recurring_event.id }
expect(event_json["occurrences"].size).to be > 1
end
end
end
context "with an all-day event" do
it "returns date-only strings for starts_at and ends_at" do
Fabricate(
:event,
original_starts_at: Time.utc(2026, 3, 12),
original_ends_at: Time.utc(2026, 3, 14),
all_day: true,
)
get "/discourse-post-event/events.json"
expect(response.status).to eq(200)
event = response.parsed_body["events"].first
expect(event["starts_at"]).to eq("2026-03-12")
expect(event["ends_at"]).to eq("2026-03-14")
expect(event["all_day"]).to eq(true)
end
it "serializes recurring no-end occurrences as a single day" do
freeze_time Time.utc(2026, 3, 1, 12)
Fabricate(
:event,
original_starts_at: Time.utc(2026, 2, 1),
all_day: true,
recurrence: "every_week",
)
get "/discourse-post-event/events.json"
expect(response.status).to eq(200)
occurrence = response.parsed_body["events"].first["occurrences"].first
expect(occurrence["ends_at"]).to eq(occurrence["starts_at"])
end
end
context "with an existing post" do
let(:user) { Fabricate(:user, admin: true) }
let(:topic) { Fabricate(:topic, user: user, category: Fabricate(:category)) }
let(:post1) { Fabricate(:post, user: user, topic: topic) }
let(:invitee1) { Fabricate(:user) }
let(:invitee2) { Fabricate(:user) }
context "with an existing event" do
let(:event_1) { Fabricate(:event, post: post1) }
before { sign_in(user) }
context "when updating" do
context "when doing csv bulk invite" do
let(:valid_file) do
file = Tempfile.new("valid.csv")
file.write("bob,going\n")
file.write("sam,interested\n")
file.write("the_foo_bar_group,not_going\n")
file.rewind
file
end
let(:empty_file) do
file = Tempfile.new("invalid.pdf")
file.rewind
file
end
context "when current user can manage the event" do
context "when no file is given" do
it "returns an error" do
post "/discourse-post-event/events/#{event_1.id}/csv-bulk-invite.json"
expect(response.parsed_body["error_type"]).to eq("invalid_parameters")
end
end
context "when an empty file is given" do
it "returns an error" do
post "/discourse-post-event/events/#{event_1.id}/csv-bulk-invite.json",
params: {
file: fixture_file_upload(empty_file),
}
expect(response.status).to eq(422)
end
end
context "when a valid file is given" do
before { Jobs.run_later! }
it "enqueues the job and returns 200" do
expect_enqueued_with(
job: :discourse_post_event_bulk_invite,
args: {
"event_id" => event_1.id,
"invitees" => [
{ "identifier" => "bob", "attendance" => "going" },
{ "identifier" => "sam", "attendance" => "interested" },
{ "identifier" => "the_foo_bar_group", "attendance" => "not_going" },
],
"current_user_id" => user.id,
},
) do
post "/discourse-post-event/events/#{event_1.id}/csv-bulk-invite.json",
params: {
file: fixture_file_upload(valid_file),
}
end
expect(response.status).to eq(200)
end
end
end
context "when current user cant manage the event" do
let(:lurker) { Fabricate(:user) }
before { sign_in(lurker) }
it "returns an error" do
post "/discourse-post-event/events/#{event_1.id}/csv-bulk-invite.json"
expect(response.status).to eq(403)
end
end
end
context "when doing bulk invite" do
context "when current user can manage the event" do
context "when no invitees are given" do
it "returns an error" do
post "/discourse-post-event/events/#{event_1.id}/bulk-invite.json"
expect(response.parsed_body["error_type"]).to eq("invalid_parameters")
end
end
context "when empty invitees are given" do
it "returns an error" do
post "/discourse-post-event/events/#{event_1.id}/bulk-invite.json",
params: {
invitees: [],
}
expect(response.status).to eq(400)
end
end
context "when valid invitees are given" do
before { Jobs.run_later! }
it "enqueues the job and returns 200" do
expect_enqueued_with(
job: :discourse_post_event_bulk_invite,
args: {
"event_id" => event_1.id,
"invitees" => [
{ "identifier" => "bob", "attendance" => "going" },
{ "identifier" => "sam", "attendance" => "interested" },
{ "identifier" => "the_foo_bar_group", "attendance" => "not_going" },
],
"current_user_id" => user.id,
},
) do
post "/discourse-post-event/events/#{event_1.id}/bulk-invite.json",
params: {
invitees: [
{ "identifier" => "bob", "attendance" => "going" },
{ "identifier" => "sam", "attendance" => "interested" },
{ "identifier" => "the_foo_bar_group", "attendance" => "not_going" },
],
}
end
expect(response.status).to eq(200)
end
end
end
context "when current user cant manage the event" do
let(:lurker) { Fabricate(:user) }
before { sign_in(lurker) }
it "returns an error" do
post "/discourse-post-event/events/#{event_1.id}/bulk-invite.json"
expect(response.status).to eq(403)
end
end
end
end
context "when acting user has created the event" do
it "destroys a event" do
expect(event_1.persisted?).to be(true)
messages =
MessageBus.track_publish { delete "/discourse-post-event/events/#{event_1.id}.json" }
expect(messages.count).to eq(1)
message = messages.first
expect(message.channel).to eq("/discourse-post-event/#{event_1.post.topic_id}")
expect(message.data[:id]).to eq(event_1.id)
expect(response.status).to eq(200)
expect(Event).to_not exist(id: event_1.id)
end
end
context "when acting user has not created the event" do
let(:lurker) { Fabricate(:user) }
before { sign_in(lurker) }
it "doesnt destroy the event" do
expect(event_1.persisted?).to be(true)
delete "/discourse-post-event/events/#{event_1.id}.json"
expect(response.status).to eq(403)
expect(Event).to exist(id: event_1.id)
end
end
context "when watching user is not logged" do
before { sign_out }
context "when topic is public" do
it "can see the event" do
get "/discourse-post-event/events/#{event_1.id}.json"
expect(response.status).to eq(200)
end
end
context "when topic is not public" do
before { event_1.post.topic.convert_to_private_message(Discourse.system_user) }
it "cant see the event" do
get "/discourse-post-event/events/#{event_1.id}.json"
expect(response.status).to eq(404)
end
end
end
context "with a private event" do
fab!(:viewer, :user)
fab!(:invitee, :user)
fab!(:restricted_group) do
Fabricate(
:group,
visibility_level: Group.visibility_levels[:owners],
members_visibility_level: Group.visibility_levels[:owners],
).tap { |group| group.add(invitee) }
end
fab!(:private_event_post) do
Fabricate(
:post,
user: Fabricate(:user, admin: true),
topic: Fabricate(:topic, category: Fabricate(:category)),
)
end
fab!(:private_event) do
Fabricate(
:event,
post: private_event_post,
status: DiscoursePostEvent::Event.statuses[:private],
raw_invitees: [restricted_group.name],
)
end
before do
private_event.create_invitees(
[{ user_id: invitee.id, status: Invitee.statuses[:going] }],
)
sign_in(viewer)
end
it "does not serialize invitee details for non-invited viewers who cannot see the invited group" do
get "/discourse-post-event/events/#{private_event.id}.json"
expect(response.status).to eq(200)
event = response.parsed_body["event"]
expect(event).not_to have_key("raw_invitees")
expect(event).not_to have_key("sample_invitees")
expect(event).not_to have_key("stats")
expect(event["should_display_invitees"]).to eq(false)
end
end
context "when filtering by category" do
fab!(:category)
fab!(:subcategory) do
Fabricate(:category, parent_category: category, name: "category subcategory")
end
fab!(:event_1) do
Fabricate(
:event,
original_starts_at: 2.days.from_now,
post: Fabricate(:post, post_number: 1, topic: Fabricate(:topic, category: category)),
)
end
fab!(:event_2) do
Fabricate(
:event,
original_starts_at: 1.day.from_now,
post:
Fabricate(:post, post_number: 1, topic: Fabricate(:topic, category: subcategory)),
)
end
fab!(:event_3) do
Fabricate(
:event,
post: Fabricate(:post, post_number: 1, topic: Fabricate(:topic, category: category)),
original_starts_at: 10.days.ago,
original_ends_at: 9.days.ago,
)
end
it "can filter the event by category" do
get "/discourse-post-event/events.json?category_id=#{category.id}"
expect(response.status).to eq(200)
events = response.parsed_body["events"]
expect(events.length).to eq(2) # Now includes expired event_3
event_ids = events.map { |e| e["id"] }
expect(event_ids).to include(event_1.id)
expect(event_ids).to include(event_3.id)
end
it "includes subcategory events when param provided" do
get "/discourse-post-event/events.json?category_id=#{category.id}&include_subcategories=true"
expect(response.status).to eq(200)
events = response.parsed_body["events"]
expect(events.length).to eq(3) # Now includes expired event_3
expect(events).to match_array(
[
hash_including("id" => event_1.id),
hash_including("id" => event_2.id),
hash_including("id" => event_3.id),
],
)
end
it "limits the number of events returned when limit param provided" do
get "/discourse-post-event/events.json?category_id=#{category.id}&include_subcategories=true&limit=1"
expect(response.status).to eq(200)
events = response.parsed_body["events"]
expect(events.length).to eq(1)
expect(events[0]["id"]).to eq(event_3.id) # Expired event sorts first (NULL starts_at)
end
it "filters events before the provided datetime if before param provided" do
get "/discourse-post-event/events.json?category_id=#{category.id}&include_subcategories=true&before=#{event_2.starts_at}"
expect(response.status).to eq(200)
events = response.parsed_body["events"]
expect(events.length).to eq(1)
expect(events[0]["id"]).to eq(event_3.id)
end
end
end
end
context "with a private event" do
let(:moderator) { Fabricate(:moderator) }
let(:topic) { Fabricate(:topic, user: moderator) }
let(:first_post) { Fabricate(:post, user: moderator, topic: topic) }
let(:private_event) { Fabricate(:event, post: first_post, status: Event.statuses[:private]) }
before { sign_in(moderator) }
context "when bulk inviting via CSV file" do
def csv_file(content)
file = Tempfile.new("invites.csv")
file.write(content)
file.rewind
file
end
it "doesn't invite a private group" do
private_group = Fabricate(:group, visibility_level: Group.visibility_levels[:owners])
file = csv_file("#{private_group.name},going\n")
params = { file: fixture_file_upload(file) }
post "/discourse-post-event/events/#{private_event.id}/csv-bulk-invite.json",
params: params
expect(response.status).to eq(200)
private_event.reload
expect(private_event.raw_invitees).to be_nil
end
it "returns 200 when inviting a non-existent group" do
file = csv_file("non-existent group name,going\n")
params = { file: fixture_file_upload(file) }
post "/discourse-post-event/events/#{private_event.id}/csv-bulk-invite.json",
params: params
expect(response.status).to eq(200)
end
it "doesn't invite a public group with private members" do
public_group_with_private_members =
Fabricate(
:group,
visibility_level: Group.visibility_levels[:public],
members_visibility_level: Group.visibility_levels[:owners],
)
file = csv_file("#{public_group_with_private_members.name},going\n")
params = { file: fixture_file_upload(file) }
post "/discourse-post-event/events/#{private_event.id}/csv-bulk-invite.json",
params: params
expect(response.status).to eq(200)
private_event.reload
expect(private_event.raw_invitees).to be_nil
end
end
context "when doing bulk inviting via UI" do
it "doesn't invite a private group" do
private_group = Fabricate(:group, visibility_level: Group.visibility_levels[:owners])
params = { invitees: [{ "identifier" => private_group.name, "attendance" => "going" }] }
post "/discourse-post-event/events/#{private_event.id}/bulk-invite.json", params: params
expect(response.status).to eq(200)
private_event.reload
expect(private_event.raw_invitees).to be_nil
end
it "returns 200 when inviting a non-existent group" do
params = {
invitees: [{ "identifier" => "non-existent group name", "attendance" => "going" }],
}
post "/discourse-post-event/events/#{private_event.id}/bulk-invite.json", params: params
expect(response.status).to eq(200)
end
it "doesn't invite a public group with private members" do
public_group_with_private_members =
Fabricate(
:group,
visibility_level: Group.visibility_levels[:public],
members_visibility_level: Group.visibility_levels[:owners],
)
params = {
invitees: [
{ "identifier" => public_group_with_private_members.name, "attendance" => "going" },
],
}
post "/discourse-post-event/events/#{private_event.id}/bulk-invite.json", params: params
expect(response.status).to eq(200)
private_event.reload
expect(private_event.raw_invitees).to be_nil
end
end
end
end
describe "bulk invite respects capacity" do
before do
SiteSetting.calendar_enabled = true
SiteSetting.discourse_post_event_enabled = true
end
let(:user) { Fabricate(:user, admin: true) }
let(:topic) { Fabricate(:topic, user: user) }
let(:post1) { Fabricate(:post, user: user, topic: topic) }
let!(:event) { Fabricate(:event, post: post1, max_attendees: 1) }
it "skips creating going when full" do
sign_in(user)
user1 = Fabricate(:user)
user2 = Fabricate(:user)
expect_enqueued_with(
job: :discourse_post_event_bulk_invite,
args: {
"event_id" => event.id,
"invitees" => [
{ "identifier" => user1.username, "attendance" => "going" },
{ "identifier" => user2.username, "attendance" => "going" },
],
"current_user_id" => user.id,
},
) do
post "/discourse-post-event/events/#{event.id}/bulk-invite.json",
params: {
invitees: [
{ "identifier" => user1.username, "attendance" => "going" },
{ "identifier" => user2.username, "attendance" => "going" },
],
}
end
Jobs.run_immediately!
event.reload
expect(event.invitees.with_status(:going).count).to be <= 1
end
end
describe "#show" do
before do
SiteSetting.calendar_enabled = true
SiteSetting.discourse_post_event_enabled = true
end
fab!(:admin_user) { Fabricate(:user, admin: true) }
fab!(:category)
fab!(:topic) { Fabricate(:topic, user: admin_user, category: category) }
fab!(:post_1) { Fabricate(:post, user: admin_user, topic: topic) }
fab!(:chat_channel) { Fabricate(:chat_channel, chatable: category) }
fab!(:event) { Fabricate(:event, post: post_1, chat_enabled: true, chat_channel: chat_channel) }
fab!(:chat_message) do
Fabricate(
:chat_message,
chat_channel: chat_channel,
user: admin_user,
message: "private chat message body",
)
end
before { chat_channel.update!(last_message: chat_message) }
context "when the viewer is anonymous" do
before do
SiteSetting.chat_enabled = true
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
end
it "does not include the chat channel block or last message body" do
get "/discourse-post-event/events/#{event.id}.json"
expect(response.status).to eq(200)
expect(response.parsed_body["event"]).not_to have_key("channel")
expect(response.body).not_to include("private chat message body")
end
end
context "when the viewer cannot join the chat channel" do
fab!(:viewer, :user)
before do
SiteSetting.chat_enabled = true
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:staff]
sign_in(viewer)
end
it "does not include the chat channel block or last message body" do
get "/discourse-post-event/events/#{event.id}.json"
expect(response.status).to eq(200)
expect(response.parsed_body["event"]).not_to have_key("channel")
expect(response.body).not_to include("private chat message body")
end
end
context "when the viewer can join the chat channel" do
before do
SiteSetting.chat_enabled = true
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
sign_in(admin_user)
end
it "includes the chat channel block with the last message body" do
get "/discourse-post-event/events/#{event.id}.json"
expect(response.status).to eq(200)
expect(response.parsed_body["event"]["channel"]).to be_present
expect(response.body).to include("private chat message body")
end
end
end
describe "anonymous access to EventsController" do
before do
SiteSetting.calendar_enabled = true
SiteSetting.discourse_post_event_enabled = true
end
fab!(:admin_user) { Fabricate(:user, admin: true) }
fab!(:topic) { Fabricate(:topic, user: admin_user) }
fab!(:post_1) { Fabricate(:post, user: admin_user, topic: topic) }
fab!(:event) { Fabricate(:event, post: post_1) }
it "requires login for invite" do
post "/discourse-post-event/events/#{event.id}/invite.json"
expect(response.status).to eq(403)
end
it "requires login for destroy" do
delete "/discourse-post-event/events/#{event.id}.json"
expect(response.status).to eq(403)
end
it "requires login for bulk_invite" do
post "/discourse-post-event/events/#{event.id}/bulk-invite.json",
params: {
invitees: [{ "identifier" => "bob", "attendance" => "going" }],
}
expect(response.status).to eq(403)
end
it "requires login for csv_bulk_invite" do
post "/discourse-post-event/events/#{event.id}/csv-bulk-invite.json"
expect(response.status).to eq(403)
end
end
end