mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-27 08:42:09 +08:00
When creating an event with a location, the information was missing from the .ics / google calendar "exports". Ref - meta/t/378672
544 lines
20 KiB
Ruby
Vendored
544 lines
20 KiB
Ruby
Vendored
# 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 "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
|
||
expect(body).to include("BEGIN:VCALENDAR")
|
||
expect(body).to include("END:VCALENDAR")
|
||
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 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
|
||
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 can’t 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 can’t 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 "doesn’t 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 "can’t see the event" do
|
||
get "/discourse-post-event/events/#{event_1.id}.json"
|
||
|
||
expect(response.status).to eq(404)
|
||
end
|
||
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
|
||
end
|