zulip/web/tests/compose_validate.test.cjs
Sahil Batra 54e03f59e1 compose_validate: Fix code to check permission for resolving topics.
We only checked realm-level permission to resolve topics to decide
whether to show "Unresolve" button in the resolved topic warning
banner. This commit fixes it to check stream-level permission as
well.
2026-04-24 09:31:20 -07:00

1013 lines
37 KiB
JavaScript

"use strict";
const assert = require("node:assert/strict");
const {mock_banners} = require("./lib/compose_banner.cjs");
const {FakeComposeBox} = require("./lib/compose_helpers.cjs");
const {make_user_group} = require("./lib/example_group.cjs");
const {make_realm} = require("./lib/example_realm.cjs");
const {make_stream} = require("./lib/example_stream.cjs");
const {make_cross_realm_bot, make_user, Role} = require("./lib/example_user.cjs");
const {$t} = require("./lib/i18n.cjs");
const {mock_channel_get} = require("./lib/mock_channel.cjs");
const {mock_esm, zrequire} = require("./lib/namespace.cjs");
const {run_test, noop} = require("./lib/test.cjs");
const blueslip = require("./lib/zblueslip.cjs");
const $ = require("./lib/zjquery.cjs");
const channel = mock_esm("../src/channel");
const compose_banner = zrequire("compose_banner");
const compose_pm_pill = zrequire("compose_pm_pill");
const compose_state = zrequire("compose_state");
const compose_validate = zrequire("compose_validate");
const peer_data = zrequire("peer_data");
const people = zrequire("people");
const resolved_topic = zrequire("resolved_topic");
const {set_current_user, set_realm} = zrequire("state_data");
const stream_data = zrequire("stream_data");
const compose_recipient = zrequire("/compose_recipient");
const user_groups = zrequire("user_groups");
const {initialize_user_settings} = zrequire("user_settings");
mock_esm("../src/group_permission_settings", {
get_group_permission_setting_config: () => ({
allow_everyone_group: true,
}),
});
const REALM_EMPTY_TOPIC_DISPLAY_NAME = "general chat";
const realm = make_realm({
realm_empty_topic_display_name: REALM_EMPTY_TOPIC_DISPLAY_NAME,
realm_topics_policy: "allow_empty_topic",
});
set_realm(realm);
const current_user = {};
set_current_user(current_user);
const user_settings = {default_language: "en"};
initialize_user_settings({user_settings});
const me = make_user({
email: "me@example.com",
user_id: 30,
full_name: "Me Myself",
date_joined: new Date(),
});
const alice = make_user({
email: "alice@example.com",
user_id: 31,
full_name: "Alice",
});
const bob = make_user({
email: "bob@example.com",
user_id: 32,
full_name: "Bob",
role: Role.ADMINISTRATOR,
});
const guest = make_user({
email: "guest@example.com",
user_id: 33,
full_name: "Guest",
role: Role.GUEST,
});
const moderator = make_user({
email: "moderator@example.com",
user_id: 34,
full_name: "Charlie",
role: Role.MODERATOR,
});
const social_sub = make_stream({
stream_id: 101,
name: "social",
subscribed: true,
});
stream_data.add_sub_for_tests(social_sub);
people.add_active_user(me);
people.initialize_current_user(me.user_id);
people.add_active_user(alice);
people.add_active_user(bob);
people.add_active_user(guest);
const welcome_bot = make_cross_realm_bot({
email: "welcome-bot@example.com",
user_id: 4,
full_name: "Welcome Bot",
});
people.add_cross_realm_user(welcome_bot);
const nobody = make_user_group({
name: "role:nobody",
id: 1,
members: new Set(),
is_system_group: true,
direct_subgroup_ids: new Set(),
});
const everyone = make_user_group({
name: "role:everyone",
id: 2,
members: new Set([30, 33]),
is_system_group: true,
direct_subgroup_ids: new Set([5]),
});
const admin = make_user_group({
name: "role:administrators",
id: 3,
members: new Set([32]),
is_system_group: true,
direct_subgroup_ids: new Set(),
});
const moderators = make_user_group({
name: "role:moderators",
id: 4,
members: new Set([34]),
is_system_group: true,
direct_subgroup_ids: new Set([3]),
});
const members = make_user_group({
name: "role:members",
id: 5,
members: new Set([31]),
is_system_group: true,
direct_subgroup_ids: new Set([4]),
});
user_groups.initialize({realm_user_groups: [nobody, everyone, admin, moderators, members]});
function test_ui(label, f) {
run_test(label, (helpers) => {
$("textarea#compose-textarea").val("some message");
return f(helpers);
});
}
function stub_message_row($textarea) {
const $stub = $.set_results("message_row_stub", []);
$textarea.set_closest_results(".message_row", $stub);
}
function initialize_pm_pill(mock_template) {
$.clear_all_elements();
$("#compose-send-button").trigger("focus");
$("#compose-send-button .loader").hide();
const $pm_pill_container = $.create("fake-pm-pill-container");
$("#private_message_recipient").set_parent($pm_pill_container);
$pm_pill_container.set_find_results(".input", $("#private_message_recipient"));
$("#private_message_recipient")[0].before = noop;
compose_pm_pill.initialize({
on_pill_create_or_remove: compose_recipient.update_compose_area_placeholder_text,
});
mock_template("input_pill.hbs", false, () => "<div>pill-html</div>");
mock_banners();
}
test_ui("validate", ({mock_template, override}) => {
function add_content_to_compose_box() {
$("textarea#compose-textarea").val("foobarfoobar");
}
override(realm, "realm_can_access_all_users_group", everyone.id);
// test validating direct messages
compose_state.set_message_type("private");
initialize_pm_pill(mock_template);
add_content_to_compose_box();
let pm_recipient_error_rendered = false;
override(realm, "realm_direct_message_permission_group", everyone.id);
override(realm, "realm_direct_message_initiator_group", everyone.id);
mock_template("compose_banner/compose_banner.hbs", false, (data) => {
assert.equal(data.classname, compose_banner.CLASSNAMES.missing_private_message_recipient);
assert.equal(data.banner_text, compose_validate.NO_PRIVATE_RECIPIENT_ERROR_MESSAGE);
pm_recipient_error_rendered = true;
return "<banner-stub>";
});
$("#send_message_form").set_find_results(".message-textarea", $("textarea#compose-textarea"));
assert.ok(!compose_validate.validate());
assert.ok(pm_recipient_error_rendered);
// Textarea should not be disabled for missing recipient.
assert.ok(!$("textarea#compose-textarea").prop("disabled"));
pm_recipient_error_rendered = false;
people.add_active_user(bob);
compose_state.set_private_message_recipient_ids([bob.user_id]);
assert.ok(compose_validate.validate());
assert.ok(!pm_recipient_error_rendered);
assert.ok(!$("textarea#compose-textarea").prop("disabled"));
override(realm, "realm_direct_message_initiator_group", admin.id);
assert.ok(compose_validate.validate());
assert.ok(!pm_recipient_error_rendered);
assert.ok(!$("textarea#compose-textarea").prop("disabled"));
override(realm, "realm_direct_message_permission_group", admin.id);
assert.ok(compose_validate.validate());
assert.ok(!pm_recipient_error_rendered);
assert.ok(!$("textarea#compose-textarea").prop("disabled"));
override(realm, "realm_direct_message_initiator_group", everyone.id);
override(realm, "realm_direct_message_permission_group", everyone.id);
people.deactivate(bob);
let deactivated_user_error_rendered = false;
mock_template("compose_banner/compose_banner.hbs", false, (data) => {
assert.equal(data.classname, compose_banner.CLASSNAMES.deactivated_user);
assert.equal(
data.banner_text,
$t({defaultMessage: "You cannot send messages to deactivated users."}),
);
deactivated_user_error_rendered = true;
return "<banner-stub>";
});
assert.ok(!compose_validate.validate());
assert.ok(deactivated_user_error_rendered);
// Textarea should be disabled for deactivated user.
assert.ok($("textarea#compose-textarea").prop("disabled"));
bob.is_deleted = true;
let deleted_user_error_rendered = false;
mock_template("compose_banner/compose_banner.hbs", false, (data) => {
assert.equal(data.classname, compose_banner.CLASSNAMES.deactivated_user);
assert.equal(
data.banner_text,
$t({defaultMessage: "You cannot send messages to deleted users."}),
);
deleted_user_error_rendered = true;
return "<banner-stub>";
});
assert.ok(!compose_validate.validate());
assert.ok(deleted_user_error_rendered);
bob.is_deleted = false;
initialize_pm_pill(mock_template);
add_content_to_compose_box();
compose_state.set_private_message_recipient_ids([welcome_bot.user_id]);
$("#send_message_form").set_find_results(".message-textarea", $("textarea#compose-textarea"));
assert.ok(compose_validate.validate());
assert.ok(!$("textarea#compose-textarea").prop("disabled"));
// For this first block, we should fail due to empty compose.
initialize_pm_pill(mock_template);
compose_state.set_private_message_recipient_ids([welcome_bot.user_id]);
$("textarea#compose-textarea").removeClass("invalid");
assert.ok(!compose_validate.validate());
assert.ok(!$("#compose-send-button .loader").visible());
assert.ok($("textarea#compose-textarea").hasClass("invalid"));
assert.ok(!$("textarea#compose-textarea").prop("disabled"));
// Now add content to compose.
add_content_to_compose_box();
$("#send_message_form").set_find_results(".message-textarea", $("textarea#compose-textarea"));
$("textarea#compose-textarea").addClass("invalid");
assert.ok(compose_validate.validate());
assert.ok(!$("textarea#compose-textarea").hasClass("invalid"));
assert.ok(!$("textarea#compose-textarea").prop("disabled"));
initialize_pm_pill(mock_template);
add_content_to_compose_box();
// test validating stream messages
compose_state.set_message_type("stream");
compose_state.set_stream_id("");
let empty_stream_error_rendered = false;
mock_template("compose_banner/compose_banner.hbs", false, (data) => {
assert.equal(data.classname, compose_banner.CLASSNAMES.missing_stream);
assert.equal(data.banner_text, compose_validate.NO_CHANNEL_SELECTED_ERROR_MESSAGE);
empty_stream_error_rendered = true;
return "<banner-stub>";
});
$("#send_message_form").set_find_results(".message-textarea", $("textarea#compose-textarea"));
assert.ok(!compose_validate.validate());
assert.ok(empty_stream_error_rendered);
const denmark = {
stream_id: 100,
name: "Denmark",
topics_policy: "inherit",
};
stream_data.add_sub_for_tests(denmark);
compose_state.set_stream_id(denmark.stream_id);
override(realm, "realm_topics_policy", "disable_empty_topic");
let missing_topic_error_rendered = false;
mock_template("compose_banner/compose_banner.hbs", false, (data) => {
assert.equal(data.classname, compose_banner.CLASSNAMES.topic_missing);
missing_topic_error_rendered = true;
return "<banner-stub>";
});
for (const topic_name of ["", "(no topic)", `translated: ${REALM_EMPTY_TOPIC_DISPLAY_NAME}`]) {
compose_state.topic(topic_name);
missing_topic_error_rendered = false;
assert.ok(!compose_validate.validate());
assert.ok(missing_topic_error_rendered);
assert.ok(!$("textarea#compose-textarea").prop("disabled"));
}
// Test textarea disabled when DMs are disabled in the org.
compose_state.set_message_type("private");
compose_state.set_private_message_recipient_ids([bob.user_id]);
override(realm, "realm_direct_message_permission_group", nobody.id);
let dm_disabled_error_rendered = false;
mock_template("compose_banner/cannot_send_direct_message_error.hbs", false, (data) => {
assert.equal(data.classname, compose_banner.CLASSNAMES.cannot_send_direct_message);
dm_disabled_error_rendered = true;
return "<banner-stub>";
});
$.reset_selector(`#compose_banners .cannot_send_direct_message`);
$.set_results(`#compose_banners .cannot_send_direct_message`, []);
assert.ok(!compose_validate.validate());
assert.ok(dm_disabled_error_rendered);
assert.ok($("textarea#compose-textarea").prop("disabled"));
// Switching to stream message should re-enable textarea.
compose_state.set_message_type("stream");
assert.ok(!compose_validate.validate());
assert.ok(!$("textarea#compose-textarea").prop("disabled"));
});
test_ui("test_stream_wildcard_mention_allowed", ({override}) => {
override(current_user, "user_id", me.user_id);
// First, check for large streams (>15 subscribers) where the wildcard mention
// policy matters. Set 16 subscribers so is_recipient_large_stream() returns true.
const large_stream = {stream_id: 100, name: "Denmark"};
stream_data.add_sub_for_tests(large_stream);
compose_state.set_stream_id(large_stream.stream_id);
peer_data.set_subscribers(
large_stream.stream_id,
Array.from({length: 16}, (_, i) => i + 1),
);
override(realm, "realm_can_mention_many_users_group", everyone.id);
override(current_user, "user_id", guest.user_id);
assert.ok(compose_validate.stream_wildcard_mention_allowed());
override(realm, "realm_can_mention_many_users_group", nobody.id);
override(current_user, "user_id", bob.user_id);
assert.ok(!compose_validate.stream_wildcard_mention_allowed());
override(realm, "realm_can_mention_many_users_group", members.id);
override(current_user, "user_id", guest.user_id);
assert.ok(!compose_validate.stream_wildcard_mention_allowed());
override(current_user, "user_id", alice.user_id);
assert.ok(compose_validate.stream_wildcard_mention_allowed());
override(realm, "realm_can_mention_many_users_group", moderators.id);
assert.ok(!compose_validate.stream_wildcard_mention_allowed());
override(current_user, "user_id", moderator.user_id);
assert.ok(compose_validate.stream_wildcard_mention_allowed());
override(realm, "realm_can_mention_many_users_group", admin.id);
override(current_user, "user_id", moderator.user_id);
assert.ok(!compose_validate.stream_wildcard_mention_allowed());
// TODO: Add a by_admins_only case when we implement stream-level administrators.
override(current_user, "user_id", bob.user_id);
assert.ok(compose_validate.stream_wildcard_mention_allowed());
});
test_ui("validate_stream_message", ({override, mock_template}) => {
// This test is in kind of continuation to test_validate but since it is
// primarily used to get coverage over functions called from validate()
// we are separating it up in different test. Though their relative position
// of execution should not be changed.
mock_banners();
override(current_user, "user_id", me.user_id);
override(realm, "realm_topics_policy", "allow_empty_topic");
const special_sub = {
stream_id: 101,
name: "special",
subscribed: true,
can_send_message_group: everyone.id,
topics_policy: "inherit",
can_create_topic_group: everyone.id,
};
stream_data.add_sub_for_tests(special_sub);
compose_state.set_stream_id(special_sub.stream_id);
$("#send_message_form").set_find_results(".message-textarea", $("textarea#compose-textarea"));
assert.ok(compose_validate.validate());
assert.ok(!$("#compose-all-everyone").visible());
peer_data.set_subscribers(
special_sub.stream_id,
Array.from({length: 16}, (_, i) => i + 1),
);
let stream_wildcard_warning_rendered = false;
$.set_results("#compose_banner_area .wildcard_warning", []);
mock_template("compose_banner/stream_wildcard_warning.hbs", false, (data) => {
stream_wildcard_warning_rendered = true;
assert.equal(data.subscriber_count, 16);
return "<banner-stub>";
});
override(realm, "realm_can_mention_many_users_group", everyone.id);
compose_state.message_content("Hey @**all**");
assert.ok(!compose_validate.validate());
assert.ok(stream_wildcard_warning_rendered);
let wildcards_not_allowed_rendered = false;
mock_template("compose_banner/wildcard_mention_not_allowed_error.hbs", false, (data) => {
assert.equal(data.classname, compose_banner.CLASSNAMES.wildcards_not_allowed);
assert.equal(data.wildcard_mention_string, "all");
wildcards_not_allowed_rendered = true;
return "<banner-stub>";
});
override(realm, "realm_can_mention_many_users_group", admin.id);
assert.ok(!compose_validate.validate());
assert.ok(wildcards_not_allowed_rendered);
});
test_ui("test_stream_posting_permission", ({mock_template, override}) => {
mock_banners();
override(current_user, "user_id", 30);
const sub_stream_102 = {
stream_id: 102,
name: "stream102",
subscribed: true,
can_send_message_group: admin.id,
can_create_topic_group: everyone.id,
can_add_subscribers_group: everyone.id,
};
stream_data.add_sub_for_tests(sub_stream_102);
compose_state.topic("topic102");
compose_state.set_stream_id(sub_stream_102.stream_id);
sub_stream_102.subscribed = false;
let user_not_subscribed_rendered = false;
mock_template("compose_banner/compose_banner.hbs", false, (data) => {
assert.equal(data.classname, compose_banner.CLASSNAMES.user_not_subscribed);
assert.equal(
data.banner_text,
$t({
defaultMessage:
"You're not subscribed to this channel. You will not be notified if other users reply to your message.",
}),
);
user_not_subscribed_rendered = true;
return "<banner-stub>";
});
assert.ok(!compose_validate.validate());
assert.ok(user_not_subscribed_rendered);
sub_stream_102.subscribed = true;
let banner_rendered = false;
mock_template("compose_banner/compose_banner.hbs", false, (data) => {
assert.equal(data.classname, compose_banner.CLASSNAMES.no_post_permissions);
assert.equal(
data.banner_text,
$t({
defaultMessage: "You do not have permission to post in this channel.",
}),
);
banner_rendered = true;
return "<banner-stub>";
});
$("#send_message_form").set_find_results(".message-textarea", $("textarea#compose-textarea"));
assert.ok(!compose_validate.validate());
assert.ok(banner_rendered);
override(current_user, "user_id", 32);
banner_rendered = false;
assert.ok(compose_validate.validate());
assert.ok(!banner_rendered);
sub_stream_102.can_send_message_group = everyone.id;
override(current_user, "user_id", 30);
banner_rendered = false;
assert.ok(compose_validate.validate());
assert.ok(!banner_rendered);
// Reset error message.
compose_state.set_stream_id(social_sub.stream_id);
const anonymous_setting_group = {
direct_subgroups: [admin.id],
direct_members: [31],
};
sub_stream_102.can_send_message_group = anonymous_setting_group;
compose_state.topic("topic102");
compose_state.set_stream_id(sub_stream_102.stream_id);
override(current_user, "user_id", 30);
banner_rendered = false;
assert.ok(!compose_validate.validate());
assert.ok(banner_rendered);
override(current_user, "user_id", 31);
banner_rendered = false;
assert.ok(compose_validate.validate());
assert.ok(!banner_rendered);
override(current_user, "user_id", 32);
banner_rendered = false;
assert.ok(compose_validate.validate());
assert.ok(!banner_rendered);
});
test_ui("test_check_overflow_text", ({override, override_rewire}) => {
const fake_compose_box = new FakeComposeBox();
override_rewire(compose_validate, "validate_and_update_send_button_status", noop);
override(realm, "max_message_length", 10000);
// RED
{
fake_compose_box.set_textarea_val("a".repeat(10005));
compose_validate.check_overflow_text(fake_compose_box.$send_message_form);
fake_compose_box.assert_message_size_is_over_the_limit("-5\n");
}
// ORANGE
{
fake_compose_box.set_textarea_val("a".repeat(9100));
compose_validate.check_overflow_text(fake_compose_box.$send_message_form);
fake_compose_box.assert_message_size_is_under_the_limit("900\n");
}
// ALL CLEAR
{
fake_compose_box.set_textarea_val("a".repeat(9100 - 1));
compose_validate.check_overflow_text(fake_compose_box.$send_message_form);
fake_compose_box.assert_message_size_is_under_the_limit();
}
});
test_ui("needs_subscribe_warning", async () => {
const invalid_user_id = 999;
const test_bot = {
full_name: "Test Bot",
email: "test-bot@example.com",
user_id: 135,
is_bot: true,
};
people.add_active_user(test_bot);
const sub = {
stream_id: 110,
name: "stream",
};
stream_data.add_sub_for_tests(sub);
peer_data.set_subscribers(sub.stream_id, [bob.user_id, me.user_id]);
blueslip.expect("error", "Unknown user_id in maybe_get_user_by_id");
// Test with an invalid user id.
assert.equal(
await compose_validate.needs_subscribe_warning(invalid_user_id, sub.stream_id),
false,
);
// Test with bot user.
assert.equal(
await compose_validate.needs_subscribe_warning(test_bot.user_id, sub.stream_id),
false,
);
// Test when user is subscribed to the stream.
assert.equal(await compose_validate.needs_subscribe_warning(bob.user_id, sub.stream_id), false);
peer_data.remove_subscriber(sub.stream_id, bob.user_id);
// Test when the user is not subscribed.
assert.equal(await compose_validate.needs_subscribe_warning(bob.user_id, sub.stream_id), true);
});
test_ui("warn_if_private_stream_is_linked", async ({mock_template}) => {
mock_banners();
const $textarea = $("<textarea>").attr("id", "compose-textarea");
stub_message_row($textarea);
const test_sub = {
name: compose_state.stream_name(),
stream_id: 99,
};
stream_data.add_sub_for_tests(test_sub);
peer_data.set_subscribers(test_sub.stream_id, [1, 2]);
const denmark = {
stream_id: 100,
name: "Denmark",
};
stream_data.add_sub_for_tests(denmark);
peer_data.set_subscribers(denmark.stream_id, [1, 2, 3]);
let banner_rendered = false;
mock_template("compose_banner/private_stream_warning.hbs", false, (data) => {
assert.equal(data.classname, compose_banner.CLASSNAMES.private_stream_warning);
assert.equal(data.channel_name, "Denmark");
banner_rendered = true;
return "<banner-stub>";
});
async function test_noop_case(invite_only) {
banner_rendered = false;
compose_state.set_message_type("stream");
denmark.invite_only = invite_only;
await compose_validate.warn_if_private_stream_is_linked(denmark, $textarea);
assert.ok(!banner_rendered);
}
compose_state.set_selected_recipient_id(undefined);
await test_noop_case(false);
// invite_only=true and current compose stream subscribers are a subset
// of mentioned_stream subscribers.
await test_noop_case(true);
$("#compose_private").hide();
compose_state.set_message_type("stream");
// Not everyone is subscribed to secret_stream in denmark, so the
// warning is rendered.
compose_state.set_selected_recipient_id(denmark.stream_id);
const secret_stream = {
invite_only: true,
name: "Denmark",
stream_id: 22,
};
stream_data.add_sub_for_tests(secret_stream);
peer_data.set_subscribers(secret_stream.stream_id, []);
banner_rendered = false;
const $banner_container = $("#compose_banners");
$banner_container.set_find_results(".private_stream_warning", []);
await compose_validate.warn_if_private_stream_is_linked(secret_stream, $textarea);
assert.ok(banner_rendered);
// Simulate that the row was added to the DOM.
const $warning_row = $("#compose_banners .private_stream_warning");
$warning_row.attr("data-stream-id", "22");
// Now try to mention the same stream again. The template should
// not render.
banner_rendered = false;
$banner_container.set_find_results(".private_stream_warning", $warning_row);
await compose_validate.warn_if_private_stream_is_linked(secret_stream, $textarea);
assert.ok(!banner_rendered);
});
test_ui("warn_if_mentioning_unsubscribed_user", async ({override, mock_template}) => {
mock_banners();
const $textarea = $("<textarea>").attr("id", "compose-textarea");
stub_message_row($textarea);
compose_state.set_stream_id("");
override(realm, "realm_can_add_subscribers_group", everyone.id);
let mentioned_details = {
user: {
email: "foo@bar.com",
},
};
let new_banner_rendered = false;
mock_template("compose_banner/not_subscribed_warning.hbs", false, (data) => {
assert.equal(data.classname, compose_banner.CLASSNAMES.recipient_not_subscribed);
assert.equal(data.user_id, 34);
assert.equal(data.stream_id, 111);
assert.equal(data.name, "Foo Barson");
new_banner_rendered = true;
return "<banner-stub>";
});
async function test_noop_case(is_private, type) {
new_banner_rendered = false;
const msg_type = is_private ? "private" : "stream";
compose_state.set_message_type(msg_type);
mentioned_details.type = type;
await compose_validate.warn_if_mentioning_unsubscribed_user(mentioned_details, $textarea);
assert.ok(!new_banner_rendered);
}
await test_noop_case(true, "user");
await test_noop_case(false, "broadcast");
$("#compose_invite_users").hide();
compose_state.set_message_type("stream");
// Test with empty stream name in compose box. It should return noop.
new_banner_rendered = false;
assert.equal(compose_state.stream_name(), "");
await compose_validate.warn_if_mentioning_unsubscribed_user(mentioned_details, $textarea);
assert.ok(!new_banner_rendered);
const sub = {
stream_id: 111,
name: "random",
can_add_subscribers_group: admin.id,
can_administer_channel_group: admin.id,
can_subscribe_group: admin.id,
};
stream_data.add_sub_for_tests(sub);
compose_state.set_stream_id(sub.stream_id);
// Test with invalid stream in compose box. It should return noop.
new_banner_rendered = false;
await compose_validate.warn_if_mentioning_unsubscribed_user(mentioned_details, $textarea);
assert.ok(!new_banner_rendered);
// Test mentioning a user that should get a warning.
mentioned_details = {
type: "user",
user: {
email: "foo@bar.com",
user_id: 34,
full_name: "Foo Barson",
},
};
people.add_active_user(mentioned_details.user);
new_banner_rendered = false;
const $banner_container = $("#compose_banners");
$banner_container.set_find_results(".recipient_not_subscribed", []);
mock_channel_get(channel, (opts) => {
assert.equal(opts.url, `/json/streams/${sub.stream_id}/members`);
return opts.success({
subscribers: [],
});
});
await compose_validate.warn_if_mentioning_unsubscribed_user(mentioned_details, $textarea);
assert.ok(new_banner_rendered);
// Simulate that the row was added to the DOM.
const $warning_row = $("#compose_banners .recipient_not_subscribed");
$warning_row.attr("data-user-id", "34");
$warning_row.attr("data-stream-id", "111");
// Now try to mention the same person again. The template should
// not render.
new_banner_rendered = false;
$banner_container.set_find_results(".recipient_not_subscribed", $warning_row);
await compose_validate.warn_if_mentioning_unsubscribed_user(mentioned_details, $textarea);
assert.ok(!new_banner_rendered);
});
test_ui("maybe_clear_stale_recipient_not_subscribed_warnings", () => {
const $textarea = $("<textarea>").attr("id", "compose-textarea");
stub_message_row($textarea);
const $banner_container = $("#compose_banners");
// Using a shared factory so the removal callback has a single
// source location — coverage is satisfied as long as any banner
// is actually removed across all test cases.
const removed_banners = new Set();
function make_banner(name, user_id_str) {
removed_banners.delete(name);
const $banner = $.create(name);
if (user_id_str !== undefined) {
$banner.attr("data-user-id", user_id_str);
}
$banner[0].remove = () => {
removed_banners.add(name);
};
return $banner;
}
// No banners present: function is a no-op.
$banner_container.set_find_results(".recipient_not_subscribed", []);
$textarea.val("text without any mention");
compose_validate.maybe_clear_stale_recipient_not_subscribed_warnings($textarea);
// Banner preserved when canonical @**Name** mention is present.
{
const $banner = make_banner("canonical", String(alice.user_id));
const mention = people.get_mention_syntax(alice.full_name, alice.user_id, false);
$banner_container.set_find_results(".recipient_not_subscribed", $banner);
$textarea.val(`Hello ${mention} here.`);
compose_validate.maybe_clear_stale_recipient_not_subscribed_warnings($textarea);
assert.ok(!removed_banners.has("canonical"));
}
// Banner removed when mention is deleted from compose text.
{
const $banner = make_banner("deleted", String(alice.user_id));
$banner_container.set_find_results(".recipient_not_subscribed", $banner);
$textarea.val("Hello, how are you?");
compose_validate.maybe_clear_stale_recipient_not_subscribed_warnings($textarea);
assert.ok(removed_banners.has("deleted"));
}
// Banner preserved for @**|user_id** form.
{
const $banner = make_banner("id-only", String(alice.user_id));
$banner_container.set_find_results(".recipient_not_subscribed", $banner);
$textarea.val(`Hello @**|${alice.user_id}** how are you?`);
compose_validate.maybe_clear_stale_recipient_not_subscribed_warnings($textarea);
assert.ok(!removed_banners.has("id-only"));
}
// Banner preserved for @**Name|user_id** form.
{
const $banner = make_banner("name-and-id", String(alice.user_id));
$banner_container.set_find_results(".recipient_not_subscribed", $banner);
$textarea.val(`Hello @**${alice.full_name}|${alice.user_id}** how are you?`);
compose_validate.maybe_clear_stale_recipient_not_subscribed_warnings($textarea);
assert.ok(!removed_banners.has("name-and-id"));
}
// Banner removed when only a silent mention is present, since
// the banner is not displayed for silent mentions.
{
const silent_mention = people.get_mention_syntax(alice.full_name, alice.user_id, true);
const $banner = make_banner("silent-only", String(alice.user_id));
$banner_container.set_find_results(".recipient_not_subscribed", $banner);
$textarea.val(`Hello ${silent_mention} here.`);
compose_validate.maybe_clear_stale_recipient_not_subscribed_warnings($textarea);
assert.ok(removed_banners.has("silent-only"));
}
// Banner removed when compose text is empty.
{
const $banner = make_banner("empty-text", String(alice.user_id));
$banner_container.set_find_results(".recipient_not_subscribed", $banner);
$textarea.val("");
compose_validate.maybe_clear_stale_recipient_not_subscribed_warnings($textarea);
assert.ok(removed_banners.has("empty-text"));
}
// Banner removed when mention syntax is incomplete.
{
const $banner = make_banner("incomplete", String(alice.user_id));
$banner_container.set_find_results(".recipient_not_subscribed", $banner);
$textarea.val(`Hello @**${alice.full_name} here.`);
compose_validate.maybe_clear_stale_recipient_not_subscribed_warnings($textarea);
assert.ok(removed_banners.has("incomplete"));
}
// Banner removed for unknown user_id.
{
const $banner = make_banner("unknown-user", "99999");
$banner_container.set_find_results(".recipient_not_subscribed", $banner);
$textarea.val("No mention here.");
compose_validate.maybe_clear_stale_recipient_not_subscribed_warnings($textarea);
assert.ok(removed_banners.has("unknown-user"));
}
// Banner removed when data-user-id attribute is missing.
{
const $banner = make_banner("no-user-id", undefined);
$banner_container.set_find_results(".recipient_not_subscribed", $banner);
$textarea.val("Some text.");
compose_validate.maybe_clear_stale_recipient_not_subscribed_warnings($textarea);
assert.ok(removed_banners.has("no-user-id"));
}
});
test_ui("test warn_if_topic_resolved", ({override, mock_template}) => {
mock_banners();
$.reset_selector("#compose_banners .topic_resolved");
$.set_results("#compose_banners .topic_resolved", []);
override(realm, "realm_can_resolve_topics_group", everyone.id);
let error_shown = false;
mock_template("compose_banner/compose_banner.hbs", false, (data) => {
assert.equal(data.classname, compose_banner.CLASSNAMES.topic_resolved);
assert.equal(
data.banner_text,
$t({
defaultMessage:
"You are sending a message to a resolved topic. You can send as-is or unresolve the topic first.",
}),
);
error_shown = true;
return "<banner-stub>";
});
const sub = {
stream_id: 111,
name: "random",
can_administer_channel_group: nobody.id,
can_move_messages_out_of_channel_group: nobody.id,
can_move_messages_within_channel_group: nobody.id,
can_resolve_topics_group: nobody.id,
};
stream_data.add_sub_for_tests(sub);
compose_state.set_message_type("stream");
compose_state.set_stream_id("");
compose_state.topic(resolved_topic.resolve_name("hello"));
compose_state.message_content("content");
error_shown = false;
compose_validate.warn_if_topic_resolved(true);
assert.ok(!error_shown);
compose_state.set_stream_id(sub.stream_id);
// Show the warning now as stream also exists
error_shown = false;
compose_validate.warn_if_topic_resolved(true);
assert.ok(error_shown);
// We reset the state to be able to show the banner again
compose_state.set_recipient_viewed_topic_resolved_banner(false);
// Call it again with false; this should do the same thing.
error_shown = false;
compose_validate.warn_if_topic_resolved(false);
assert.ok(error_shown);
// Call the func again. This should not show the error because
// we have already shown the error once for this topic.
error_shown = false;
compose_validate.warn_if_topic_resolved(false);
assert.ok(!error_shown);
compose_state.topic("hello");
// The warning will be cleared now
error_shown = false;
compose_validate.warn_if_topic_resolved(true);
assert.ok(!error_shown);
// Calling with false won't do anything.
error_shown = false;
compose_validate.warn_if_topic_resolved(false);
assert.ok(!error_shown);
});
test_ui("test_warn_if_guest_in_dm_recipient", ({mock_template, override}) => {
let is_active = false;
override(realm, "realm_can_access_all_users_group", everyone.id);
mock_template("compose_banner/guest_in_dm_recipient_warning.hbs", false, (data) => {
assert.equal(data.classname, compose_banner.CLASSNAMES.guest_in_dm_recipient_warning);
assert.equal(
data.banner_text,
$t({defaultMessage: "Guest is a guest in this organization."}),
);
is_active = true;
return "<banner-stub>";
});
compose_state.set_message_type("private");
initialize_pm_pill(mock_template);
compose_state.set_private_message_recipient_ids([guest.user_id]);
const classname = compose_banner.CLASSNAMES.guest_in_dm_recipient_warning;
let $banner = $(`#compose_banners .${CSS.escape(classname)}`);
// if setting is disabled, remove warning if exists
realm.realm_enable_guest_user_dm_warning = false;
compose_validate.warn_if_guest_in_dm_recipient();
assert.ok(!is_active);
// to show warning for guest emails, banner should be created
realm.realm_enable_guest_user_dm_warning = true;
$.reset_selector(`#compose_banners .${CSS.escape(classname)}`);
$banner = $.set_results(`#compose_banners .${CSS.escape(classname)}`, []);
compose_validate.warn_if_guest_in_dm_recipient();
assert.ok(is_active);
assert.deepEqual(compose_state.get_recipient_guest_ids_for_dm_warning(), [33]);
// don't show warning for same guests if user closed the banner.
is_active = false;
compose_validate.warn_if_guest_in_dm_recipient();
assert.ok(!is_active);
// on modifying the guest recipient, update banner if already shown.
is_active = true;
const new_guest = {
email: "new_guest@example.com",
user_id: 34,
full_name: "New Guest",
is_guest: true,
};
people.add_active_user(new_guest);
initialize_pm_pill(mock_template);
compose_state.set_private_message_recipient_ids([guest.user_id, new_guest.user_id]);
$.reset_selector(`#compose_banners .${CSS.escape(classname)}`);
$banner = $(`#compose_banners .${CSS.escape(classname)}`);
const $banner_content = $(`#compose_banners .${CSS.escape(classname)} .banner_content`);
$banner.set_find_results(".banner_content", $banner_content);
compose_validate.warn_if_guest_in_dm_recipient();
assert.equal(
$banner_content.text(),
$t({defaultMessage: "Guest and New Guest are guests in this organization."}),
);
assert.deepEqual(compose_state.get_recipient_guest_ids_for_dm_warning(), [33, 34]);
});