mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-10 05:00:13 +00:00
* MM-68219 - Add redacted files placeholder and related functionality * add feature flag control and e2e tests * adjust placeholder design, polish code, fix permalink preview issue * polish code and add missing test * use import path with right path * gate post refetch behind the feature flag, refetch the open thread and avoid observeCount * restore filesInfo for the permalink files prop added upstream * fix stale redaction on background servers, consume the flag on every fetch path and share the viewport emitter * replace the in-memory redaction flag with a persisted per-post epoch so ABAC file decisions survive restarts and stale posts stay hidden * raise the redaction epoch on every resync, capture the epoch counter, only invalidate channels on role changes, and store the newer epoch when a re-verified post is otherwise unchanged * store superseded responses unverified instead of dropping them, keep the since-fetch when re-verifying, verify permalink embeds against the linked channel, and capture the websocket epoch on arrival * nvalidate file decisions when a user attribute field changes or all its values are cleared * batch channel invalidations, close file viewers only when affected, reset the epoch on unstamped redaction changes, and only re-verify the newest page's attachment posts * re-check older channel posts a page at a time instead of one request per post * skip the unstamped epoch lookup for posts with no files, redacted count or permalink * keep the attribute-view retry after later invalidations, retry linked posts unless refused for the current epoch, and stop older payloads from rolling back edits via the redaction check * MM-70905 - disable and grey out the attachment button when an ABAC pe… (#10195) * MM-70905 - disable and grey out the attachment button when an ABAC permission policy denies uploads in the channel * only back off render permission fetches when the server request fails, and log other errors without retrying * remove the disableServer helper and have removeServer do both steps * split defaultAllowed into allowWhenUnenforced and allowWhenNotEvaluated, and treat render permissions as unenforced on servers without the decisions API * MM-69627: Add playbook task requirements support on mobile (#10035) * MM-69627: Add playbook task requirements support on mobile Allow filling required task fields when checking off checklist items during a run, gated by the same Playbooks beta setting used on web. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Address PR review feedback for task requirements Propagate settings write failures, harden fill-requirements save/title handling, and add missing unit coverage. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Fix TypeScript errors in new unit tests Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Use defineMessages for fill requirements titles Keep i18n extraction able to find the header titles after moving them to the route. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Address review feedback for task requirements Update checklist items locally when saving requirement values instead of refetching the run, add missing query/navigation tests, and apply small callback/reduce cleanups from review. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Fix task requirements review follow-ups Propagate local checklist write failures, clear a stale task-requirements flag when playbooks is unsupported, and cap fill values at the desktop/ server max length. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Move requirement field into its own component Keep one component per file on the fill-requirements screen. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69122: Add file element support to interactive dialogs (#9898) * Implement Interactive Dialog - file upload * update demo plugin version for e2etests * fix and add e2etests * review fixes * Fix version bump checkout when a tag shares the branch name (#10229) * Fix Android crashes caused by animated custom emoji (#10030) * Route GIF decoding through Glide - Keep APNG4Android for APNG, animated WebP, and animated AVIF support. - Prepend Glide's GIF decoders to avoid APNG4Android's native GIF overflow. * Clear Expo image resources with Glide targets - Detach drawables when Glide clears their custom target. - Prevent recycled GIF frame resources from remaining attached to Expo image views. * Preserve foreground image during GIF target recycling --------- Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Nuno Simões <nuno.simoes@mattermost.com> Co-authored-by: MangelSpec <74370284+MangelSpec@users.noreply.github.com> * detox - fix clobbering of feature flags that cause failures in other suites. Also small fix of a incorrectly set test-id --------- Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Nuno Simões <nuno.simoes@mattermost.com> Co-authored-by: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Co-authored-by: Nate Schlossberg <nate.schlossberg@mattermost.com>
840 lines
30 KiB
TypeScript
840 lines
30 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||
// See LICENSE.txt for license information.
|
||
|
||
import {createIntl} from 'react-intl';
|
||
import {Alert} from 'react-native';
|
||
|
||
import {getUsersCountFromMentions} from '@actions/local/post';
|
||
import {General, Post} from '@constants';
|
||
import {mockedPosts} from '@database/operator/utils/mock';
|
||
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
||
import {getUserById} from '@queries/servers/user';
|
||
import TestHelper from '@test/test_helper';
|
||
import {toMilliseconds} from '@utils/datetime';
|
||
|
||
import {
|
||
areConsecutivePosts,
|
||
hasAiGeneratedMetadata,
|
||
isFromWebhook,
|
||
isEdited,
|
||
isPostEphemeral,
|
||
restoreEphemeralIdentityFieldsForEdit,
|
||
isPostFailed,
|
||
isPostPendingOrFailed,
|
||
isSystemMessage,
|
||
fromAutoResponder,
|
||
postUserDisplayName,
|
||
shouldIgnorePost,
|
||
processPostsFetched,
|
||
getLastFetchedAtFromPosts,
|
||
moreThan5minAgo,
|
||
hasSpecialMentions,
|
||
persistentNotificationsConfirmation,
|
||
scheduledPostFromPost,
|
||
hasInteractivePostContent,
|
||
isPermalinkEmbedRedacted,
|
||
getPermalinkRedactedFileCount,
|
||
isAttachmentDecisionCurrent,
|
||
} from './index';
|
||
|
||
jest.mock('@actions/local/post', () => ({
|
||
getUsersCountFromMentions: jest.fn(),
|
||
}));
|
||
|
||
jest.mock('@queries/servers/user', () => ({
|
||
getUserById: jest.fn(),
|
||
}));
|
||
|
||
jest.mock('@database/manager', () => ({
|
||
getServerDatabaseAndOperator: jest.fn().mockReturnValue({
|
||
database: {},
|
||
}),
|
||
}));
|
||
|
||
describe('post utils', () => {
|
||
describe('hasAiGeneratedMetadata', () => {
|
||
it('should return true when both ai_generated_by and ai_generated_by_username are non-empty strings', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {
|
||
ai_generated_by: 'bot-user-id',
|
||
ai_generated_by_username: 'ai-bot',
|
||
},
|
||
});
|
||
|
||
expect(hasAiGeneratedMetadata(post)).toBe(true);
|
||
});
|
||
|
||
it('should return false when metadata is missing or invalid', () => {
|
||
expect(hasAiGeneratedMetadata(TestHelper.fakePostModel({
|
||
props: {ai_generated_by_username: 'ai-bot'},
|
||
}))).toBe(false);
|
||
expect(hasAiGeneratedMetadata(TestHelper.fakePostModel({
|
||
props: {ai_generated_by: 'bot-user-id'},
|
||
}))).toBe(false);
|
||
expect(hasAiGeneratedMetadata(TestHelper.fakePostModel({
|
||
props: {ai_generated_by: '', ai_generated_by_username: ''},
|
||
}))).toBe(false);
|
||
expect(hasAiGeneratedMetadata(TestHelper.fakePostModel({
|
||
props: {ai_generated_by: 123, ai_generated_by_username: true},
|
||
}))).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('areConsecutivePosts', () => {
|
||
it('should return true for consecutive posts from the same user within the collapse timeout', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
userId: 'user1',
|
||
createAt: 1000,
|
||
props: {},
|
||
});
|
||
const previousPost = TestHelper.fakePostModel({
|
||
userId: 'user1',
|
||
createAt: 500,
|
||
props: {},
|
||
});
|
||
|
||
const result = areConsecutivePosts(post, previousPost, 'en');
|
||
expect(result).toBe(true);
|
||
});
|
||
|
||
it('should return false for posts from different users', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
userId: 'user1',
|
||
createAt: 1000,
|
||
props: {},
|
||
});
|
||
const previousPost = TestHelper.fakePostModel({
|
||
userId: 'user2',
|
||
createAt: 500,
|
||
props: {},
|
||
});
|
||
|
||
const result = areConsecutivePosts(post, previousPost, 'en');
|
||
expect(result).toBe(false);
|
||
});
|
||
|
||
it('should return false for normal post after AI post from same user', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
userId: 'user1',
|
||
createAt: 1000,
|
||
props: {},
|
||
});
|
||
const previousPost = TestHelper.fakePostModel({
|
||
userId: 'user1',
|
||
createAt: 500,
|
||
props: {
|
||
ai_generated_by: 'user1',
|
||
ai_generated_by_username: 'ai-bot',
|
||
},
|
||
});
|
||
|
||
const result = areConsecutivePosts(post, previousPost, 'en');
|
||
expect(result).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('isFromWebhook', () => {
|
||
it('should return true for posts from a webhook', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {
|
||
from_webhook: 'true',
|
||
},
|
||
});
|
||
|
||
const result = isFromWebhook(post);
|
||
expect(result).toBe(true);
|
||
});
|
||
|
||
it('should return false for posts not from a webhook', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {
|
||
from_webhook: 'false',
|
||
},
|
||
});
|
||
|
||
const result = isFromWebhook(post);
|
||
expect(result).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('isEdited', () => {
|
||
it('should return true if the post is edited', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
editAt: 1000,
|
||
});
|
||
|
||
const result = isEdited(post);
|
||
expect(result).toBe(true);
|
||
});
|
||
|
||
it('should return false if the post is not edited', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
editAt: 0,
|
||
});
|
||
|
||
const result = isEdited(post);
|
||
expect(result).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('isPostEphemeral', () => {
|
||
it('should return true for an ephemeral post', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
type: Post.POST_TYPES.EPHEMERAL,
|
||
});
|
||
|
||
const result = isPostEphemeral(post);
|
||
expect(result).toBe(true);
|
||
});
|
||
|
||
it('should return false for a non-ephemeral post', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
type: '',
|
||
});
|
||
|
||
const result = isPostEphemeral(post);
|
||
expect(result).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('restoreEphemeralIdentityFieldsForEdit', () => {
|
||
it('should preserve create_at and root_id when an edited ephemeral arrives with zero create_at', () => {
|
||
const incoming = TestHelper.fakePost({
|
||
id: 'eph1',
|
||
type: Post.POST_TYPES.EPHEMERAL,
|
||
message: 'after',
|
||
create_at: 0,
|
||
update_at: 9000,
|
||
root_id: '',
|
||
user_id: 'user1',
|
||
});
|
||
|
||
const result = restoreEphemeralIdentityFieldsForEdit(incoming, {
|
||
create_at: 5000,
|
||
root_id: 'root1',
|
||
user_id: 'user1',
|
||
});
|
||
|
||
expect(result.create_at).toBe(5000);
|
||
expect(result.root_id).toBe('root1');
|
||
expect(result.message).toBe('after');
|
||
expect(result.update_at).toBe(9000);
|
||
});
|
||
|
||
it('should preserve user_id when an edited ephemeral arrives with empty user_id', () => {
|
||
const incoming = TestHelper.fakePost({
|
||
id: 'eph1',
|
||
type: Post.POST_TYPES.EPHEMERAL,
|
||
message: 'after',
|
||
create_at: 5000,
|
||
update_at: 9000,
|
||
user_id: '',
|
||
});
|
||
|
||
const result = restoreEphemeralIdentityFieldsForEdit(incoming, {
|
||
create_at: 5000,
|
||
user_id: 'creator1',
|
||
});
|
||
|
||
expect(result.user_id).toBe('creator1');
|
||
expect(result.message).toBe('after');
|
||
});
|
||
});
|
||
|
||
describe('isPostFailed', () => {
|
||
it('should return true if the post has failed prop', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {
|
||
failed: true,
|
||
},
|
||
pendingPostId: 'id',
|
||
id: 'id',
|
||
updateAt: Date.now() - Post.POST_TIME_TO_FAIL - 1000,
|
||
});
|
||
|
||
const result = isPostFailed(post);
|
||
expect(result).toBe(true);
|
||
});
|
||
|
||
it('should return true if the post is pending and the update time has exceeded the failure time', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {},
|
||
pendingPostId: 'id',
|
||
id: 'id',
|
||
updateAt: Date.now() - Post.POST_TIME_TO_FAIL - 1000,
|
||
});
|
||
|
||
const result = isPostFailed(post);
|
||
expect(result).toBe(true);
|
||
});
|
||
|
||
it('should return false if the post is not failed', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {},
|
||
pendingPostId: 'id',
|
||
id: 'id',
|
||
updateAt: Date.now(),
|
||
});
|
||
|
||
const result = isPostFailed(post);
|
||
expect(result).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('isPostPendingOrFailed', () => {
|
||
it('should return true if the post is pending', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
pendingPostId: 'id',
|
||
id: 'id',
|
||
props: {},
|
||
});
|
||
|
||
const result = isPostPendingOrFailed(post);
|
||
expect(result).toBe(true);
|
||
});
|
||
|
||
it('should return true if the post has failed', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
pendingPostId: 'id',
|
||
id: 'id',
|
||
updateAt: Date.now() - Post.POST_TIME_TO_FAIL - 1000,
|
||
props: {},
|
||
});
|
||
|
||
const result = isPostPendingOrFailed(post);
|
||
expect(result).toBe(true);
|
||
});
|
||
|
||
it('should return false if the post is neither pending nor failed', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
pendingPostId: 'differentId',
|
||
id: 'id',
|
||
props: {},
|
||
});
|
||
|
||
const result = isPostPendingOrFailed(post);
|
||
expect(result).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('isSystemMessage', () => {
|
||
it('should return true if the post is a system message', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
type: `${Post.POST_TYPES.SYSTEM_MESSAGE_PREFIX}any_type` as PostType,
|
||
});
|
||
|
||
const result = isSystemMessage(post);
|
||
expect(result).toBe(true);
|
||
});
|
||
|
||
it('should return false if the post is not a system message', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
type: 'add_bot_teams_channels',
|
||
});
|
||
|
||
const result = isSystemMessage(post);
|
||
expect(result).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('hasSpecialMentions', () => {
|
||
test.each([
|
||
['@here where is Jessica Hyde', true],
|
||
['@all where is Jessica Hyde', true],
|
||
['@channel where is Jessica Hyde', true],
|
||
|
||
['where is Jessica Hyde @here', true],
|
||
['where is Jessica Hyde @all', true],
|
||
['where is Jessica Hyde @channel', true],
|
||
|
||
['where is Jessica @here Hyde', true],
|
||
['where is Jessica @all Hyde', true],
|
||
['where is Jessica @channel Hyde', true],
|
||
|
||
['where is Jessica Hyde\n@here', true],
|
||
['where is Jessica Hyde\n@all', true],
|
||
['where is Jessica Hyde\n@channel', true],
|
||
|
||
['where is Jessica\n@here Hyde', true],
|
||
['where is Jessica\n@all Hyde', true],
|
||
['where is Jessica\n@channel Hyde', true],
|
||
|
||
['where is Jessica Hyde @her', false],
|
||
['where is Jessica Hyde @al', false],
|
||
['where is Jessica Hyde @chann', false],
|
||
|
||
['where is Jessica Hyde@here', false],
|
||
['where is Jessica Hyde@all', false],
|
||
['where is Jessica Hyde@channel', false],
|
||
|
||
['where is Jessica @hereHyde', false],
|
||
['where is Jessica @allHyde', false],
|
||
['where is Jessica @channelHyde', false],
|
||
|
||
['@herewhere is Jessica Hyde@here', false],
|
||
['@allwhere is Jessica Hyde@all', false],
|
||
['@channelwhere is Jessica Hyde@channel', false],
|
||
|
||
['where is Jessica Hyde here', false],
|
||
['where is Jessica Hyde all', false],
|
||
['where is Jessica Hyde channel', false],
|
||
|
||
['where is Jessica Hyde', false],
|
||
])('hasSpecialMentions: %s => %s', (message, expected) => {
|
||
expect(hasSpecialMentions(message)).toBe(expected);
|
||
});
|
||
});
|
||
|
||
describe('fromAutoResponder', () => {
|
||
it('should return true if the post is from an auto responder', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
type: Post.POST_TYPES.SYSTEM_AUTO_RESPONDER,
|
||
});
|
||
|
||
const result = fromAutoResponder(post);
|
||
expect(result).toBe(true);
|
||
});
|
||
|
||
it('should return false if the post is not from an auto responder', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
type: 'add_bot_teams_channels',
|
||
});
|
||
|
||
const result = fromAutoResponder(post);
|
||
expect(result).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('persistentNotificationsConfirmation', () => {
|
||
const serverUrl = 'http://server';
|
||
const value = '@user';
|
||
const mentionsList = ['@user'];
|
||
const sendMessage = jest.fn();
|
||
const persistentNotificationMaxRecipients = 10;
|
||
const persistentNotificationInterval = 5;
|
||
const currentUserId = 'current_user_id';
|
||
const channelName = 'channel_id__teammate_id';
|
||
const intl = createIntl({locale: DEFAULT_LOCALE, messages: getTranslations(DEFAULT_LOCALE)});
|
||
|
||
it('should show alert with DM channel description when channelType is DM_CHANNEL', async () => {
|
||
const mockUser = TestHelper.fakeUserModel({username: 'teammate'});
|
||
jest.mocked(getUserById).mockResolvedValue(mockUser);
|
||
|
||
await persistentNotificationsConfirmation(
|
||
serverUrl,
|
||
value,
|
||
mentionsList,
|
||
intl,
|
||
sendMessage,
|
||
persistentNotificationMaxRecipients,
|
||
persistentNotificationInterval,
|
||
currentUserId,
|
||
channelName,
|
||
General.DM_CHANNEL,
|
||
);
|
||
|
||
expect(Alert.alert).toHaveBeenCalledWith(
|
||
'Send persistent notifications',
|
||
`${mockUser.username} will be notified every ${persistentNotificationInterval} minutes until they’ve acknowledged or replied to the message.`,
|
||
expect.any(Array),
|
||
);
|
||
});
|
||
|
||
it('should show alert when special mentions are present', async () => {
|
||
await persistentNotificationsConfirmation(
|
||
serverUrl,
|
||
'@channel',
|
||
mentionsList,
|
||
intl,
|
||
sendMessage,
|
||
persistentNotificationMaxRecipients,
|
||
persistentNotificationInterval,
|
||
currentUserId,
|
||
channelName,
|
||
);
|
||
|
||
expect(Alert.alert).toHaveBeenCalledWith(
|
||
'',
|
||
'Cannot use @channel, @all or @here to mention recipients of persistent notifications.',
|
||
expect.any(Array),
|
||
);
|
||
});
|
||
|
||
it('should show alert when no mentions found', async () => {
|
||
jest.mocked(getUsersCountFromMentions).mockResolvedValue(0);
|
||
|
||
await persistentNotificationsConfirmation(
|
||
serverUrl,
|
||
value,
|
||
mentionsList,
|
||
intl,
|
||
sendMessage,
|
||
persistentNotificationMaxRecipients,
|
||
persistentNotificationInterval,
|
||
currentUserId,
|
||
channelName,
|
||
);
|
||
|
||
expect(Alert.alert).toHaveBeenCalledWith(
|
||
'Recipients must be @mentioned',
|
||
'There are no recipients mentioned in your message. You’ll need add mentions to be able to send persistent notifications.',
|
||
expect.any(Array),
|
||
);
|
||
});
|
||
|
||
it('should show alert when mentions exceed max recipients', async () => {
|
||
jest.mocked(getUsersCountFromMentions).mockResolvedValue(15);
|
||
|
||
await persistentNotificationsConfirmation(
|
||
serverUrl,
|
||
value,
|
||
mentionsList,
|
||
intl,
|
||
sendMessage,
|
||
persistentNotificationMaxRecipients,
|
||
persistentNotificationInterval,
|
||
currentUserId,
|
||
channelName,
|
||
);
|
||
|
||
expect(Alert.alert).toHaveBeenCalledWith(
|
||
'Too many recipients',
|
||
`You can send persistent notifications to a maximum of ${persistentNotificationMaxRecipients} recipients. There are ${mentionsList.length} recipients mentioned in your message. You’ll need to change who you’ve mentioned before you can send.`,
|
||
expect.any(Array),
|
||
);
|
||
});
|
||
|
||
it('should show confirmation alert for valid mentions within limit', async () => {
|
||
jest.mocked(getUsersCountFromMentions).mockResolvedValue(5);
|
||
|
||
await persistentNotificationsConfirmation(
|
||
serverUrl,
|
||
value,
|
||
mentionsList,
|
||
intl,
|
||
sendMessage,
|
||
persistentNotificationMaxRecipients,
|
||
persistentNotificationInterval,
|
||
currentUserId,
|
||
channelName,
|
||
);
|
||
|
||
expect(Alert.alert).toHaveBeenCalledWith(
|
||
'Send persistent notifications',
|
||
`Mentioned recipients will be notified every ${persistentNotificationInterval} minutes until they’ve acknowledged or replied to the message.`,
|
||
expect.any(Array),
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('postUserDisplayName', () => {
|
||
it('should return the override username if from webhook and override is enabled', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {
|
||
from_webhook: 'true',
|
||
override_username: 'webhook_user',
|
||
},
|
||
});
|
||
|
||
const result = postUserDisplayName(post, undefined, undefined, true);
|
||
expect(result).toBe('webhook_user');
|
||
});
|
||
|
||
it('should return the author’s display name if not from webhook or override is disabled', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {
|
||
from_webhook: 'false',
|
||
},
|
||
});
|
||
const author = TestHelper.fakeUserModel({
|
||
username: 'user1',
|
||
locale: 'en',
|
||
});
|
||
|
||
const result = postUserDisplayName(post, author, undefined, false);
|
||
expect(result).toBe('user1');
|
||
});
|
||
|
||
it('should return the author’s display name using the teammate name display', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {
|
||
from_webhook: 'false',
|
||
},
|
||
});
|
||
const author = TestHelper.fakeUserModel({
|
||
username: 'user1',
|
||
locale: 'en',
|
||
});
|
||
|
||
const result = postUserDisplayName(post, author, 'nickname', false);
|
||
expect(result).toBe('user1');
|
||
});
|
||
});
|
||
|
||
describe('shouldIgnorePost', () => {
|
||
it('should return true if the post type is in the ignore list', () => {
|
||
const post = TestHelper.fakePost({
|
||
type: Post.POST_TYPES.CHANNEL_DELETED,
|
||
});
|
||
|
||
const result = shouldIgnorePost(post);
|
||
expect(result).toBe(true);
|
||
});
|
||
|
||
it('should return false if the post type is not in the ignore list', () => {
|
||
const post = TestHelper.fakePost({
|
||
type: Post.POST_TYPES.EPHEMERAL,
|
||
});
|
||
|
||
const result = shouldIgnorePost(post);
|
||
expect(result).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('processPostsFetched', () => {
|
||
it('should process the fetched posts correctly', () => {
|
||
const data = {
|
||
order: ['post1', 'post2'],
|
||
posts: {
|
||
post1: TestHelper.fakePost({id: 'post1', message: 'First post'}),
|
||
post2: TestHelper.fakePost({id: 'post2', message: 'Second post'}),
|
||
},
|
||
prev_post_id: 'post0',
|
||
};
|
||
|
||
const result = processPostsFetched(data);
|
||
expect(result).toEqual({
|
||
posts: [
|
||
expect.objectContaining({id: 'post1', message: 'First post'}),
|
||
expect.objectContaining({id: 'post2', message: 'Second post'}),
|
||
],
|
||
order: ['post1', 'post2'],
|
||
previousPostId: 'post0',
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('getLastFetchedAtFromPosts', () => {
|
||
it('should return the maximum timestamp from the posts', () => {
|
||
const posts = [
|
||
TestHelper.fakePost({create_at: 1000, update_at: 2000, delete_at: 0}),
|
||
TestHelper.fakePost({create_at: 1500, update_at: 2500, delete_at: 3000}),
|
||
];
|
||
|
||
const result = getLastFetchedAtFromPosts(posts);
|
||
expect(result).toBe(3000);
|
||
});
|
||
|
||
it('should return 0 if no posts are provided', () => {
|
||
const result = getLastFetchedAtFromPosts();
|
||
expect(result).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('moreThan5minAgo', () => {
|
||
it('should return true if the time is more than 5 minutes ago', () => {
|
||
const time = Date.now() - toMilliseconds({minutes: 6});
|
||
const result = moreThan5minAgo(time);
|
||
expect(result).toBe(true);
|
||
});
|
||
|
||
it('should return false if the time is within 5 minutes', () => {
|
||
const time = Date.now() - toMilliseconds({minutes: 4});
|
||
const result = moreThan5minAgo(time);
|
||
expect(result).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('hasInteractivePostContent', () => {
|
||
it('should return false when mmBlocksEnabled is false', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {
|
||
mm_blocks: [{type: 'text', text: {type: 'plain_text', text: 'Hello'}}],
|
||
},
|
||
});
|
||
|
||
expect(hasInteractivePostContent(post, false)).toBe(false);
|
||
});
|
||
|
||
it('should return false when mmBlocksEnabled is true but props are missing', () => {
|
||
const post = TestHelper.fakePostModel({props: undefined});
|
||
|
||
expect(hasInteractivePostContent(post, true)).toBe(false);
|
||
});
|
||
|
||
it('should return false when all interactive arrays are empty', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {
|
||
mm_blocks: [],
|
||
blocks: [],
|
||
cards: [],
|
||
attachments: [],
|
||
},
|
||
});
|
||
|
||
expect(hasInteractivePostContent(post, true)).toBe(false);
|
||
});
|
||
|
||
it('should return false when interactive props are not arrays', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {
|
||
mm_blocks: 'invalid',
|
||
blocks: {},
|
||
cards: null,
|
||
attachments: 'invalid',
|
||
},
|
||
});
|
||
|
||
expect(hasInteractivePostContent(post, true)).toBe(false);
|
||
});
|
||
|
||
it('should return true when mm_blocks are present', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {
|
||
mm_blocks: [{type: 'text', text: {type: 'plain_text', text: 'Hello'}}],
|
||
},
|
||
});
|
||
|
||
expect(hasInteractivePostContent(post, true)).toBe(true);
|
||
});
|
||
|
||
it('should return true when blocks are present', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {
|
||
blocks: [{type: 'section', text: {type: 'mrkdwn', text: 'Block content'}}],
|
||
},
|
||
});
|
||
|
||
expect(hasInteractivePostContent(post, true)).toBe(true);
|
||
});
|
||
|
||
it('should return true when cards are present', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {
|
||
cards: [{header: {title: 'Card title'}, sections: []}],
|
||
},
|
||
});
|
||
|
||
expect(hasInteractivePostContent(post, true)).toBe(true);
|
||
});
|
||
|
||
it('should return true when attachments are present', () => {
|
||
const post = TestHelper.fakePostModel({
|
||
props: {
|
||
attachments: [{text: 'Attachment body'}],
|
||
},
|
||
});
|
||
|
||
expect(hasInteractivePostContent(post, true)).toBe(true);
|
||
});
|
||
|
||
it('should work with Post objects', () => {
|
||
const post = TestHelper.fakePost({
|
||
props: {
|
||
attachments: [{text: 'Attachment body'}],
|
||
},
|
||
});
|
||
|
||
expect(hasInteractivePostContent(post, true)).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('scheduledPostFromPost', () => {
|
||
const post: Post = mockedPosts.posts[mockedPosts.order[0]];
|
||
|
||
const schedulingInfo: SchedulingInfo = {
|
||
scheduled_at: Date.now() + 10000,
|
||
};
|
||
|
||
const postPriority: PostPriority = {
|
||
priority: 'important',
|
||
};
|
||
|
||
const postFiles: FileInfo[] = [
|
||
TestHelper.fakeFileInfo({id: 'fileid1'}),
|
||
TestHelper.fakeFileInfo({id: 'fileid2'}),
|
||
];
|
||
|
||
it('should create a scheduled post with the given scheduling info', () => {
|
||
const result: ScheduledPost = scheduledPostFromPost(post, schedulingInfo);
|
||
expect(result.scheduled_at).toBe(schedulingInfo.scheduled_at);
|
||
});
|
||
|
||
it('should not include the post priority if its a post in thread', () => {
|
||
const result: ScheduledPost = scheduledPostFromPost(post, schedulingInfo, postPriority);
|
||
expect(result.priority).toBeUndefined();
|
||
});
|
||
|
||
it('should include the file IDs if post files are provided', () => {
|
||
const result: ScheduledPost = scheduledPostFromPost(post, schedulingInfo, postPriority, postFiles);
|
||
expect(result.file_ids).toEqual(['fileid1', 'fileid2']);
|
||
});
|
||
|
||
it('should include the post files if provided', () => {
|
||
const result: ScheduledPost = scheduledPostFromPost(post, schedulingInfo, postPriority, postFiles);
|
||
expect(result.metadata?.files).toEqual(postFiles);
|
||
});
|
||
|
||
it('should return a scheduled post with the same properties as the original post', () => {
|
||
const result: ScheduledPost = scheduledPostFromPost(post, schedulingInfo);
|
||
expect(result.id).toBe(post.id);
|
||
expect(result.message).toBe(post.message);
|
||
expect(result.channel_id).toBe(post.channel_id);
|
||
expect(result.create_at).toBe(post.create_at);
|
||
expect(result.update_at).toBe(post.update_at);
|
||
expect(result.user_id).toBe(post.user_id);
|
||
});
|
||
});
|
||
|
||
describe('permalink redaction', () => {
|
||
const embed = (metadata: object): PermalinkEmbedData => ({
|
||
post_id: 'linked-post-id',
|
||
post: {metadata},
|
||
team_name: 'team',
|
||
channel_display_name: 'channel',
|
||
channel_type: 'O',
|
||
channel_id: 'channel-id',
|
||
} as PermalinkEmbedData);
|
||
|
||
it('should treat a non-zero embed redacted count as denied', () => {
|
||
expect(isPermalinkEmbedRedacted(embed({redacted_file_count: 2}))).toBe(true);
|
||
expect(isPermalinkEmbedRedacted(embed({redacted_file_count: 0}))).toBe(false);
|
||
expect(isPermalinkEmbedRedacted(undefined)).toBe(false);
|
||
});
|
||
|
||
it('should report the embed count when the embed denies access', () => {
|
||
expect(getPermalinkRedactedFileCount(embed({redacted_file_count: 2}), 0)).toBe(2);
|
||
});
|
||
|
||
it('should report nothing redacted when the embed lists files', () => {
|
||
// The embed is authoritative, so a stale stored count must not resurrect the placeholder.
|
||
expect(getPermalinkRedactedFileCount(embed({files: [{id: 'file-1'}]}), 3)).toBe(0);
|
||
});
|
||
|
||
it('should fall back to the linked post when the embed says neither', () => {
|
||
expect(getPermalinkRedactedFileCount(embed({}), 3)).toBe(3);
|
||
expect(getPermalinkRedactedFileCount(undefined, 3)).toBe(3);
|
||
});
|
||
});
|
||
|
||
describe('isAttachmentDecisionCurrent', () => {
|
||
it('should render a decision confirmed under the required epoch', () => {
|
||
expect(isAttachmentDecisionCurrent(true, 3, 3, 2)).toBe(true);
|
||
});
|
||
|
||
it('should gate a decision behind the required epoch while enforced', () => {
|
||
expect(isAttachmentDecisionCurrent(true, 2, 3, 0)).toBe(false);
|
||
});
|
||
|
||
it('should render allowed attachments behind the epoch once enforcement is off', () => {
|
||
// Nothing new is redacted, so without a cached denial there is nothing to re-check.
|
||
expect(isAttachmentDecisionCurrent(false, 0, 3, 0)).toBe(true);
|
||
});
|
||
|
||
it('should re-check a denial cached while enforced once enforcement is off', () => {
|
||
// Its file rows are gone and no since-fetch re-delivers it.
|
||
expect(isAttachmentDecisionCurrent(false, 2, 3, 1)).toBe(false);
|
||
});
|
||
});
|
||
});
|