mattermost-mobile/app/queries/servers/team.test.ts
Daniel Espino García 9fcc6e580d
Fix GM/DM unread threads highlighting all teams as unreads (#8905)
* Fix GM/DM unread threads highlighting all teams as unreads

* Use options object instead of boolean flags

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2025-06-10 15:30:58 +02:00

1519 lines
56 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import {processReceivedThreads} from '@actions/local/thread';
import {ActionType, Config, Preferences, Screens} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import TestHelper from '@test/test_helper';
import {prepareAllMyChannels} from './channel';
import {getTeamHistory} from './system';
import {
getCurrentTeam,
addChannelToTeamHistory,
getTeamById,
observeTeam,
getTeamChannelHistory,
removeChannelFromTeamHistory,
getMyTeamById,
observeMyTeam,
getTeamByName,
queryTeamSearchHistoryByTeamId,
getTeamSearchHistoryById,
removeTeamFromTeamHistory,
addTeamToTeamHistory,
getNthLastChannelFromTeam,
getLastTeam,
observeMyTeamRoles,
queryJoinedTeams,
queryMyTeams,
queryMyTeamsByIds,
queryOtherTeams,
queryTeamsById,
deleteMyTeams,
getAvailableTeamIds,
observeCurrentTeam,
prepareDeleteTeam,
prepareMyTeams,
queryTeamByName,
getDefaultTeamId,
observeIsTeamUnread,
observeSortedJoinedTeams,
observeTeamLastChannelId,
} from './team';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type {Database} from '@nozbe/watermelondb';
const BASE_TEAM = {
create_at: 0,
update_at: 0,
delete_at: 0,
description: '',
email: '',
type: 'O',
company_name: '',
allowed_domains: '',
invite_id: '',
allow_open_invite: false,
scheme_id: '',
group_constrained: null,
last_team_icon_update: 0,
};
const BASE_CHANNEL = {
create_at: 0,
update_at: 0,
delete_at: 0,
display_name: '',
name: '',
header: '',
purpose: '',
last_post_at: 0,
total_msg_count: 0,
extra_update_at: 0,
creator_id: '',
scheme_id: null,
group_constrained: null,
shared: false,
};
const BASE_USER = {
create_at: 0,
update_at: 0,
delete_at: 0,
auth_service: '',
email: '',
nickname: '',
first_name: '',
last_name: '',
position: '',
notify_props: {
auto_responder_active: undefined,
auto_responder_message: undefined,
channel: 'true',
comments: 'any',
desktop: 'mention',
desktop_notification_sound: undefined,
desktop_sound: 'true',
email: 'true',
first_name: 'true',
mark_unread: undefined,
mention_keys: '',
highlight_keys: '',
push: 'mention',
push_status: 'ooo',
user_id: undefined,
push_threads: undefined,
email_threads: undefined,
calls_desktop_sound: 'true',
calls_notification_sound: '',
calls_mobile_sound: '',
calls_mobile_notification_sound: '',
},
};
describe('Team Queries', () => {
const serverUrl = 'baseHandler.test.com';
const teamId = 'team1';
let database: Database;
let operator: ServerDataOperator;
const createTestTeam = (id: string, name = 'team1', displayName = 'Team 1') => ({
...BASE_TEAM,
id,
name,
display_name: displayName,
} as Team);
const createTestChannel = (id: string, _teamId: string) => ({
...BASE_CHANNEL,
id,
team_id: _teamId,
type: 'O',
} as Channel);
const createTestUser = (id: string, username: string, roles = 'system_user', locale = 'en') => ({
...BASE_USER,
id,
username,
roles,
locale,
} as UserProfile);
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = serverDatabaseAndOperator.database;
operator = serverDatabaseAndOperator.operator;
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
});
describe('getCurrentTeam', () => {
it('should return undefined if no current team id', async () => {
const team = await getCurrentTeam(database);
expect(team).toBeUndefined();
});
it('should return team if current team id exists', async () => {
await operator.handleSystem({
systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}],
prepareRecordsOnly: false,
});
await operator.handleTeam({
teams: [createTestTeam(teamId)],
prepareRecordsOnly: false,
});
const team = await getCurrentTeam(database);
expect(team?.id).toBe(teamId);
});
});
describe('addChannelToTeamHistory', () => {
it('should add channel to empty team history', async () => {
const channelId = 'channel1';
await operator.handleMyChannel({
myChannels: [{
id: channelId,
channel_id: channelId,
user_id: 'user1',
notify_props: {},
} as ChannelMembership],
channels: [createTestChannel(channelId, teamId)],
prepareRecordsOnly: false,
});
const result = await addChannelToTeamHistory(operator, teamId, channelId);
expect(result.length).toBeGreaterThan(0);
});
it('should add channel to start of existing team history', async () => {
const channelId = 'channel1';
const existingChannelId = 'channel2';
await operator.handleMyChannel({
myChannels: [
{
id: channelId,
roles: '',
channel_id: '',
user_id: '',
last_viewed_at: 0,
msg_count: 0,
mention_count: 0,
notify_props: {},
last_update_at: 0,
},
{
id: existingChannelId,
roles: '',
channel_id: '',
user_id: '',
last_viewed_at: 0,
msg_count: 0,
mention_count: 0,
notify_props: {},
last_update_at: 0,
},
],
channels: [{
id: channelId,
team_id: teamId,
create_at: 0,
update_at: 0,
delete_at: 0,
type: 'O',
display_name: '',
name: '',
header: '',
purpose: '',
last_post_at: 0,
total_msg_count: 0,
extra_update_at: 0,
creator_id: '',
scheme_id: null,
group_constrained: null,
shared: false,
}, {
id: existingChannelId,
team_id: teamId,
create_at: 0,
update_at: 0,
delete_at: 0,
type: 'O',
display_name: '',
name: '',
header: '',
purpose: '',
last_post_at: 0,
total_msg_count: 0,
extra_update_at: 0,
creator_id: '',
scheme_id: null,
group_constrained: null,
shared: false,
}],
prepareRecordsOnly: false,
});
await operator.handleTeamChannelHistory({
teamChannelHistories: [{
id: teamId,
channel_ids: [existingChannelId],
}],
prepareRecordsOnly: false,
});
const result = await addChannelToTeamHistory(operator, teamId, channelId);
expect(result.length).toBeGreaterThan(0);
expect(result[0].channelIds[0]).toEqual(channelId);
});
it('should add GLOBAL_THREADS to team history but skip channel check', async () => {
const channelId = Screens.GLOBAL_THREADS;
const result = await addChannelToTeamHistory(operator, teamId, channelId);
expect(result.length).toBe(1);
const history = await getTeamChannelHistory(database, teamId);
expect(history).toEqual([Screens.GLOBAL_THREADS]);
});
it('should add GLOBAL_DRAFTS to team history but skip channel check', async () => {
const channelId = Screens.GLOBAL_DRAFTS;
const result = await addChannelToTeamHistory(operator, teamId, channelId);
expect(result.length).toBe(1);
const history = await getTeamChannelHistory(database, teamId);
expect(history).toEqual([Screens.GLOBAL_DRAFTS]);
});
});
describe('getTeamChannelHistory', () => {
it('should return empty array for non-existent team', async () => {
const history = await getTeamChannelHistory(database, 'nonexistent');
expect(history).toEqual([]);
});
it('should return channel history for existing team', async () => {
const channelIds = ['channel1', 'channel2'];
await operator.handleTeamChannelHistory({
teamChannelHistories: [{
id: teamId,
channel_ids: channelIds,
}],
prepareRecordsOnly: false,
});
const history = await getTeamChannelHistory(database, teamId);
expect(history).toEqual(channelIds);
});
});
describe('removeChannelFromTeamHistory', () => {
it('should remove channel from team history', async () => {
const channelIds = ['channel1', 'channel2'];
await operator.handleTeamChannelHistory({
teamChannelHistories: [{
id: teamId,
channel_ids: channelIds,
}],
prepareRecordsOnly: false,
});
const result = await removeChannelFromTeamHistory(operator, teamId, 'channel1');
expect(result.length).toBeGreaterThan(0);
const history = await getTeamChannelHistory(database, teamId);
expect(history).toEqual(['channel2']);
});
it('should return empty array if channel not in history', async () => {
const result = await removeChannelFromTeamHistory(operator, teamId, 'nonexistent');
expect(result).toEqual([]);
});
});
describe('getTeamById', () => {
it('should return undefined for non-existent team', async () => {
const team = await getTeamById(database, 'nonexistent');
expect(team).toBeUndefined();
});
it('should return team for existing id', async () => {
await operator.handleTeam({
teams: [createTestTeam(teamId)],
prepareRecordsOnly: false,
});
const team = await getTeamById(database, teamId);
expect(team?.id).toBe(teamId);
});
});
describe('getMyTeamById', () => {
it('should return undefined for non-existent team', async () => {
const team = await getMyTeamById(database, 'nonexistent');
expect(team).toBeUndefined();
});
it('should return myTeam for existing id', async () => {
await operator.handleMyTeam({
myTeams: [{id: teamId, roles: 'team_role'}],
prepareRecordsOnly: false,
});
const team = await getMyTeamById(database, teamId);
expect(team?.id).toBe(teamId);
expect(team?.roles).toBe('team_role');
});
});
describe('getTeamByName', () => {
it('should return undefined for non-existent team', async () => {
const team = await getTeamByName(database, 'nonexistent');
expect(team).toBeUndefined();
});
it('should return team for existing name', async () => {
const teamName = 'team1';
await operator.handleTeam({
teams: [createTestTeam(teamId, teamName)],
prepareRecordsOnly: false,
});
const team = await getTeamByName(database, teamName);
expect(team?.name).toBe(teamName);
});
});
describe('team history management', () => {
it('should add and remove teams from history', async () => {
// Add team to history
await addTeamToTeamHistory(operator, teamId);
const history1 = await getTeamHistory(database);
expect(history1).toContain(teamId);
// Remove team from history
await removeTeamFromTeamHistory(operator, teamId);
const history2 = await getTeamHistory(database);
expect(history2).not.toContain(teamId);
});
it('should handle adding team that is already in history', async () => {
// Add team to history first time
await addTeamToTeamHistory(operator, teamId);
const history1 = await getTeamHistory(database);
expect(history1[0]).toBe(teamId);
// Add same team again - should move to front
await operator.handleSystem({
systems: [{
id: SYSTEM_IDENTIFIERS.TEAM_HISTORY,
value: JSON.stringify(['team2', teamId]),
}],
prepareRecordsOnly: false,
});
await addTeamToTeamHistory(operator, teamId);
const history2 = await getTeamHistory(database);
expect(history2[0]).toBe(teamId);
expect(history2[1]).toBe('team2');
});
it('should handle removing non-existent team from history', async () => {
const result = await removeTeamFromTeamHistory(operator, 'nonexistent');
expect(result).toBeUndefined();
});
it('should handle removing team from empty history', async () => {
const result = await removeTeamFromTeamHistory(operator, 'team1');
expect(result).toBeUndefined();
});
});
describe('getNthLastChannelFromTeam', () => {
it('should get nth channel from team history', async () => {
const channelIds = ['channel1', 'channel2', 'channel3'];
await operator.handleTeamChannelHistory({
teamChannelHistories: [{
id: teamId,
channel_ids: channelIds,
}],
prepareRecordsOnly: false,
});
const secondChannel = await getNthLastChannelFromTeam(database, teamId, 1);
expect(secondChannel).toBe('channel2');
});
});
describe('team search history', () => {
it('should handle team search history operations', async () => {
const model = await operator.handleTeamSearchHistory({
teamSearchHistories: [{
team_id: teamId,
created_at: 123,
term: 'test search',
display_term: 'Test Search',
}],
prepareRecordsOnly: false,
});
const searchHistory = await getTeamSearchHistoryById(database, model[0].id);
expect(searchHistory?.teamId).toBe(teamId);
const searchResults = await queryTeamSearchHistoryByTeamId(database, teamId).fetch();
expect(searchResults.length).toBe(1);
expect(searchResults[0].term).toBe('test search');
});
});
describe('observeTeam', () => {
it('should observe team changes', (done) => {
operator.handleTeam({
teams: [createTestTeam(teamId)],
prepareRecordsOnly: false,
}).then(() => {
observeTeam(database, teamId).subscribe((team) => {
if (team?.id === teamId) {
done();
} else {
done.fail();
}
});
});
}, 1500);
});
describe('observeMyTeam', () => {
it('should observe myTeam changes', (done) => {
operator.handleMyTeam({
myTeams: [{id: teamId, roles: 'team_role'}],
prepareRecordsOnly: false,
}).then(() => {
observeMyTeam(database, teamId).subscribe((myTeam) => {
if (myTeam?.id === teamId && myTeam?.roles === 'team_role') {
done();
} else {
done.fail();
}
});
});
}, 1500);
});
describe('observeMyTeamRoles', () => {
it('should observe team role changes', (done) => {
operator.handleMyTeam({
myTeams: [{id: teamId, roles: 'team_role'}],
prepareRecordsOnly: false,
}).then(() => {
observeMyTeamRoles(database, teamId).subscribe((roles) => {
if (roles === 'team_role') {
done();
} else {
done.fail();
}
});
});
}, 1500);
});
describe('queryTeamsById', () => {
it('should return teams by ids', async () => {
const teamIds = ['team1', 'team2'];
await operator.handleTeam({
teams: [
createTestTeam(teamIds[0], 'team1', 'Team 1'),
createTestTeam(teamIds[1], 'team2', 'Team 2'),
],
prepareRecordsOnly: false,
});
const teams = await queryTeamsById(database, teamIds).fetch();
expect(teams.length).toBe(2);
expect([...teams.map((t) => t.id)].sort()).toEqual([...teamIds].sort());
});
});
describe('queryOtherTeams', () => {
it('should return teams not in provided ids', async () => {
const excludeIds = ['team1', 'team2'];
const includeIds = ['team3', 'team4'];
await operator.handleTeam({
teams: [
createTestTeam(excludeIds[0], 'team1', 'Team 1'),
createTestTeam(excludeIds[1], 'team2', 'Team 2'),
createTestTeam(includeIds[0], 'team3', 'Team 3'),
createTestTeam(includeIds[1], 'team4', 'Team 4'),
],
prepareRecordsOnly: false,
});
const teams = await queryOtherTeams(database, excludeIds).fetch();
expect(teams.length).toBe(2);
expect(teams.map((t) => t.id).sort()).toEqual(includeIds.sort());
});
});
describe('queryJoinedTeams', () => {
it('should return teams user has joined', async () => {
const teamIds = ['team1', 'team2'];
await operator.handleTeam({
teams: [
{
id: teamIds[0],
display_name: 'Team 1',
name: 'team1',
create_at: 0,
update_at: 0,
delete_at: 0,
description: '',
email: '',
type: 'O',
company_name: '',
allowed_domains: '',
invite_id: '',
allow_open_invite: false,
scheme_id: '',
group_constrained: null,
last_team_icon_update: 0,
},
{
id: teamIds[1],
display_name: 'Team 2',
name: 'team2',
create_at: 0,
update_at: 0,
delete_at: 0,
description: '',
email: '',
type: 'O',
company_name: '',
allowed_domains: '',
invite_id: '',
allow_open_invite: false,
scheme_id: '',
group_constrained: null,
last_team_icon_update: 0,
},
{
id: 'team3',
display_name: 'Team 3',
name: 'team3',
create_at: 0,
update_at: 0,
delete_at: 0,
description: '',
email: '',
type: 'O',
company_name: '',
allowed_domains: '',
invite_id: '',
allow_open_invite: false,
scheme_id: '',
group_constrained: null,
last_team_icon_update: 0,
},
],
prepareRecordsOnly: false,
});
await operator.handleMyTeam({
myTeams: [
{id: teamIds[0], roles: 'team_role'},
{id: teamIds[1], roles: 'team_role'},
],
prepareRecordsOnly: false,
});
const teams = await queryJoinedTeams(database).fetch();
expect(teams.length).toBe(2);
expect(teams.map((t) => t.id).sort()).toEqual(teamIds.sort());
});
});
describe('queryMyTeams', () => {
it('should return all my teams', async () => {
const teamIds = ['team1', 'team2'];
await operator.handleMyTeam({
myTeams: [
{id: teamIds[0], roles: 'team_role'},
{id: teamIds[1], roles: 'team_role'},
],
prepareRecordsOnly: false,
});
const teams = await queryMyTeams(database).fetch();
expect(teams.length).toBe(2);
expect(teams.map((t) => t.id).sort()).toEqual(teamIds.sort());
});
});
describe('queryMyTeamsByIds', () => {
it('should return my teams by ids', async () => {
const teamIds = ['team1', 'team2', 'team3'];
const queryIds = ['team1', 'team2'];
await operator.handleMyTeam({
myTeams: teamIds.map((id) => ({id, roles: 'team_role'})),
prepareRecordsOnly: false,
});
const teams = await queryMyTeamsByIds(database, queryIds).fetch();
expect(teams.length).toBe(2);
expect([...teams.map((t) => t.id)].sort()).toEqual([...queryIds].sort());
});
});
describe('getDefaultTeamId', () => {
it('should return undefined when no teams exist', async () => {
const id = await getDefaultTeamId(database);
expect(id).toBeUndefined();
});
it('should respect user locale for sorting', async () => {
const teams = [
{id: 'team1', display_name: 'équipe', name: 'team1'},
{id: 'team2', display_name: 'Zebra', name: 'team2'},
{id: 'team3', display_name: 'Apple', name: 'team3'},
] as Team[];
await operator.handleTeam({teams, prepareRecordsOnly: false});
await operator.handleMyTeam({
myTeams: teams.map((t) => ({id: t.id, roles: 'team_role'})),
prepareRecordsOnly: false,
});
// Create user with French locale
await operator.handleUsers({
users: [createTestUser('me', 'me', 'system_user', 'fr')],
prepareRecordsOnly: false,
});
const id = await getDefaultTeamId(database);
expect(id).toBe('team3'); // Should pick 'Apple' first alphabetically
});
it('should respect team order preference', async () => {
const teams = [
{id: 'team1', display_name: 'Team A', name: 'team1'},
{id: 'team2', display_name: 'Team B', name: 'team2'},
] as Team[];
await operator.handleTeam({teams, prepareRecordsOnly: false});
await operator.handleMyTeam({
myTeams: teams.map((t) => ({id: t.id, roles: 'team_role'})),
prepareRecordsOnly: false,
});
// Set team order preference
await operator.handlePreferences({
preferences: [{
category: Preferences.CATEGORIES.TEAMS_ORDER,
name: '',
user_id: 'me',
value: 'team2,team1',
}],
prepareRecordsOnly: false,
});
const id = await getDefaultTeamId(database);
expect(id).toBe('team2');
});
it('should respect experimental primary team setting', async () => {
const teams = [
{id: 'team1', display_name: 'Team A', name: 'team1'},
{id: 'team2', display_name: 'Team B', name: 'team2'},
] as Team[];
await operator.handleTeam({teams, prepareRecordsOnly: false});
await operator.handleMyTeam({
myTeams: teams.map((t) => ({id: t.id, roles: 'team_role'})),
prepareRecordsOnly: false,
});
await operator.handleConfigs({
configs: [
{id: 'ExperimentalPrimaryTeam', value: 'team2'},
],
configsToDelete: [],
prepareRecordsOnly: false,
});
const id = await getDefaultTeamId(database);
expect(id).toBe('team2');
});
it('should ignore specified team id', async () => {
const teams = [
{id: 'team1', display_name: 'Team A', name: 'team1'},
{id: 'team2', display_name: 'Team B', name: 'team2'},
] as Team[];
await operator.handleTeam({teams, prepareRecordsOnly: false});
await operator.handleMyTeam({
myTeams: teams.map((t) => ({id: t.id, roles: 'team_role'})),
prepareRecordsOnly: false,
});
const id = await getDefaultTeamId(database, 'team1');
expect(id).toBe('team2');
});
});
describe('getLastTeam', () => {
it('should return first team from history', async () => {
await operator.handleSystem({
systems: [{
id: SYSTEM_IDENTIFIERS.TEAM_HISTORY,
value: JSON.stringify([teamId, 'team2']),
}],
prepareRecordsOnly: false,
});
const lastTeam = await getLastTeam(database);
expect(lastTeam).toBe(teamId);
});
it('should return default team if no history', async () => {
await operator.handleTeam({
teams: [createTestTeam(teamId)],
prepareRecordsOnly: false,
});
await operator.handleMyTeam({
myTeams: [{id: teamId, roles: 'team_role'}],
prepareRecordsOnly: false,
});
const lastTeam = await getLastTeam(database);
expect(lastTeam).toBe(teamId);
});
});
describe('prepareMyTeams', () => {
it('should prepare team records', async () => {
const teams = [
{id: 'team1', display_name: 'Team 1', name: 'team1'},
{id: 'team2', display_name: 'Team 2', name: 'team2'},
] as Team[];
const memberships = [
{team_id: 'team1', roles: 'team_role', delete_at: 0},
{team_id: 'team2', roles: 'team_role', delete_at: 0},
{team_id: 'team3', roles: 'team_role', delete_at: 0}, // Should be filtered out
] as TeamMembership[];
const records = await prepareMyTeams(operator, teams, memberships);
expect(records.length).toBe(3); // teamRecords, membershipRecords, myTeamRecords
expect(records[0]).toBeTruthy();
expect(records[1]).toBeTruthy();
expect(records[2]).toBeTruthy();
});
it('should return empty array on error', async () => {
const records = await prepareMyTeams(operator, null as any, null as any);
expect(records).toEqual([]);
});
});
describe('deleteMyTeams', () => {
it('should delete my teams', async () => {
const teamIds = ['team1', 'team2'];
const myTeams = await operator.handleMyTeam({
myTeams: teamIds.map((id) => ({id, roles: 'team_role'})),
prepareRecordsOnly: false,
});
const result = await deleteMyTeams(operator, myTeams);
expect(result).toEqual({});
const remaining = await queryMyTeams(database).fetch();
expect(remaining.length).toBe(0);
});
it('should handle errors', async () => {
const result = await deleteMyTeams(operator, null as any);
expect(result).toHaveProperty('error');
});
});
describe('prepareDeleteTeam', () => {
it('should prepare delete records for team and related entities', async () => {
// Setup team with related records
const channelId = 'channel1';
const categoryId = 'category1';
// Create team and basic relations
await operator.handleTeam({
teams: [createTestTeam(teamId)],
prepareRecordsOnly: false,
});
await operator.handleMyTeam({
myTeams: [{id: teamId, roles: 'team_role'}],
prepareRecordsOnly: false,
});
await operator.handleTeamChannelHistory({
teamChannelHistories: [{id: teamId, channel_ids: [channelId]}],
prepareRecordsOnly: false,
});
// Add members
await operator.handleTeamMemberships({
teamMemberships: [{
team_id: teamId,
user_id: 'user1',
mention_count: 0,
msg_count: 0,
roles: '',
delete_at: 0,
scheme_user: false,
scheme_admin: false,
}],
prepareRecordsOnly: false,
});
// Add search histories
await operator.handleTeamSearchHistory({
teamSearchHistories: [{
team_id: teamId,
term: 'search',
display_term: 'Search',
created_at: 0,
}],
prepareRecordsOnly: false,
});
// Add categories
await operator.handleCategories({
categories: [{
id: categoryId,
team_id: teamId,
type: 'custom',
display_name: 'Custom Category',
sort_order: 0,
sorting: 'alpha',
muted: false,
collapsed: false,
}],
prepareRecordsOnly: false,
});
// Add channels
await operator.handleChannel({
channels: [createTestChannel(channelId, teamId)],
prepareRecordsOnly: false,
});
const team = await getTeamById(database, teamId);
const records = await prepareDeleteTeam(team!);
expect(records.length).toBeGreaterThan(0);
});
it('should handle failed relation fetches', async () => {
await operator.handleTeam({
teams: [createTestTeam(teamId)],
prepareRecordsOnly: false,
});
const team = await getTeamById(database, teamId);
const records = await prepareDeleteTeam(team!);
expect(records.length).toBe(1); // Just the team record
});
it('should handle failed children fetches', async () => {
// Create team but make relations throw errors by using invalid IDs
await operator.handleTeam({
teams: [createTestTeam(teamId)],
prepareRecordsOnly: false,
});
await operator.handleTeamMemberships({
teamMemberships: [{
team_id: 'invalid',
user_id: 'user1',
mention_count: 0,
msg_count: 0,
roles: '',
delete_at: 0,
scheme_user: false,
scheme_admin: false,
}],
prepareRecordsOnly: false,
});
const team = await getTeamById(database, teamId);
const records = await prepareDeleteTeam(team!);
expect(records.length).toBe(1); // Just the team record
});
it('should return empty array on error', async () => {
const records = await prepareDeleteTeam(null as any);
expect(records).toEqual([]);
});
});
describe('queryTeamByName', () => {
it('should query team by name', async () => {
const teamName = 'team1';
await operator.handleTeam({
teams: [createTestTeam(teamId, teamName)],
prepareRecordsOnly: false,
});
const teams = await queryTeamByName(database, teamName).fetch();
expect(teams.length).toBe(1);
expect(teams[0].name).toBe(teamName);
});
});
describe('getAvailableTeamIds', () => {
it('should return available team ids excluding specified team', async () => {
const teams = [
{id: 'team1', display_name: 'Team 1', name: 'team1'},
{id: 'team2', display_name: 'Team 2', name: 'team2'},
] as Team[];
await operator.handleTeam({teams, prepareRecordsOnly: false});
await operator.handleMyTeam({
myTeams: teams.map((t) => ({id: t.id, roles: 'team_role'})),
prepareRecordsOnly: false,
});
const availableIds = await getAvailableTeamIds(database, 'team1');
expect(availableIds).toEqual(['team2']);
});
it('should handle provided teams and preferences', async () => {
const teams = [
{id: 'team1', display_name: 'Team 1', name: 'team1'},
{id: 'team2', display_name: 'Team 2', name: 'team2'},
] as Team[];
const prefs = [{
category: Preferences.CATEGORIES.TEAMS_ORDER,
name: '',
user_id: 'me',
value: 'team2,team1',
}];
await operator.handleTeam({teams, prepareRecordsOnly: false});
const availableIds = await getAvailableTeamIds(database, 'team1', teams, prefs);
expect(availableIds).toEqual(['team2']);
});
});
describe('observeCurrentTeam', () => {
it('should observe current team changes', (done) => {
Promise.all([
operator.handleSystem({
systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}],
prepareRecordsOnly: false,
}),
operator.handleTeam({
teams: [createTestTeam(teamId)],
prepareRecordsOnly: false,
}),
]).then(() => {
observeCurrentTeam(database).subscribe((team) => {
if (team?.id === teamId) {
done();
} else {
done.fail();
}
});
});
}, 1500);
});
describe('observeIsTeamUnread', () => {
const userId = 'user_id';
const waitTime = 50;
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = serverDatabaseAndOperator.database;
operator = serverDatabaseAndOperator.operator;
await operator.handleSystem({
systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: userId}],
prepareRecordsOnly: false,
});
await operator.handleConfigs({
configs: [
{id: 'CollapsedThreads', value: Config.ALWAYS_ON},
{id: 'Version', value: '7.6.0'},
],
configsToDelete: [],
prepareRecordsOnly: false,
});
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
});
it('should return true when there are channel unreads', async () => {
const subscriptionNext = jest.fn();
const notify_props = {mark_unread: 'all' as const};
const result = observeIsTeamUnread(database, teamId);
result.subscribe({next: subscriptionNext});
await TestHelper.wait(waitTime);
// The subscription always return the first value
expect(subscriptionNext).toHaveBeenCalledWith(false);
subscriptionNext.mockClear();
// Setup initial state - channel with no unreads
let models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 20})],
[TestHelper.fakeMyChannel({
channel_id: 'channel1',
user_id: userId,
notify_props,
msg_count: 20,
})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
await TestHelper.wait(waitTime);
// No change
expect(subscriptionNext).not.toHaveBeenCalled();
// Update channel with new messages
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({
channel_id: 'channel1',
user_id: userId,
notify_props,
msg_count: 20,
})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
await TestHelper.wait(waitTime);
expect(subscriptionNext).toHaveBeenCalledWith(true);
});
it('should return true when there are thread unreads', async () => {
const subscriptionNext = jest.fn();
const channelId = 'channel1';
const threadId = 'thread1';
const result = observeIsTeamUnread(database, teamId);
result.subscribe({next: subscriptionNext});
await TestHelper.wait(waitTime);
// The subscription always return the first value
expect(subscriptionNext).toHaveBeenCalledWith(false);
subscriptionNext.mockClear();
const channelModels = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: channelId, team_id: teamId, total_msg_count: 20})],
[TestHelper.fakeMyChannel({
channel_id: channelId,
user_id: userId,
msg_count: 20,
})],
false,
)))).flat();
const threadModels = await processReceivedThreads(serverUrl, [TestHelper.fakeThread({
id: threadId,
reply_count: 1,
unread_replies: 0,
post: TestHelper.fakePost({id: threadId, channel_id: channelId}),
is_following: true,
})], teamId, true);
await operator.batchRecords([...channelModels, ...threadModels.models!], 'test');
// No change
expect(subscriptionNext).not.toHaveBeenCalled();
await processReceivedThreads(serverUrl, [TestHelper.fakeThread({
id: threadId,
reply_count: 1,
unread_replies: 1,
post: TestHelper.fakePost({id: threadId, channel_id: channelId}),
is_following: true,
})], teamId, false);
expect(subscriptionNext).toHaveBeenCalledWith(true);
});
it('changes in other teams should not trigger the subscription', async () => {
const subscriptionNext = jest.fn();
const channelId = 'channel1';
const threadId = 'thread1';
const otherTeamId = 'other_team';
const result = observeIsTeamUnread(database, teamId);
result.subscribe({next: subscriptionNext});
await TestHelper.wait(waitTime);
// The subscription always return the first value
expect(subscriptionNext).toHaveBeenCalledWith(false);
subscriptionNext.mockClear();
let channelModels = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: channelId, team_id: otherTeamId, total_msg_count: 20})],
[TestHelper.fakeMyChannel({
channel_id: channelId,
user_id: userId,
msg_count: 20,
})],
false,
)))).flat();
let threadModels = await processReceivedThreads(serverUrl, [TestHelper.fakeThread({
id: threadId,
reply_count: 1,
unread_replies: 0,
post: TestHelper.fakePost({id: threadId, channel_id: channelId}),
is_following: true,
})], teamId, true);
await operator.batchRecords([...channelModels, ...threadModels.models!], 'test');
await TestHelper.wait(waitTime);
expect(subscriptionNext).not.toHaveBeenCalled();
channelModels = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: channelId, team_id: otherTeamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({
channel_id: channelId,
user_id: userId,
msg_count: 20,
})],
false,
)))).flat();
threadModels = await processReceivedThreads(serverUrl, [TestHelper.fakeThread({
id: threadId,
reply_count: 1,
unread_replies: 1,
post: TestHelper.fakePost({id: threadId, channel_id: channelId}),
is_following: true,
})], teamId, true);
await operator.batchRecords([...channelModels, ...threadModels.models!], 'test');
await TestHelper.wait(waitTime);
expect(subscriptionNext).not.toHaveBeenCalled();
});
it('should not consider the team unread when a gm or a dm are unread', async () => {
const subscriptionNext = jest.fn();
const notify_props = {mark_unread: 'all' as const};
const result = observeIsTeamUnread(database, teamId);
result.subscribe({next: subscriptionNext});
await TestHelper.wait(waitTime);
// The subscription always return the first value
expect(subscriptionNext).toHaveBeenCalledWith(false);
subscriptionNext.mockClear();
// Setup DM channel with unreads
let channelModels = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'dm1', team_id: '', type: 'D', total_msg_count: 30})],
[TestHelper.fakeMyChannel({
channel_id: 'dm1',
user_id: userId,
notify_props,
msg_count: 20,
})],
false,
)))).flat();
await operator.batchRecords([...channelModels], 'test');
await TestHelper.wait(waitTime);
expect(subscriptionNext).not.toHaveBeenCalled();
// Setup GM channel with unreads
channelModels = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'gm1', team_id: '', type: 'G', total_msg_count: 30})],
[TestHelper.fakeMyChannel({
channel_id: 'gm1',
user_id: userId,
notify_props,
msg_count: 20,
})],
false,
)))).flat();
await operator.batchRecords([...channelModels], 'test');
await TestHelper.wait(waitTime);
expect(subscriptionNext).not.toHaveBeenCalled();
});
it('should not consider the team unread if a thread in a dm or a gm has unread messages', async () => {
const subscriptionNext = jest.fn();
const notify_props = {mark_unread: 'all' as const};
const result = observeIsTeamUnread(database, teamId);
result.subscribe({next: subscriptionNext});
await TestHelper.wait(waitTime);
// The subscription always return the first value
expect(subscriptionNext).toHaveBeenCalledWith(false);
subscriptionNext.mockClear();
// Setup DM channel with thread
let channelModels = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'dm1', team_id: '', type: 'D', total_msg_count: 20})],
[TestHelper.fakeMyChannel({
channel_id: 'dm1',
user_id: userId,
notify_props,
msg_count: 20,
})],
false,
)))).flat();
const dmPost = TestHelper.fakePost({id: 'dm_thread', channel_id: 'dm1'});
const dmThreads = [{
...TestHelper.fakeThread({
id: 'dm_thread',
participants: undefined,
reply_count: 2,
last_reply_at: 123,
is_following: true,
unread_replies: 2,
unread_mentions: 1,
}),
lastFetchedAt: 0,
}];
const dmModels = await operator.handleThreads({threads: dmThreads, prepareRecordsOnly: false});
let postModels = await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
order: [],
posts: [dmPost],
prepareRecordsOnly: true,
});
await operator.batchRecords([...channelModels, ...dmModels, ...postModels], 'test');
await TestHelper.wait(waitTime);
expect(subscriptionNext).not.toHaveBeenCalled();
// Setup GM channel with thread
channelModels = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'gm1', team_id: '', type: 'G', total_msg_count: 20})],
[TestHelper.fakeMyChannel({
channel_id: 'gm1',
user_id: userId,
notify_props,
msg_count: 20,
})],
false,
)))).flat();
const gmPost = TestHelper.fakePost({id: 'gm_thread', channel_id: 'gm1'});
const gmThreads = [{
...TestHelper.fakeThread({
id: 'gm_thread',
participants: undefined,
reply_count: 3,
last_reply_at: 123,
is_following: true,
unread_replies: 3,
unread_mentions: 2,
}),
lastFetchedAt: 123,
}];
const gmModels = await operator.handleThreads({threads: gmThreads, prepareRecordsOnly: false});
postModels = await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
order: [],
posts: [gmPost],
prepareRecordsOnly: true,
});
await operator.batchRecords([...channelModels, ...gmModels, ...postModels], 'test');
await TestHelper.wait(waitTime);
expect(subscriptionNext).not.toHaveBeenCalled();
});
});
describe('observeSortedJoinedTeams', () => {
it('should return teams sorted by preference order', (done) => {
const teamIds = ['team1', 'team2', 'team3'];
const teams = teamIds.map((id, index) => createTestTeam(id, `team${index + 1}`, `Team ${index + 1}`));
const myTeams = teamIds.map((id) => ({id, roles: 'team_role'}));
operator.handleTeam({teams, prepareRecordsOnly: false}).then(() => {
operator.handleMyTeam({myTeams, prepareRecordsOnly: false}).then(() => {
operator.handlePreferences({
preferences: [{
category: Preferences.CATEGORIES.TEAMS_ORDER,
name: '',
user_id: 'me',
value: 'team2,team1,team3',
}],
prepareRecordsOnly: false,
}).then(() => {
observeSortedJoinedTeams(database).subscribe((sortedTeams) => {
expect(sortedTeams.map((t) => t.id)).toEqual(['team2', 'team1', 'team3']);
done();
});
});
});
});
});
it('should return teams sorted alphabetically if no preference order', (done) => {
const teamIds = ['team1', 'team2', 'team3'];
const teams = [
createTestTeam('team1', 'team1', 'Zebra'),
createTestTeam('team2', 'team2', 'Apple'),
createTestTeam('team3', 'team3', 'Banana'),
];
const myTeams = teamIds.map((id) => ({id, roles: 'team_role'}));
operator.handleTeam({teams, prepareRecordsOnly: false}).then(() => {
operator.handleMyTeam({myTeams, prepareRecordsOnly: false}).then(() => {
observeSortedJoinedTeams(database).subscribe((sortedTeams) => {
expect(sortedTeams.map((t) => t.displayName)).toEqual(['Apple', 'Banana', 'Zebra']);
done();
});
});
});
});
it('should return teams sorted alphabetically if preference order is empty', (done) => {
const teamIds = ['team1', 'team2', 'team3'];
const teams = [
createTestTeam('team1', 'team1', 'Zebra'),
createTestTeam('team2', 'team2', 'Apple'),
createTestTeam('team3', 'team3', 'Banana'),
];
const myTeams = teamIds.map((id) => ({id, roles: 'team_role'}));
operator.handleTeam({teams, prepareRecordsOnly: false}).then(() => {
operator.handleMyTeam({myTeams, prepareRecordsOnly: false}).then(() => {
operator.handlePreferences({
preferences: [{
category: Preferences.CATEGORIES.TEAMS_ORDER,
name: '',
user_id: 'me',
value: '',
}],
prepareRecordsOnly: false,
}).then(() => {
observeSortedJoinedTeams(database).subscribe((sortedTeams) => {
expect(sortedTeams.map((t) => t.displayName)).toEqual(['Apple', 'Banana', 'Zebra']);
done();
});
});
});
});
});
it('should return teams sorted alphabetically if preference order does not match any team', (done) => {
const teamIds = ['team1', 'team2', 'team3'];
const teams = [
createTestTeam('team1', 'team1', 'Zebra'),
createTestTeam('team2', 'team2', 'Apple'),
createTestTeam('team3', 'team3', 'Banana'),
];
const myTeams = teamIds.map((id) => ({id, roles: 'team_role'}));
operator.handleTeam({teams, prepareRecordsOnly: false}).then(() => {
operator.handleMyTeam({myTeams, prepareRecordsOnly: false}).then(() => {
operator.handlePreferences({
preferences: [{
category: Preferences.CATEGORIES.TEAMS_ORDER,
name: '',
user_id: 'me',
value: 'team4,team5',
}],
prepareRecordsOnly: false,
}).then(() => {
observeSortedJoinedTeams(database).subscribe((sortedTeams) => {
expect(sortedTeams.map((t) => t.displayName)).toEqual(['Apple', 'Banana', 'Zebra']);
done();
});
});
});
});
});
});
describe('observeTeamLastChannelId', () => {
it('should return undefined if no team channel history exists', (done) => {
observeTeamLastChannelId(database, teamId).subscribe((channelId) => {
expect(channelId).toBeUndefined();
done();
});
});
it('should return the last channel id from team channel history', async () => {
const channelId = 'channel1';
await operator.handleTeamChannelHistory({
teamChannelHistories: [{
id: teamId,
channel_ids: [channelId],
}],
prepareRecordsOnly: false,
});
observeTeamLastChannelId(database, teamId).subscribe((lastChannelId) => {
expect(lastChannelId).toBe(channelId);
});
});
it('should update when team channel history changes', async () => {
const initialChannelId = 'channel1';
const newChannelId = 'channel2';
await operator.handleTeamChannelHistory({
teamChannelHistories: [{
id: teamId,
channel_ids: [initialChannelId],
}],
prepareRecordsOnly: false,
});
const subscription = observeTeamLastChannelId(database, teamId).subscribe((lastChannelId) => {
if (lastChannelId === initialChannelId) {
operator.handleTeamChannelHistory({
teamChannelHistories: [{
id: teamId,
channel_ids: [newChannelId],
}],
prepareRecordsOnly: false,
});
} else if (lastChannelId === newChannelId) {
expect(lastChannelId).toBe(newChannelId);
subscription.unsubscribe();
}
});
});
});
});