"use strict"; const assert = require("node:assert/strict"); const {parseISO} = require("date-fns"); const _ = require("lodash"); const {make_user_group} = require("./lib/example_group.cjs"); const {make_realm} = require("./lib/example_realm.cjs"); const {make_bot, make_cross_realm_bot, make_user, Role} = require("./lib/example_user.cjs"); const {$t} = require("./lib/i18n.cjs"); const {clock, mock_esm, zrequire, set_global} = require("./lib/namespace.cjs"); const {run_test, noop} = require("./lib/test.cjs"); const blueslip = require("./lib/zblueslip.cjs"); const {page_params} = require("./lib/zpage_params.cjs"); const message_user_ids = mock_esm("../src/message_user_ids"); const settings_data = mock_esm("../src/settings_data", { user_can_access_all_other_users: () => true, }); mock_esm("../src/server_events_state", { has_received_first_events_response: () => true, }); const channel = mock_esm("../src/channel"); let additional_calls_before_set_timeout = noop; set_global("setTimeout", (func) => { additional_calls_before_set_timeout(); func(); }); const muted_users = zrequire("muted_users"); const people = zrequire("people"); const retry_backoff = zrequire("retry_backoff"); const settings_config = zrequire("../src/settings_config.ts"); const {set_current_user, set_realm} = zrequire("state_data"); const user_groups = zrequire("user_groups"); const {initialize_user_settings} = zrequire("user_settings"); const current_user = {}; set_current_user(current_user); const realm = make_realm(); set_realm(realm); const user_settings = {}; initialize_user_settings({user_settings}); const welcome_bot = make_cross_realm_bot({ email: "welcome-bot@example.com", user_id: 4, full_name: "Welcome Bot", // cross realm bots have no owner }); const email_gateway_bot = make_cross_realm_bot({ email: "emailgateway@example.com", user_id: 5, full_name: "Email Gateway", // cross realm bots have no owner }); const me = make_user({ email: "me@example.com", user_id: 30, full_name: "Me Myself", timezone: "America/Los_Angeles", // no avatar, so client should construct a /avatar/{user_id} URL. }); const isaac = make_user({ email: "isaac@example.com", delivery_email: "isaac-delivery@example.com", user_id: 32, full_name: "Isaac Newton", }); const unknown_user = people.make_user(1500, "unknown@example.com", "Unknown user"); function initialize() { additional_calls_before_set_timeout = noop; people.init(); people.add_active_user({...me}); people.initialize_current_user(me.user_id); muted_users.set_muted_users([]); people._add_user(unknown_user); } 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]), is_system_group: true, direct_subgroup_ids: new Set(), }); user_groups.initialize({realm_user_groups: [nobody, everyone]}); /* TEST SETUP NOTES: We don't add all these users right away, but they are convenient to have for various tests, and we just add them as needed after calling something like `people.init()`. Note that we deliberately make it so that alphabetical order mismatches id order, since that can uncover bugs where we neglect to be rigorous about sort order. */ const realm_admin = make_user({ email: "realm_admin@example.com", full_name: "Realm Admin", user_id: 32, role: Role.ADMINISTRATOR, }); const guest = make_user({ email: "guest@example.com", full_name: "Guest User", user_id: 33, role: Role.GUEST, }); const realm_owner = make_user({ email: "realm_owner@example.com", full_name: "Realm Owner", user_id: 34, role: Role.OWNER, }); const bot_botson = make_bot({ email: "botson-bot@example.com", user_id: 35, full_name: "Bot Botson", bot_owner_id: isaac.user_id, role: Role.MODERATOR, }); const moderator = make_user({ email: "moderator@example.com", full_name: "Moderator", user_id: 36, role: Role.MODERATOR, }); const bot_with_inaccessible_owner = make_bot({ email: "inaccessible-owner-bot@example.com", user_id: 37, full_name: "Inaccessible owner bot", bot_owner_id: 38, role: Role.MODERATOR, }); const steven = make_user({ email: "steven@example.com", delivery_email: "steven-delivery@example.com", user_id: 77, full_name: "Steven", }); const alice1 = make_user({ email: "alice1@example.com", delivery_email: "alice1-delivery@example.com", user_id: 202, full_name: "Alice", }); const bob = make_user({ email: "bob@example.com", delivery_email: "bob-delivery@example.com", user_id: 203, full_name: "Bob van Roberts", }); const charles = make_user({ email: "charles@example.com", user_id: 301, full_name: "Charles Dickens", avatar_url: "http://charles.com/foo.png", }); const maria = make_user({ email: "Athens@example.com", user_id: 302, full_name: "Maria Athens", // With client_gravatar enabled, requests that client compute gravatar avatar_url: null, }); const cedar = make_user({ email: "Cedar@example.com", user_id: 305, full_name: "Cedar Athens", // With client_gravatar enabled, requests that client compute gravatar avatar_url: null, }); const leo = make_user({ email: "Leo@example.com", user_id: 306, full_name: "Leo Athens", // With client_gravatar enabled, requests that client compute gravatar avatar_url: null, }); const ashton = make_user({ email: "ashton@example.com", user_id: 303, full_name: "Ashton Smith", }); const linus = make_user({ email: "ltorvalds@example.com", user_id: 304, full_name: "Linus Torvalds", }); // A user whose full name starts with digits, used to verify that // id matching is exact and does not collide with digit-leading names. const digit_named = make_user({ email: "digit-named@example.com", user_id: 9001, full_name: "304 Robot", }); const emp401 = make_user({ email: "emp401@example.com", user_id: 401, full_name: "whatever 401", }); const emp402 = make_user({ email: "EMP402@example.com", user_id: 402, full_name: "whatever 402", }); const debbie = make_user({ email: "deBBie71@example.com", user_id: 501, full_name: "Debra Henton", }); const stephen1 = make_user({ email: "stephen-the-author@example.com", user_id: 601, full_name: "Stephen King", }); const stephen2 = make_user({ email: "stephen-the-explorer@example.com", user_id: 602, full_name: "Stephen King", }); const noah = make_user({ email: "emnoa@example.com", user_id: 1200, full_name: "Nöôáàh Ëmerson", }); const plain_noah = make_user({ email: "otheremnoa@example.com", user_id: 1201, full_name: "Nooaah Emerson", }); const all1 = make_user({ email: "all1@example.com", user_id: 1202, full_name: "all", }); const all2 = make_user({ email: "all2@example.com", user_id: 1203, full_name: "all", }); const stewie = make_user({ email: "stewie@example.com", user_id: 1204, full_name: "Stewart Gilligan", profile_data: { 1: "(888) 888-8888", 2: "(555) 555-5555", 3: "he/him", }, }); const imported_user = { email: "imported-user@example.com", user_id: 1205, full_name: "Imported user", is_imported_stub: true, }; function get_all_persons() { return people.filter_all_persons(() => true); } run_test("basics", ({override}) => { initialize(); const persons = get_all_persons(); assert.deepEqual(people.get_realm_users(), [me]); assert.equal(persons.length, 2); assert.equal(persons[0].full_name, "Me Myself"); let realm_persons = people.get_realm_users(); assert.equal(realm_persons[0].full_name, "Me Myself"); realm_persons = people.get_realm_users(); assert.equal(realm_persons.length, 1); assert.equal(people.get_realm_active_human_user_ids().length, 1); const full_name = "Isaac Newton"; const email = "isaac@example.com"; assert.ok(!people.is_known_user_id(32)); assert.ok(!people.is_valid_full_name_and_user_id(full_name, 32)); assert.equal(people.get_user_id_from_name(full_name), undefined); people.add_active_user(isaac); assert.equal(people.get_actual_name_from_user_id(32), full_name); assert.ok(people.is_valid_full_name_and_user_id(full_name, 32)); assert.ok(people.is_known_user_id(32)); assert.equal(people.get_active_human_count(), 2); assert.equal(people.get_user_id_from_name(full_name), 32); let person = people.get_by_email(email); assert.equal(person.full_name, full_name); realm_persons = people.get_realm_users(); assert.equal(realm_persons.length, 2); const active_user_ids = people.get_active_user_ids().toSorted(); assert.deepEqual(active_user_ids, [me.user_id, isaac.user_id]); assert.equal(people.is_active_user_or_system_bot(isaac.user_id), true); assert.ok(people.is_valid_user_id_for_compose(isaac.user_id)); let bot_user_ids = people.get_bot_ids(); assert.equal(bot_user_ids.length, 0); // Now deactivate isaac people.deactivate(isaac); assert.equal(people.get_non_active_human_ids().length, 1); assert.equal(people.get_non_active_user_ids_count([isaac.user_id]), 1); assert.equal(people.get_active_human_count(), 1); assert.equal(people.is_active_user_or_system_bot(isaac.user_id), false); assert.equal(people.is_valid_user_id_for_compose(isaac.user_id), true); people.add_active_user(bot_botson); assert.equal(people.is_active_user_or_system_bot(bot_botson.user_id), true); bot_user_ids = people.get_bot_ids(); assert.deepEqual(bot_user_ids, [bot_botson.user_id]); assert.equal(people.get_bot_owner_user(bot_botson).full_name, "Isaac Newton"); assert.equal(people.can_admin_user(me), true); assert.equal(people.can_admin_user(bot_botson), false); // Add our cross-realm bot. It won't add to our human // count, and it has no owner. people.add_cross_realm_user(welcome_bot); people.add_cross_realm_user(email_gateway_bot); assert.equal(people.get_bot_owner_user(welcome_bot), undefined); assert.equal(people.get_bot_owner_user(email_gateway_bot), undefined); assert.equal(people.get_active_human_count(), 1); assert.equal(people.get_by_email(welcome_bot.email).full_name, "Welcome Bot"); assert.equal(people.get_by_email(email_gateway_bot.email).full_name, "Email Gateway"); override(settings_data, "user_can_access_all_other_users", () => false); assert.equal( people.get_bot_owner_user(bot_with_inaccessible_owner).full_name, "translated: Unknown user", ); // get_realm_users() will include our active bot, // but will exclude isaac (who is deactivated) assert.deepEqual( people .get_realm_users() .map((u) => u.user_id) .toSorted(), [me.user_id, bot_botson.user_id], ); // get_bot_ids() includes all bot users. bot_user_ids = people.get_bot_ids(); assert.deepEqual(bot_user_ids, [ bot_botson.user_id, welcome_bot.user_id, email_gateway_bot.user_id, ]); // The bot doesn't add to our human count. assert.equal(people.get_active_human_count(), 1); // Invalid user ID returns true and warns. blueslip.expect("warn", "Unexpectedly invalid user_id in user popover query"); assert.equal(people.is_active_user_or_system_bot(123412), true); unknown_user.is_inaccessible_user = true; assert.equal(people.is_active_user_or_system_bot(unknown_user.user_id), true); unknown_user.is_inaccessible_user = false; // We can still get their info for non-realm needs. person = people.get_by_email(email); assert.equal(person.email, email); // The current user should still be there person = people.get_by_email("me@example.com"); assert.equal(person.full_name, "Me Myself"); // Test is_my_user_id function assert.equal(people.is_my_user_id(me.user_id), true); assert.equal(people.is_my_user_id(isaac.user_id), false); // Reactivating issac people.add_active_user(isaac); const active_humans = people.get_realm_active_human_user_ids(); assert.equal(active_humans.length, 2); assert.deepEqual(active_humans.toSorted(), [me.user_id, isaac.user_id]); // get_users_from_ids assert.deepEqual(people.get_users_from_ids([me.user_id, isaac.user_id]), [me, isaac]); // Test with imported user. people.add_active_user(imported_user); assert.equal(people.get_realm_active_human_user_ids().length, 3); assert.equal(people.get_realm_active_human_user_ids_for_users_panel().length, 2); assert.equal(people.get_realm_active_imported_stub_user_ids().length, 1); // Check get_realm_active_imported_stub_user_ids does not include deactivated users. assert.equal(people.get_non_active_human_ids().length, 0); people.deactivate(imported_user); assert.equal(people.get_realm_active_human_user_ids().length, 2); assert.equal(people.get_realm_active_human_user_ids_for_users_panel().length, 2); assert.equal(people.get_realm_active_imported_stub_user_ids().length, 0); assert.equal(people.get_non_active_human_ids().length, 1); }); run_test("sort_but_pin_current_user_on_top with me", () => { initialize(); people.add_active_user(maria); people.add_active_user(steven); // We need the actual object from people.js, not the // "me" object we made a copy of. const my_user = people.get_by_user_id(me.user_id); const users = [steven, debbie, maria, my_user]; people.sort_but_pin_current_user_on_top(users); assert.deepEqual(users, [my_user, debbie, maria, steven]); }); run_test("sort_but_pin_current_user_on_top without me", () => { initialize(); people.add_active_user(maria); people.add_active_user(steven); const users = [steven, maria]; people.sort_but_pin_current_user_on_top(users); assert.deepEqual(users, [maria, steven]); }); run_test("check_active_non_active_users", ({override}) => { initialize(); people.add_active_user(bot_botson); people.add_active_user(isaac); let active_users = people.get_realm_users(); let non_active_users = people.get_non_active_realm_users(); assert.equal(active_users.length, 3); assert.equal(non_active_users.length, 0); people.add_active_user(maria); people.add_active_user(linus); active_users = people.get_realm_users(); assert.equal(active_users.length, 5); // Invalid ID blueslip.expect("error", "No user found"); people.is_person_active(1000001); assert.equal(people.is_person_active(maria.user_id), true); assert.equal(people.is_person_active(linus.user_id), true); people.deactivate(maria); non_active_users = people.get_non_active_realm_users(); active_users = people.get_realm_users(); assert.equal(non_active_users.length, 1); assert.equal(active_users.length, 4); assert.equal(people.is_person_active(maria.user_id), false); people.deactivate(linus); people.add_active_user(maria); non_active_users = people.get_non_active_realm_users(); active_users = people.get_realm_users(); assert.equal(non_active_users.length, 1); assert.equal(active_users.length, 4); assert.equal(people.is_person_active(maria.user_id), true); assert.equal(people.is_person_active(linus.user_id), false); // If user cannot access a user, that user will be treated as active. override(settings_data, "user_can_access_all_other_users", () => false); assert.equal(people.is_person_active(99), true); }); run_test("pm_lookup_key", () => { initialize(); assert.equal(people.pm_lookup_key("30"), "30"); assert.equal(people.pm_lookup_key("32,30"), "32"); assert.equal(people.pm_lookup_key("101,32,30"), "32,101"); }); run_test("get_recipients", () => { initialize(); people.add_active_user(isaac); people.add_active_user(linus); assert.deepEqual(people.get_recipients("30"), ["Me Myself"]); assert.deepEqual(people.get_recipients("30,32"), ["Isaac Newton"]); muted_users.add_muted_user(304); assert.deepEqual(people.get_recipients("304,32"), ["Isaac Newton", "translated: Muted user"]); }); run_test("get_full_name", () => { initialize(); people.add_active_user(isaac); const names = people.get_full_name(isaac.user_id); assert.equal(names, "Isaac Newton"); }); run_test("get_full_names_for_poll_option", () => { initialize(); people.add_active_user(isaac); const names = people.get_full_names_for_poll_option([me.user_id, isaac.user_id]); assert.equal(names, "Me Myself, Isaac Newton"); }); run_test("get_display_full_names", ({override}) => { initialize(); people.initialize_current_user(me.user_id); people.add_active_user(steven); people.add_active_user(bob); people.add_active_user(charles); people.add_active_user(guest); override(realm, "realm_enable_guest_user_indicator", true); let user_ids = [me.user_id, steven.user_id, bob.user_id, charles.user_id, guest.user_id]; let names = people.get_display_full_names(user_ids); // This doesn't do anything special for the current user. The caller has // to take care of such cases and do the appropriate. assert.deepEqual(names, [ "Me Myself", "Steven", "Bob van Roberts", "Charles Dickens", "translated: Guest User (guest)", ]); muted_users.add_muted_user(charles.user_id); override(realm, "realm_enable_guest_user_indicator", false); names = people.get_display_full_names(user_ids); assert.deepEqual(names, [ "Me Myself", "Steven", "Bob van Roberts", "translated: Muted user", "Guest User", ]); muted_users.add_muted_user(guest.user_id); names = people.get_display_full_names(user_ids); assert.deepEqual(names, [ "Me Myself", "Steven", "Bob van Roberts", "translated: Muted user", "translated: Muted user", ]); override(realm, "realm_enable_guest_user_indicator", true); names = people.get_display_full_names(user_ids); assert.deepEqual(names, [ "Me Myself", "Steven", "Bob van Roberts", "translated: Muted user", "translated: Muted user (guest)", ]); override(settings_data, "user_can_access_all_other_users", () => false); const inaccessible_user_id = 99; user_ids = [me.user_id, steven.user_id, inaccessible_user_id]; names = people.get_display_full_names(user_ids, true); assert.deepEqual(names, ["Me Myself", "Steven", "translated: Unknown user"]); }); run_test("my_custom_profile_data", () => { initialize(); const person = people.get_by_email(me.email); person.profile_data = {3: "My address", 4: "My phone number"}; assert.equal(people.my_custom_profile_data(3), "My address"); assert.equal(people.my_custom_profile_data(4), "My phone number"); }); run_test("get_custom_fields_by_type", ({override}) => { initialize(); people.add_active_user(stewie); const person = people.get_by_user_id(stewie.user_id); override(realm, "custom_profile_field_types", { SHORT_TEXT: { id: 1, name: "Short text", }, PRONOUNS: { id: 8, name: "Pronouns", }, }); override(realm, "custom_profile_fields", [ { id: 1, name: "Phone number (mobile)", type: 1, }, { id: 2, name: "Phone number (office)", type: 1, }, { id: 3, name: "Pronouns", type: 8, }, ]); const SHORT_TEXT_ID = 1; assert.deepEqual(people.get_custom_fields_by_type(person.user_id, SHORT_TEXT_ID), [ "(888) 888-8888", "(555) 555-5555", ]); assert.deepEqual(people.get_custom_fields_by_type(person.user_id, 8), ["he/him"]); assert.deepEqual(people.get_custom_fields_by_type(person.user_id, 100), []); // Bots have no profile_data, so the function returns undefined. people.add_active_user(bot_botson); assert.equal(people.get_custom_fields_by_type(bot_botson.user_id, SHORT_TEXT_ID), undefined); }); run_test("bot_custom_profile_data", () => { initialize(); // If this test fails, then try opening organization settings > bots // http://localhost:9991/#organization/bots/all-bots // and then try to edit any of the bots. people.add_active_user(bot_botson); assert.equal(people.get_custom_profile_data(bot_botson.user_id, 3), undefined); }); run_test("user_timezone", ({override}) => { initialize(); clock.setSystemTime(parseISO("20130208T080910").getTime()); override(user_settings, "twenty_four_hour_time", true); assert.equal(people.get_user_time(me.user_id), "00:09"); override(user_settings, "twenty_four_hour_time", false); assert.equal(people.get_user_time(me.user_id), "12:09 AM"); }); run_test("utcToZonedTime", ({override}) => { initialize(); clock.setSystemTime(parseISO("20130208T080910").getTime()); override(user_settings, "twenty_four_hour_time", true); assert.deepEqual(people.get_user_time(unknown_user.user_id), undefined); assert.equal(people.get_user_time(me.user_id), "00:09"); override(people.get_by_user_id(me.user_id), "timezone", "Eriador/Rivendell"); blueslip.expect( "warn", "Error formatting time in Eriador/Rivendell: RangeError: Invalid time zone specified: Eriador/Rivendell", ); people.get_user_time(me.user_id); }); run_test("user_type", () => { initialize(); people.init(); people.add_active_user(me); people.add_active_user(realm_admin); people.add_active_user(guest); people.add_active_user(realm_owner); people.add_active_user(moderator); people.add_active_user(bot_botson); assert.equal(people.get_user_type(me.user_id), $t({defaultMessage: "Member"})); assert.equal(people.get_user_type(realm_admin.user_id), $t({defaultMessage: "Administrator"})); assert.equal(people.get_user_type(guest.user_id), $t({defaultMessage: "Guest"})); assert.equal(people.get_user_type(realm_owner.user_id), $t({defaultMessage: "Owner"})); assert.equal(people.get_user_type(moderator.user_id), $t({defaultMessage: "Moderator"})); assert.equal(people.get_user_type(bot_botson.user_id), $t({defaultMessage: "Moderator"})); }); run_test("updates", () => { initialize(); const person = people.get_by_email("me@example.com"); people.set_full_name(person, "Me the Third"); assert.equal(people.my_full_name(), "Me the Third"); assert.equal(person.full_name, "Me the Third"); assert.equal(people.get_user_id_from_name("Me the Third"), me.user_id); }); run_test("get_by_user_id", () => { initialize(); let person = make_user({ email: "mary@example.com", user_id: 42, full_name: "Mary", }); people.add_active_user(person); person = people.get_by_email("mary@example.com"); assert.equal(person.full_name, "Mary"); person = people.get_by_user_id(42); assert.equal(person.email, "mary@example.com"); people.set_full_name(person, "Mary New"); person = people.get_by_user_id(42); assert.equal(person.full_name, "Mary New"); // deactivate() should eventually just take a user_id, but // now it takes a full person object. Note that deactivate() // won't actually make the user disappear completely. people.deactivate(person); person = people.get_by_user_id(42); assert.equal(person.user_id, 42); }); run_test("set_custom_profile_field_data", () => { initialize(); const person = people.get_by_email(me.email); person.profile_data = {}; const field = { id: 3, value: "Field value", rendered_value: "

Field value

", }; people.set_custom_profile_field_data(person.user_id, field); assert.equal(person.profile_data[field.id].value, "Field value"); assert.equal(person.profile_data[field.id].rendered_value, "

Field value

"); people.set_custom_profile_field_data(person.user_id, {id: 3, value: null}); assert.ok(!Object.hasOwn(person.profile_data, field.id)); }); run_test("is_current_user_only_owner", ({override}) => { initialize(); const person = people.get_by_email(me.email); person.is_owner = false; override(current_user, "is_owner", false); assert.ok(!people.is_current_user_only_owner()); person.is_owner = true; override(current_user, "is_owner", true); assert.ok(people.is_current_user_only_owner()); people.add_active_user(realm_owner); assert.ok(!people.is_current_user_only_owner()); }); run_test("user_can_change_their_own_role", ({override}) => { initialize(); const bob = make_user({ email: "bob@example.com", user_id: 1, full_name: "Bob", }); people.add_active_user(bob); const person = people.get_by_email(bob.email); assert.ok(person); person.is_owner = false; override(current_user, "is_owner", true); assert.ok(!people.user_can_change_their_own_role()); override(current_user, "is_owner", false); override(current_user, "is_admin", true); assert.ok(people.user_can_change_their_own_role()); override(current_user, "is_owner", false); override(current_user, "is_admin", false); assert.ok(!people.user_can_change_their_own_role()); person.is_owner = true; override(current_user, "is_owner", true); override(current_user, "is_admin", true); assert.ok(people.user_can_change_their_own_role()); }); run_test("filtered_users", () => { initialize(); people.add_active_user(charles); people.add_active_user(maria); people.add_active_user(ashton); people.add_active_user(linus); people.add_active_user(noah); people.add_active_user(plain_noah); const search_term = "a"; const users = people.get_realm_users(); let filtered_people = people.filter_people_by_search_terms(users, search_term); assert.equal(filtered_people.size, 2); assert.ok(filtered_people.has(ashton.user_id)); assert.ok(filtered_people.has(maria.user_id)); assert.ok(!filtered_people.has(charles.user_id)); filtered_people = people.filter_people_by_search_terms(users, ""); assert.equal(filtered_people.size, 7); filtered_people = people.filter_people_by_search_terms(users, "ltorv"); assert.equal(filtered_people.size, 1); assert.ok(filtered_people.has(linus.user_id)); filtered_people = people.filter_people_by_search_terms(users, "ch di, maria"); assert.equal(filtered_people.size, 2); assert.ok(filtered_people.has(charles.user_id)); assert.ok(filtered_people.has(maria.user_id)); // Test filtering of names with diacritics // This should match Nöôáàh by ignoring diacritics, and also match Nooaah filtered_people = people.filter_people_by_search_terms(users, "noOa"); assert.equal(filtered_people.size, 2); assert.ok(filtered_people.has(noah.user_id)); assert.ok(filtered_people.has(plain_noah.user_id)); // This should match ëmerson, but not emerson filtered_people = people.filter_people_by_search_terms(users, "ëm"); assert.equal(filtered_people.size, 1); assert.ok(filtered_people.has(noah.user_id)); // Multi-word query should ignore diacritics when the query // itself has no diacritics. filtered_people = people.filter_people_by_search_terms(users, "nooa em"); assert.equal(filtered_people.size, 2); assert.ok(filtered_people.has(noah.user_id)); assert.ok(filtered_people.has(plain_noah.user_id)); // Multi-word query with diacritics should match only Nöôáàh filtered_people = people.filter_people_by_search_terms(users, "nöôáàh Ëme"); assert.equal(filtered_people.size, 1); assert.ok(filtered_people.has(noah.user_id)); }); run_test("dm_matches_search_string", () => { initialize(); people.add_active_user(charles); people.add_active_user(maria); people.add_active_user(ashton); people.add_active_user(linus); people.add_active_user(noah); people.add_active_user(plain_noah); people.add_active_user(digit_named); let result = people.dm_matches_search_string([ashton], "a"); assert.ok(result); // Maria's email starts with `a`. result = people.dm_matches_search_string([maria], "a"); assert.ok(result); // Neither Charles' full name or email start with `a`. result = people.dm_matches_search_string([charles], "a"); assert.ok(!result); // Empty search terms should always return true result = people.dm_matches_search_string([charles], ""); assert.ok(result); result = people.dm_matches_search_string([charles, maria, noah], ""); assert.ok(result); // Match with email. result = people.dm_matches_search_string([linus], "ltorv"); assert.ok(result); // Match with user id. result = people.dm_matches_search_string([linus], "304"); assert.ok(result); result = people.dm_matches_search_string([linus], "30"); assert.ok(!result); // A full name starting with digits is still matched by name, and // is not affected by the (unrelated) id of any other user. result = people.dm_matches_search_string([digit_named], "304"); assert.ok(result); // Test filtering of names with diacritics. This should match // Nöôáàh by ignoring diacritics, and also match Nooaah. result = people.dm_matches_search_string([noah], "noOa"); assert.ok(result); result = people.dm_matches_search_string([plain_noah], "noOa"); assert.ok(result); // This should match ëmerson, but not emerson. result = people.dm_matches_search_string([noah], "ëm"); assert.ok(result); result = people.dm_matches_search_string([plain_noah], "ëm"); assert.ok(!result); }); run_test("filter_other_guest_ids", ({override}) => { initialize(); people.add_active_user(emp401); people.add_active_user(emp402); people.add_active_user(guest); assert.equal(emp401.user_id, 401); assert.equal(emp402.user_id, 402); assert.equal(guest.user_id, 33); let ids = [401, 402]; let guest_ids = people.filter_other_guest_ids(ids); assert.equal(guest_ids.length, 0); ids = [401, 402, 33]; guest_ids = people.filter_other_guest_ids(ids); assert.equal(guest_ids.length, 1); assert.equal(guest_ids[0], 33); override(current_user, "is_guest", true); override(current_user, "user_id", 1); // User ID of the current user will not be included in result. guest_ids = people.filter_other_guest_ids([1]); assert.equal(guest_ids.length, 0); }); run_test("user_ids_to_full_names_array", () => { initialize(); people.add_active_user(emp401); people.add_active_user(emp402); assert.equal(emp401.user_id, 401); assert.equal(emp402.user_id, 402); const ids = [401, 402]; const names = people.user_ids_to_full_names_array(ids); assert.equal(names.length, 2); assert.equal(names[0], emp401.full_name); assert.equal(names[1], emp402.full_name); }); run_test("multi_user_methods", () => { initialize(); // Add users to valid_user_ids. const source = "server_events"; people.add_active_user(emp401, source); people.add_active_user(emp402, source); // The order of user_ids is relevant here. assert.equal(emp401.user_id, 401); assert.equal(emp402.user_id, 402); let user_ids = people.slug_to_user_ids("402,401"); assert.deepEqual(user_ids, [402, 401]); user_ids = people.slug_to_user_ids("402,401-group"); assert.deepEqual(user_ids, [402, 401]); assert.equal(people.user_ids_string_to_slug("401,402"), "401,402-group"); assert.equal(people.user_ids_string_to_slug("402"), "402-whatever-402"); assert.equal(people.user_ids_to_slug([401, 402]), "401,402-group"); assert.equal(people.user_ids_to_slug([402]), "402-whatever-402"); assert.equal(people.user_ids_to_slug([]), undefined); }); run_test("user_ids_to_full_names_string", () => { initialize(); people.add_active_user(charles); people.add_active_user(maria); assert.equal( people.user_ids_to_full_names_string([charles.user_id, maria.user_id]), `${charles.full_name}, ${maria.full_name}`, ); blueslip.expect("error", "Unknown user_id in maybe_get_user_by_id"); assert.equal( people.user_ids_to_full_names_string([charles.user_id, 9999, maria.user_id]), `${charles.full_name}, ${maria.full_name}, translated: Unknown user`, ); }); run_test("concat_direct_message_group", () => { initialize(); /* We assume that user_ids passed in to concat_direct_message_group have already been validated, so we don't need actual people for these tests to pass. That may change in the future. */ const user_ids = [303, 301, 302]; assert.equal(people.concat_direct_message_group(user_ids, 304), "301,302,303,304"); // IMPORTANT: we always want to sort // ids numerically to create direct message group strings. assert.equal(people.concat_direct_message_group(user_ids, 99), "99,301,302,303"); }); run_test("message_methods", () => { initialize(); people.add_active_user(charles); people.add_active_user(maria); people.add_active_user(cedar); people.add_active_user(leo); people.add_active_user(ashton); assert.equal(people.get_muted_user_avatar_url(), "/static/images/muted-user/muted-sender.png"); assert.equal(maria.is_guest, false); assert.equal( people.small_avatar_url_for_person(maria), "https://secure.gravatar.com/avatar/6dbdd7946b58d8b11351fcb27e5cdd55?d=identicon", ); assert.equal( maria.avatar_url, "https://secure.gravatar.com/avatar/6dbdd7946b58d8b11351fcb27e5cdd55?d=identicon", ); // small_avatar_url_for_user_id delegates to small_avatar_url_for_person. assert.equal(people.small_avatar_url_for_user_id(maria.user_id), maria.avatar_url); // This will use the cached gravatar url assert.equal( people.medium_avatar_url_for_person(maria), "https://secure.gravatar.com/avatar/6dbdd7946b58d8b11351fcb27e5cdd55?d=identicon&s=500", ); // This will create a new gravatar url assert.equal( people.medium_avatar_url_for_person(cedar), "https://secure.gravatar.com/avatar/2e6ed9fc1de54b7b5bc98ced46fe7a14?d=identicon&s=500", ); assert.equal( cedar.avatar_url, "https://secure.gravatar.com/avatar/2e6ed9fc1de54b7b5bc98ced46fe7a14?d=identicon", ); assert.equal(people.medium_avatar_url_for_person(charles), "/avatar/301/medium?version=0"); assert.equal(people.medium_avatar_url_for_person(ashton), "/avatar/303/medium?version=0"); muted_users.add_muted_user(30); assert.deepEqual(people.sender_info_for_recent_view_row([30]), [ { ...me, avatar_url_small: "/avatar/30", is_muted: true, }, ]); let message = { type: "private", display_recipient: [{id: maria.user_id}, {id: me.user_id}, {id: charles.user_id}], sender_id: charles.user_id, }; assert.equal(people.pm_with_url(message), "#narrow/dm/301,302-group"); assert.equal(people.pm_perma_link(message), "#narrow/dm/30,301,302-group"); assert.equal(people.small_avatar_url(message), "http://charles.com/foo.png"); message = { type: "private", display_recipient: [{id: maria.user_id}, {id: me.user_id}], avatar_url: "legacy.png", }; assert.equal(people.pm_with_url(message), "#narrow/dm/302-Maria-Athens"); assert.equal(people.pm_perma_link(message), "#narrow/dm/30,302-dm"); assert.equal(people.small_avatar_url(message), "legacy.png"); message = { avatar_url: undefined, sender_id: maria.user_id, }; assert.equal( people.small_avatar_url(message), "https://secure.gravatar.com/avatar/6dbdd7946b58d8b11351fcb27e5cdd55?d=identicon", ); // No gravatar url cached yet message = { avatar_url: undefined, sender_id: leo.user_id, }; assert.equal( people.small_avatar_url(message), "https://secure.gravatar.com/avatar/ce9581fbf1beefbad43a4233aa65954c?d=identicon", ); blueslip.expect("error", "Unknown user_id in maybe_get_user_by_id"); message = { avatar_url: undefined, sender_email: "foo@example.com", sender_id: 9999999, }; assert.equal( people.small_avatar_url(message), "https://secure.gravatar.com/avatar/b48def645758b95537d4424c84d1a9ff?d=identicon", ); message = { sender_id: ashton.user_id, }; assert.equal(people.small_avatar_url(message), `/avatar/${ashton.user_id}`); message = { type: "private", display_recipient: [{id: me.user_id}], }; assert.equal(people.pm_with_url(message), "#narrow/dm/30-Me-Myself"); assert.equal(people.pm_perma_link(message), "#narrow/dm/30-dm"); message = {type: "stream"}; assert.equal(people.pm_with_user_ids(message), undefined); assert.equal(people.all_user_ids_in_pm(message), undefined); // Test undefined user_ids assert.equal(people.pm_reply_user_string(message), undefined); assert.equal(people.pm_with_url(message), undefined); // Test sender_is_bot const bot = bot_botson; people.add_active_user(bot); message = {sender_id: bot.user_id}; assert.equal(people.sender_is_bot(message), true); message = {sender_id: maria.user_id}; assert.equal(people.sender_is_bot(message), false); message = {sender_id: undefined}; assert.equal(people.sender_is_bot(message), false); // Test sender_is_guest people.add_active_user(guest); message = {sender_id: guest.user_id}; assert.equal(people.sender_is_guest(message), true); message = {sender_id: maria.user_id}; assert.equal(people.sender_is_guest(message), false); message = {sender_id: charles.user_id}; assert.equal(people.sender_is_guest(message), false); message = {sender_id: undefined}; assert.equal(people.sender_is_guest(message), false); // Test sender_is_deactivated people.deactivate(maria); message = {sender_id: maria.user_id}; assert.equal(people.sender_is_deactivated(message), true); message = {sender_id: charles.user_id}; assert.equal(people.sender_is_deactivated(message), false); message = {sender_id: undefined}; assert.equal(people.sender_is_deactivated(message), false); }); run_test("extract_people_from_message", () => { initialize(); const message = { type: "stream", sender_full_name: maria.full_name, sender_id: maria.user_id, sender_email: maria.email, }; assert.ok(!people.is_known_user_id(maria.user_id)); blueslip.expect("error", "Added user late"); people.extract_people_from_message(message); assert.ok(people.is_known_user_id(maria.user_id)); blueslip.reset(); }); run_test("get_people_for_search_bar", ({override}) => { initialize(); let user_ids; override(message_user_ids, "user_ids", () => user_ids); for (const i of _.range(20)) { const person = make_user({ email: "whatever@email.com", full_name: "James Jones", user_id: 1000 + i, }); people.add_active_user(person); } user_ids = []; const big_results = people.get_people_for_search_bar("James"); assert.equal(big_results.length, 20); user_ids = [1001, 1002, 1003, 1004, 1005, 1006]; const small_results = people.get_people_for_search_bar("Jones"); // As long as there are 5+ results among the user_ids // in message_user_ids, we will get a small result and not // search all people. assert.equal(small_results.length, 6); }); run_test("updates", () => { initialize(); const old_email = "FOO@example.com"; const new_email = "bar@example.com"; const user_id = 502; let person = make_user({ email: old_email, user_id, full_name: "Foo Barson", }); people.add_active_user(person); // Do sanity checks on our data. assert.equal(people.get_by_email(old_email).user_id, user_id); assert.equal(people.get_by_email(new_email), undefined); // DO THE EMAIL UPDATE HERE. people.update_email(user_id, new_email); // Now look up using the new email. assert.equal(people.get_by_email(new_email).user_id, user_id); const all_people = get_all_persons(); assert.equal(all_people.length, 3); person = all_people.find((p) => p.email === new_email); assert.equal(person.full_name, "Foo Barson"); // Test shim where we can still retrieve user info using the // old email. blueslip.expect( "warn", "Obsolete email passed to get_by_email: FOO@example.com new email = bar@example.com", ); person = people.get_by_email(old_email); assert.equal(person.user_id, user_id); }); run_test("track_duplicate_full_names", () => { initialize(); people.add_active_user(maria); people.add_active_user(stephen1); assert.ok(!people.is_duplicate_full_name("Stephen King")); assert.equal(people.get_user_id_from_name("Stephen King"), stephen1.user_id); // Now duplicate the Stephen King name. people.add_active_user({...stephen2}); // For duplicate names we won't try to guess which // user_id the person means; the UI should use // other codepaths for disambiguation. assert.equal(people.get_user_id_from_name("Stephen King"), undefined); assert.ok(people.is_duplicate_full_name("Stephen King")); assert.ok(!people.is_duplicate_full_name("Maria Athens")); assert.ok(!people.is_duplicate_full_name("Some Random Name")); // It is somewhat janky that we have to clone // stephen2 here. It would be nice if people.set_full_name // just took a user_id as the first parameter. people.set_full_name({...stephen2}, "Stephen King JP"); assert.ok(!people.is_duplicate_full_name("Stephen King")); assert.ok(!people.is_duplicate_full_name("Stephen King JP")); }); run_test("get_mention_syntax", () => { initialize(); // blueslip warning is not raised for wildcard mentions without a user_id assert.equal(people.get_mention_syntax("all"), "@**all**"); assert.equal(people.get_mention_syntax("everyone", undefined, true), "@_**everyone**"); assert.equal(people.get_mention_syntax("stream"), "@**channel**"); assert.equal(people.get_mention_syntax("channel"), "@**channel**"); assert.equal(people.get_mention_syntax("topic"), "@**topic**"); people.add_active_user(stephen1); people.add_active_user(stephen2); people.add_active_user(maria); assert.ok(people.is_duplicate_full_name("Stephen King")); blueslip.expect("warn", "get_mention_syntax called without user_id."); assert.equal(people.get_mention_syntax("Stephen King"), "@**Stephen King**"); blueslip.reset(); assert.equal(people.get_mention_syntax("Stephen King", 601), "@**Stephen King|601**"); assert.equal(people.get_mention_syntax("Stephen King", 602, true), "@_**Stephen King|602**"); assert.equal(people.get_mention_syntax("Maria Athens", 603), "@**Maria Athens**"); // With always_include_id, the id is pinned even when the name is unique. assert.equal( people.get_mention_syntax("Maria Athens", 603, false, true), "@**Maria Athens|603**", ); // Following tests handle a special case when `full_name` matches with a wildcard. // // At this point, there is no duplicate full name, `all`, so we should still get // mention syntax with `user_id` appended to it. people.add_active_user(all1); assert.equal(people.get_mention_syntax("all", 1202), "@**all|1202**"); people.add_active_user(all2); assert.ok(people.is_duplicate_full_name("all")); assert.equal(people.get_mention_syntax("all", 1203, true), "@_**all|1203**"); }); run_test("initialize", () => { initialize(); people.init(); const params = {}; const retiree = make_user({ email: "retiree@example.com", user_id: 15, full_name: "Retiree", }); params.realm_non_active_users = [retiree]; const current_user = make_user({ email: "my_email@example.com", user_id: 42, full_name: "Me Myself", }); const alice = make_user({ email: "alice@example.com", user_id: 16, full_name: "Alice", }); params.realm_users = [alice, current_user]; const test_bot = make_user({ email: "bot@example.com", user_id: 17, full_name: "Test Bot", }); params.cross_realm_bots = [test_bot]; const user_group_params = { realm_user_groups: [ make_user_group({ is_system_group: true, members: [42, 17, 16, 15], }), ], }; people.initialize(current_user.user_id, params, user_group_params); assert.equal(people.is_active_user_or_system_bot(17), true); assert.ok(people.is_valid_user_id_for_compose(test_bot.user_id)); assert.ok(people.is_valid_user_id_for_compose(alice.user_id)); assert.ok(people.is_valid_user_id_for_compose(retiree.user_id)); blueslip.expect("error", "Unknown user_id in maybe_get_user_by_id"); assert.ok(!people.is_valid_user_id_for_compose(9999)); assert.ok(people.is_valid_bulk_user_ids_for_compose([17, 16, 15])); blueslip.reset(); blueslip.expect("error", "Unknown user_id in maybe_get_user_by_id"); assert.ok(!people.is_valid_bulk_user_ids_for_compose([17, 9999, 15], false)); blueslip.reset(); assert.ok(people.is_my_user_id(42)); const fetched_retiree = people.get_by_user_id(15); assert.equal(fetched_retiree.full_name, "Retiree"); assert.equal(page_params.realm_users, undefined); assert.equal(page_params.cross_realm_bots, undefined); assert.equal(page_params.realm_non_active_users, undefined); }); run_test("predicate_for_user_settings_filters", ({override}) => { initialize(); /* This function calls matches_user_settings_search, so that is where we do more thorough testing. This test is just a sanity check for now. */ override(current_user, "is_admin", false); const fred_smith = {full_name: "Fred Smith", role: Role.OWNER}; // Test only when text_search filter is true assert.equal( people.predicate_for_user_settings_filters(fred_smith, {text_search: "fr", role_code: 0}), true, ); // Test only when role_code filter is true assert.equal( people.predicate_for_user_settings_filters(fred_smith, {text_search: "", role_code: 100}), true, ); // Test only when text_search filter is false assert.equal( people.predicate_for_user_settings_filters(fred_smith, {text_search: "ab", role_code: 0}), false, ); // Test only when role_code filter is false assert.equal( people.predicate_for_user_settings_filters(fred_smith, {text_search: "", role_code: 200}), false, ); // Test when both text_search and role_code filter are true assert.equal( people.predicate_for_user_settings_filters(fred_smith, { text_search: "smi", role_code: 100, }), true, ); // Test when both text_search and role_code filter are false assert.equal( people.predicate_for_user_settings_filters(fred_smith, {text_search: "de", role_code: 300}), false, ); }); run_test("matches_user_settings_search", ({override}) => { initialize(); const match = people.matches_user_settings_search; override(current_user, "is_admin", false); assert.equal(match({email: "fred@example.com"}, "fred"), false); assert.equal(match({full_name: "Fred Smith"}, "fr"), true); override(current_user, "is_admin", true); assert.equal(match({delivery_email: "fred@example.com"}, "fr"), true); assert.equal( match( { delivery_email: "bogus", email: "fred@example.com", }, "fr", ), false, ); assert.equal(match({delivery_email: "fred@example.com"}, "fr"), true); assert.equal(match({email: "fred@example.com"}, "fr"), false); // test normal stuff assert.equal(match({email: "fred@example.com"}, "st"), false); assert.equal(match({full_name: "Fred Smith"}, "st"), false); assert.equal(match({full_name: "Joe Frederick"}, "st"), false); assert.equal(match({delivery_email: "fred@example.com"}, "fr"), true); assert.equal(match({full_name: "Fred Smith"}, "fr"), true); assert.equal(match({full_name: "Joe Frederick"}, "fr"), true); // test in-string matches...we may want not to be so liberal // here about matching, as it's noisy for large realms (who // need search the most) assert.equal(match({delivery_email: "fred@example.com"}, "re"), true); assert.equal(match({full_name: "Fred Smith"}, "re"), true); assert.equal(match({full_name: "Joe Frederick"}, "re"), true); }); run_test("is_valid_full_name_and_user_id", () => { initialize(); assert.ok(!people.is_valid_full_name_and_user_id("bogus", 99)); assert.ok(!people.is_valid_full_name_and_user_id(me.full_name, 99)); assert.ok(people.is_valid_full_name_and_user_id(me.full_name, me.user_id)); }); run_test("get_visible_email", () => { initialize(); people.add_active_user(steven); people.add_active_user(maria); let email = people.get_visible_email(steven); assert.equal(email, steven.delivery_email); email = people.get_visible_email(maria); assert.equal(email, maria.email); }); run_test("get_active_message_people", () => { initialize(); message_user_ids.user_ids = () => [steven.user_id, maria.user_id, alice1.user_id]; people.add_active_user(steven); people.add_active_user(maria); people.add_active_user(alice1); let active_message_people = people.get_active_message_people(); assert.deepEqual(active_message_people, [steven, maria, alice1]); people.deactivate(alice1); active_message_people = people.get_active_message_people(); assert.deepEqual(active_message_people, [steven, maria]); }); run_test("direct_message_group_string", () => { initialize(); assert.equal(people.direct_message_group_string({type: "stream"}), undefined); function direct_message_group(user_ids) { return people.direct_message_group_string({ type: "private", display_recipient: user_ids.map((id) => ({id})), }); } people.add_active_user(maria); people.add_active_user(bob); assert.equal(direct_message_group([]), undefined); assert.equal(direct_message_group([me.user_id, maria.user_id]), undefined); assert.equal(direct_message_group([me.user_id, maria.user_id, bob.user_id]), "203,302"); }); run_test("get_realm_active_human_users", () => { initialize(); let humans = people.get_realm_active_human_users(); assert.equal(humans.length, 1); assert.deepEqual(humans, [me]); people.add_active_user(maria); people.add_active_user(bot_botson); humans = people.get_realm_active_human_users(); assert.equal(humans.length, 2); assert.deepEqual(humans, [me, maria]); people.deactivate(maria); humans = people.get_realm_active_human_users(); assert.equal(humans.length, 1); assert.deepEqual(humans, [me]); }); run_test("should_show_guest_user_indicator", ({override}) => { initialize(); people.add_active_user(charles); people.add_active_user(guest); override(realm, "realm_enable_guest_user_indicator", false); assert.equal(people.should_add_guest_user_indicator(charles.user_id), false); assert.equal(people.should_add_guest_user_indicator(guest.user_id), false); override(realm, "realm_enable_guest_user_indicator", true); assert.equal(people.should_add_guest_user_indicator(charles.user_id), false); assert.equal(people.should_add_guest_user_indicator(guest.user_id), true); }); run_test("get_user_by_id_assert_valid", ({override}) => { initialize(); people.add_active_user(charles); const inaccessible_user_id = 99; override(realm, "realm_bot_domain", "zulipdev.com"); override(settings_data, "user_can_access_all_other_users", () => false); let user = people.get_user_by_id_assert_valid(inaccessible_user_id); assert.equal(user.full_name, "translated: Unknown user"); assert.equal(user.user_id, inaccessible_user_id); assert.ok(user.is_inaccessible_user); assert.equal(user.email, "user99@zulipdev.com"); user = people.get_user_by_id_assert_valid(charles.user_id); assert.equal(user.full_name, charles.full_name); assert.ok(!user.is_inaccessible_user); assert.equal(user.email, charles.email); override(settings_data, "user_can_access_all_other_users", () => true); assert.throws( () => { people.get_user_by_id_assert_valid(199); }, { name: "Error", message: "Unknown user_id in get_by_user_id: 199", }, ); user = people.get_user_by_id_assert_valid(charles.user_id); assert.equal(user.full_name, charles.full_name); assert.ok(!user.is_inaccessible_user); assert.equal(user.email, charles.email); }); run_test("user_can_initiate_direct_message_thread", ({override}) => { initialize(); people.add_active_user(welcome_bot); override(realm, "realm_direct_message_initiator_group", nobody.id); assert.ok(!people.user_can_initiate_direct_message_thread("32")); // Can send if only bots and self are present. assert.ok(people.user_can_initiate_direct_message_thread("4,30")); override(realm, "realm_direct_message_initiator_group", everyone.id); assert.ok(people.user_can_initiate_direct_message_thread("32")); }); run_test("sort_by_username", () => { initialize(); people.add_active_user(maria); people.add_active_user(cedar); people.add_active_user(leo); assert.deepEqual( people.sort_user_ids_by_username([maria.user_id, cedar.user_id, leo.user_id]), [cedar.user_id, leo.user_id, maria.user_id], ); }); run_test("get_users_that_match_role_ids", () => { people.add_active_user(realm_admin); people.add_active_user(realm_owner); people.add_active_user(moderator); people.add_active_user(guest); const user_ids = new Set([ realm_admin.user_id, realm_owner.user_id, moderator.user_id, guest.user_id, ]); let users = people.get_users_that_match_role_ids( user_ids, new Set([settings_config.user_role_values.admin.code]), ); assert.deepEqual(new Set(users), new Set([realm_admin])); users = people.get_users_that_match_role_ids( user_ids, new Set([settings_config.user_role_values.moderator.code]), ); assert.deepEqual(new Set(users), new Set([moderator])); users = people.get_users_that_match_role_ids( user_ids, new Set([ settings_config.user_role_values.admin.code, settings_config.user_role_values.owner.code, ]), ); assert.deepEqual(new Set(users), new Set([realm_owner, realm_admin])); }); // reset to native Date() run_test("reset MockDate", () => { clock.reset(); }); run_test("user fetch retries a failed request", async ({override, override_rewire}) => { initialize(); people.add_valid_user_id(1); let retry_count = 1; override(channel, "get", ({url, data, success, error}) => { assert.equal(url, "/json/users"); assert.ok(data.user_ids.includes("1")); // Simulate failure of the first two attempts. if (retry_count < 3) { retry_count += 1; error({responseJSON: {msg: "test error"}}); return; } success({ members: [ { email: "user1@example.com", user_id: 1, full_name: "First user", delivery_email: "", date_joined: "", is_active: true, is_owner: false, is_admin: false, is_guest: false, role: 1, avatar_url: "", avatar_version: 1, is_bot: false, is_imported_stub: false, }, ], result: "success", msg: "", }); }); // Math.round will be `0`. override_rewire(retry_backoff, "get_retry_backoff_seconds", () => retry_count / 1000); // Check that we retry the request after a failed attempt. blueslip.expect( "warn", "Fetch for users failed, retrying after 0 seconds. Error: test error", 2, ); await people.fetch_users_from_ids_internal([1]); }); run_test("user fetch during initialize", async ({override}) => { initialize(); people.init(); // Valid users missing from params data sent by server. const users_in_response = [ { email: "retiree@example.com", user_id: 15, full_name: "Retiree", delivery_email: "", date_joined: "", is_active: true, is_owner: false, is_admin: false, is_guest: false, role: 1, avatar_url: "", avatar_version: 1, is_bot: false, is_imported_stub: false, }, { email: "alice@example.com", user_id: 16, full_name: "Alice", delivery_email: "", date_joined: "", is_active: false, is_owner: false, is_admin: false, is_guest: false, role: 1, avatar_url: "", avatar_version: 1, is_bot: false, is_imported_stub: false, }, ]; const params = { realm_users: [ { email: "my_email@example.com", user_id: 42, full_name: "Me Myself", }, ], realm_non_active_users: [], cross_realm_bots: [ { email: "bot@example.com", user_id: 17, full_name: "Test Bot", }, ], }; const user_group_params = { realm_user_groups: [ make_user_group({ is_system_group: true, members: [42, 17, 16, 15], }), ], }; const my_user_id = 42; let user_GET_request_calls = 0; let promise_for_user_already_in_transit; override(channel, "get", ({url, data, success, _error}) => { user_GET_request_calls += 1; assert.equal(url, "/json/users"); assert.ok(data.user_ids.includes("15")); assert.ok(data.user_ids.includes("16")); assert.ok(!data.user_ids.includes("42")); assert.ok(!data.user_ids.includes("17")); // While this get request is in-flight, request for the // a subset of the users again and check if it resolved. promise_for_user_already_in_transit = people.get_or_fetch_users_from_ids([15]); success({ members: users_in_response, result: "success", msg: "", }); }); additional_calls_before_set_timeout = () => { // This fetch request doesn't result in any new GET request, // since the request for this user is added to pending users // for fetch and returns a promise. const promise = people.fetch_users_from_ids_internal([15]); assert.ok(promise instanceof Promise); }; await people.initialize(my_user_id, params, user_group_params); await promise_for_user_already_in_transit; assert.equal(user_GET_request_calls, 1); const retiree = people.get_by_user_id(15); const alice = people.get_by_user_id(16); assert.equal(retiree.full_name, "Retiree"); assert.equal(alice.full_name, "Alice"); assert.ok(people.is_valid_user_id(15)); assert.ok(people.is_valid_user_id(16)); assert.ok(people.is_valid_user_id(42)); assert.ok(people.is_valid_user_id(17)); assert.ok(people.is_valid_user_ids([15, 16, 42, 17])); assert.ok(!people.is_valid_user_ids([15, 16, 42, 9999])); assert.equal(people.get_by_email("alice@example.com").user_id, 16); assert.equal(people.get_by_email("retiree@example.com").user_id, 15); blueslip.expect("error", "Ignored invalid user_ids: 1, 2"); assert.deepEqual(await people.get_or_fetch_users_from_ids([1, 2]), []); assert.equal(user_GET_request_calls, 1); }); function make_fetched_user(user_id) { return { email: `user${user_id}@example.com`, user_id, full_name: `User ${user_id}`, delivery_email: "", date_joined: "", is_active: true, is_owner: false, is_admin: false, is_guest: false, role: Role.MEMBER, avatar_url: "", avatar_version: 1, is_bot: false, is_imported_stub: false, }; } function make_params_for_users_missing_from_register(my_user_id, missing_user_ids) { return { people_params: { realm_users: [ { email: "my_email@example.com", user_id: my_user_id, full_name: "Me Myself", }, ], realm_non_active_users: [], cross_realm_bots: [], }, user_group_params: { realm_user_groups: [ make_user_group({ is_system_group: true, members: [my_user_id, ...missing_user_ids], }), ], }, }; } run_test("user fetch batches a large request", async ({override}) => { initialize(); people.init(); const my_user_id = 42; const user_ids_to_fetch = Array.from({length: 1200}, (_unused, index) => 1000 + index); const {people_params, user_group_params} = make_params_for_users_missing_from_register( my_user_id, user_ids_to_fetch, ); const requested_batches = []; override(channel, "get", ({url, data, success}) => { assert.equal(url, "/json/users"); const batch = JSON.parse(data.user_ids); // Users from the earlier batches are already known by the time we // request this one, rather than only once every batch has arrived. assert.ok(requested_batches.flat().every((user_id) => people.is_known_user_id(user_id))); requested_batches.push(batch); success({ members: batch.map((user_id) => make_fetched_user(user_id)), result: "success", msg: "", }); }); await people.initialize(my_user_id, people_params, user_group_params); assert.deepEqual( requested_batches.map((batch) => batch.length), [500, 500, 200], ); assert.deepEqual( requested_batches.flat().toSorted((a, b) => a - b), user_ids_to_fetch, ); for (const user_id of user_ids_to_fetch) { assert.equal(people.get_by_user_id(user_id).full_name, `User ${user_id}`); } }); run_test("user fetch resumes after a failed batch", async ({override, override_rewire}) => { // Inaccessible users make a batch succeed while returning no members, // which is what requires tracking progress by the IDs we sent. initialize(); people.init(); override(settings_data, "user_can_access_all_other_users", () => false); const my_user_id = 42; const inaccessible_user_ids = Array.from({length: 1200}, (_unused, index) => 1000 + index); const {people_params, user_group_params} = make_params_for_users_missing_from_register( my_user_id, inaccessible_user_ids, ); const response_with_no_accessible_users = {members: [], result: "success", msg: ""}; const requested_batches = []; // Requests 2 and 4 are the first attempts at the second and third // batches, so a success falls between the two failures. const failing_request_numbers = new Set([2, 4]); override(channel, "get", ({url, data, success, error}) => { assert.equal(url, "/json/users"); requested_batches.push(JSON.parse(data.user_ids)); if (failing_request_numbers.has(requested_batches.length)) { error({responseJSON: {msg: "test error"}}); return; } success(response_with_no_accessible_users); }); const backoff_attempt_counts = []; override_rewire(retry_backoff, "get_retry_backoff_seconds", (_xhr, attempts) => { backoff_attempt_counts.push(attempts); return 0; }); blueslip.expect( "warn", "Fetch for users failed, retrying after 0 seconds. Error: test error", 2, ); await people.initialize(my_user_id, people_params, user_group_params); // Only the failed batches are requested twice; the rest are not refetched. assert.deepEqual( requested_batches.map((batch) => batch.length), [500, 500, 500, 200, 200], ); assert.deepEqual(requested_batches[1], requested_batches[2]); assert.deepEqual(requested_batches[3], requested_batches[4]); const request_count_by_user_id = new Map(); for (const user_id of requested_batches.flat()) { request_count_by_user_id.set(user_id, (request_count_by_user_id.get(user_id) ?? 0) + 1); } assert.ok(requested_batches[0].every((user_id) => request_count_by_user_id.get(user_id) === 1)); // The counter restarts at 1 because a batch succeeded between the failures. assert.deepEqual(backoff_attempt_counts, [1, 1]); for (const user_id of inaccessible_user_ids) { assert.ok(people.get_by_user_id(user_id).is_inaccessible_user); } }); run_test("user fetch corner case", async ({override, override_rewire}) => { // We test here for the following sequence of fetches: // 1st request for [1, 2]. // 2nd request for [1, 2, 3] while the first one is in-flight. // 3rd request for [3] while the first one is in-flight. // // We should end up with two GET requests: // - The first for [1, 2]. // - The second for [3]. // // If the request for [3] finished first, we should resolve the 3rd // request without waiting for the 1st request to finish. initialize(); people.add_valid_user_id(1); people.add_valid_user_id(2); people.add_valid_user_id(3); const first_request_response = [ { email: "retiree@example.com", user_id: 1, full_name: "Retiree", delivery_email: "", date_joined: "", is_active: true, is_owner: false, is_admin: false, is_guest: false, role: 1, avatar_url: "", avatar_version: 1, is_bot: false, is_imported_stub: false, }, { email: "alice@example.com", user_id: 2, full_name: "Alice", delivery_email: "", date_joined: "", is_active: false, is_owner: false, is_admin: false, is_guest: false, role: 1, avatar_url: "", avatar_version: 1, is_bot: false, is_imported_stub: false, }, ]; const second_request_response = [ { email: "third@example.com", user_id: 3, full_name: "Third user", delivery_email: "", date_joined: "", is_active: true, is_owner: false, is_admin: false, is_guest: false, role: 1, avatar_url: "", avatar_version: 1, is_bot: false, is_imported_stub: false, }, ]; let sent_success_response_for_third_user = false; override(channel, "get", ({url, data, success, error}) => { assert.equal(url, "/json/users"); // There shouldn't be a fetch for [1, 2, 3]. assert.ok(data.user_ids !== "[1,2,3]"); if (data.user_ids === "[3]") { sent_success_response_for_third_user = true; success({ members: second_request_response, result: "success", msg: "", }); return; } if (data.user_ids === "[1,2]") { if (!sent_success_response_for_third_user) { error({responseJSON: {msg: "Network error"}}); return; } success({ members: first_request_response, result: "success", msg: "", }); return; } }); override_rewire(retry_backoff, "get_retry_backoff_seconds", () => 0); // Check that we retry the request after a failed attempt. blueslip.expect( "warn", "Fetch for users failed, retrying after 0 seconds. Error: Network error", ); // We need to check that third promise resolves before the first promise. let third_promise_resolved = false; let first_promise_resolved = false; await Promise.all([ (async () => { await people.get_or_fetch_users_from_ids([1, 2]); first_promise_resolved = true; assert.ok(third_promise_resolved); })(), (async () => { await people.get_or_fetch_users_from_ids([1, 2, 3]); assert.ok(first_promise_resolved); assert.ok(third_promise_resolved); })(), (async () => { await people.get_or_fetch_users_from_ids([3]); third_promise_resolved = true; assert.ok(!first_promise_resolved); })(), ]); const user1 = people.get_by_user_id(1); const user2 = people.get_by_user_id(2); const user3 = people.get_by_user_id(3); assert.equal(user1.full_name, "Retiree"); assert.equal(user2.full_name, "Alice"); assert.equal(user3.full_name, "Third user"); }); run_test("fetch inaccessible user", async ({override, override_rewire}) => { initialize(); override_rewire(people, "fetch_users_from_ids_internal", noop); people.add_valid_user_id(1); override(settings_data, "user_can_access_all_other_users", () => false); const [inaccessible_user] = await people.get_or_fetch_users_from_ids([1]); assert.equal(inaccessible_user.user_id, 1); assert.equal(inaccessible_user.is_inaccessible_user, true); }); run_test("get_by_user_id", () => { initialize(); people.add_active_user(maria); const user = people.get_by_user_id(maria.user_id); assert.equal(user.full_name, maria.full_name); assert.throws( () => { people.get_by_user_id(9999); }, { name: "Error", message: "Unknown user_id in get_by_user_id: 9999", }, ); blueslip.expect("error", "User ID: 8888 is valid but not found in people_by_user_id_dict"); people.add_valid_user_id(8888); assert.throws( () => { people.get_by_user_id(8888); }, { name: "Error", message: "Unknown user_id in get_by_user_id: 8888", }, ); }); run_test("is_valid_bot_user", () => { initialize(); people.add_active_user(bot_botson); people.add_active_user(isaac); assert.equal(people.is_valid_bot_user(bot_botson.user_id), true); assert.equal(people.is_valid_bot_user(isaac.user_id), false); assert.equal(people.is_valid_bot_user(me.user_id), false); // Unknown user_id returns false without error (uses ignore_missing). assert.equal(people.is_valid_bot_user(9999), false); }); run_test("maybe_get_user_by_id", () => { initialize(); people.add_active_user(isaac); const user = people.maybe_get_user_by_id(isaac.user_id); assert.equal(user.full_name, "Isaac Newton"); // With ignore_missing=true, returns undefined without error. assert.equal(people.maybe_get_user_by_id(9999, true), undefined); // Without ignore_missing, logs an error and returns undefined. blueslip.expect("error", "Unknown user_id in maybe_get_user_by_id"); assert.equal(people.maybe_get_user_by_id(9999), undefined); }); run_test("small_avatar_url_for_person", () => { initialize(); people.add_active_user(charles); people.add_active_user(maria); // User with an explicit avatar_url returns it directly. assert.equal(people.small_avatar_url_for_person(charles), "http://charles.com/foo.png"); // Current user (me) has no avatar_url set, so falls back to /avatar/{user_id}. assert.equal(people.small_avatar_url_for_person(me), `/avatar/${me.user_id}`); // User with avatar_url=null computes a gravatar URL from email. assert.equal( people.small_avatar_url_for_person(maria), "https://secure.gravatar.com/avatar/6dbdd7946b58d8b11351fcb27e5cdd55?d=identicon", ); // Verify the gravatar URL was cached on the person object. assert.equal( maria.avatar_url, "https://secure.gravatar.com/avatar/6dbdd7946b58d8b11351fcb27e5cdd55?d=identicon", ); }); run_test("sender_is_bot", () => { initialize(); people.add_active_user(bot_botson); people.add_active_user(isaac); assert.equal(people.sender_is_bot({sender_id: bot_botson.user_id}), true); assert.equal(people.sender_is_bot({sender_id: isaac.user_id}), false); assert.equal(people.sender_is_bot({sender_id: me.user_id}), false); assert.equal(people.sender_is_bot({sender_id: undefined}), false); }); run_test("sender_is_guest", () => { initialize(); people.add_active_user(guest); people.add_active_user(isaac); assert.equal(people.sender_is_guest({sender_id: guest.user_id}), true); assert.equal(people.sender_is_guest({sender_id: isaac.user_id}), false); assert.equal(people.sender_is_guest({sender_id: me.user_id}), false); assert.equal(people.sender_is_guest({sender_id: undefined}), false); }); run_test("get_user_mentions_for_display", () => { people.add_active_user(stephen1); people.add_active_user(stephen2); people.add_active_user(maria); assert.equal(people.get_user_mentions_for_display([stephen1]), "@**Stephen King|601**"); assert.equal( people.get_user_mentions_for_display([stephen1, stephen2]), "@**Stephen King|601** and @**Stephen King|602**", ); // User mentions are sorted. assert.equal( people.get_user_mentions_for_display([stephen1, stephen2, maria]), "@**Maria Athens**, @**Stephen King|601**, and @**Stephen King|602**", ); assert.equal( people.get_user_mentions_for_display([stephen1, stephen2, maria], true), "@_**Maria Athens**, @_**Stephen King|601**, and @_**Stephen King|602**", ); });