mattermost-mobile-upstream/app/client/rest/drafts.test.ts
Guillermo Vayá 1013adb891 feat(drafts): Phase 2 — draft sync API DTOs, REST client, normalization, capability
Client, mapping, and capability layer only — intentionally NOT wired into any
lifecycle/trigger path yet (no coordinator, no workers).

- types/api/drafts.d.ts: add DraftApi (server response) and DraftUpsertRequest
  (POST body) DTOs alongside DraftProps.
- types/api/config.d.ts: add AllowSyncedDrafts to ClientConfig.
- client/rest/base.ts: add draft route helpers (drafts POST, user/team GET,
  channel/thread DELETE).
- client/rest/drafts.ts (+test): ClientDrafts mixin — getDrafts, upsertDraft,
  deleteDraft (channel vs thread by rootId), Connection-Id header on POST/DELETE.
  Mixed into client/rest/index.ts (also drops a pre-existing duplicate
  ClientScheduledPost entry in the .with() list).
- constants/preferences.ts: add ADVANCED_SYNC_DRAFTS ('sync_drafts').
- utils/draft/sync.ts (+test): pure bidirectional normalization. server->mobile
  maps update_at->server_update_at (never local update_at), priority, props
  passthrough, exact file_ids, files from metadata.files; mobile->server sends
  only channel_id/root_id/message/type/props/file_ids/priority and returns null
  for an empty message so it never POSTs. Fail-closed burn-on-read reconstruction
  (durations come from server config, not props; missing/invalid -> no enabled
  borConfig).
- queries/servers/drafts.ts: getIsDraftSyncEnabled / observeIsDraftSyncEnabled —
  enabled when AllowSyncedDrafts==='true' AND sync_drafts pref not 'false'
  (absent = enabled). No server-version gate.

Checks: npm run tsc clean; eslint --quiet clean; 69 focused Jest tests pass
across the new client/sync suites and the scheduled_post/post-handler/drafts
regression neighbors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:49:47 +02:00

91 lines
2.8 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import TestHelper from '@test/test_helper';
import type ClientBase from './base';
import type {ClientDraftsMix} from './drafts';
describe('ClientDrafts', () => {
let client: ClientDraftsMix & ClientBase;
const draft: DraftUpsertRequest = {
channel_id: 'channel_id',
root_id: '',
message: 'draft message',
type: '',
props: {},
file_ids: [],
};
beforeAll(() => {
client = TestHelper.createClient();
client.doFetch = jest.fn();
});
test('getDrafts', async () => {
const teamId = 'team_id';
const groupLabel = 'WebSocket Reconnect';
const expectedUrl = client.getUserTeamDraftsRoute('me', teamId);
const expectedOptions = {
method: 'get',
groupLabel,
};
await client.getDrafts(teamId, groupLabel);
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
});
test('upsertDraft', async () => {
await client.upsertDraft(draft, 'connection_id');
const expectedUrl = client.getDraftsRoute();
const expectedOptions = {
method: 'post',
body: draft,
headers: {'Connection-Id': 'connection_id'},
};
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
});
test('upsertDraft without connection ID', async () => {
await client.upsertDraft(draft);
const expectedUrl = client.getDraftsRoute();
const expectedOptions = {
method: 'post',
body: draft,
headers: {},
};
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
});
test('deleteDraft for a channel draft (empty rootId) hits the channel route', async () => {
const channelId = 'channel_id';
const connectionId = 'connection_id';
const expectedUrl = client.getUserChannelDraftsRoute('me', channelId);
const expectedOptions = {
method: 'delete',
headers: {'Connection-Id': connectionId},
};
await client.deleteDraft(channelId, '', connectionId);
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
});
test('deleteDraft for a thread draft (non-empty rootId) hits the thread route', async () => {
const channelId = 'channel_id';
const rootId = 'root_id';
const expectedUrl = client.getUserThreadDraftRoute('me', channelId, rootId);
const expectedOptions = {
method: 'delete',
headers: {},
};
await client.deleteDraft(channelId, rootId);
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
});
});