Files
native/src/lib/__tests__/push-background-task.test.ts
T
Linus Rath 95c24a4214 fix: stop notifications fetching sender favicons when favicons are off
The "Sender Favicons" setting only covered the in-app avatars. Push
notifications always fetched the sender's favicon from
icons.duckduckgo.com, so with the setting off every notification still
sent the sender's domain and the device's IP to a third party.

The webmail has the same setting, on by default, and fetches through
its own `/api/favicon` proxy, so DuckDuckGo sees the server rather than
the user. Native has no proxy, but turning the default off would
silently drop the logos existing users see and diverge from the
webmail, which is a product call. So the default stays on and the one
existing switch now covers every favicon fetch: the notification task
reads it from the persisted settings and shows initials when it is off,
or when the settings cannot be read.
2026-09-24 19:03:34 +02:00

112 lines
4.1 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
vi.mock('../../api/jmap-client', () => ({
jmapClient: {
accountId: 'jmap-primary',
getStoredCredentials: vi.fn(async () => null),
setStoredCredentials: vi.fn(async () => undefined),
},
}));
vi.mock('../client-cert', () => ({ secureFetch: vi.fn(async () => ({ ok: false, status: 500 })) }));
vi.mock('../oauth', () => ({ refreshOAuthAccessToken: vi.fn(async (t: unknown) => t) }));
import AsyncStorage from '@react-native-async-storage/async-storage';
import {
matchAccountsForPush,
parseRelayPushData,
selectNotifiableEmails,
senderFaviconsAllowed,
} from '../push-background-task';
import type { Email } from '../../api/types';
describe('parseRelayPushData', () => {
it('decodes the relay FCM data payload (all values are strings)', () => {
const parsed = parseRelayPushData({
kind: 'jmap-email-push',
accountLabel: 'alice',
accountId: 'a1',
emailIds: JSON.stringify(['m1', 'm2']),
changed: JSON.stringify({ a1: { EmailDelivery: 's1' } }),
});
expect(parsed).toEqual({
kind: 'jmap-email-push',
accountLabel: 'alice',
jmapAccountId: 'a1',
emailIds: ['m1', 'm2'],
changed: { a1: { EmailDelivery: 's1' } },
});
});
it('falls back to the first key of `changed` for the account id', () => {
const parsed = parseRelayPushData({
kind: 'jmap-state-change',
emailIds: '[]',
changed: JSON.stringify({ a9: { Email: 'x' } }),
});
expect(parsed.jmapAccountId).toBe('a9');
expect(parsed.emailIds).toEqual([]);
});
it('tolerates garbage', () => {
expect(parseRelayPushData(null).emailIds).toEqual([]);
expect(parseRelayPushData({ emailIds: '{not json' }).emailIds).toEqual([]);
expect(parseRelayPushData({ kind: 'weird' }).kind).toBeNull();
});
});
describe('matchAccountsForPush', () => {
const accounts = ['alice@mail.example.com', 'bob@mail.example.com'];
const registry = [
{ id: 'alice@mail.example.com', username: 'alice' },
{ id: 'bob@mail.example.com', username: 'bob' },
];
it('matches on the recorded JMAP account id first', () => {
const payload = parseRelayPushData({ accountId: 'jb', accountLabel: 'alice' });
expect(matchAccountsForPush(payload, accounts, { 'bob@mail.example.com': 'jb' }, registry))
.toEqual(['bob@mail.example.com']);
});
it('falls back to the relay accountLabel (username)', () => {
const payload = parseRelayPushData({ accountId: 'unknown', accountLabel: 'alice' });
expect(matchAccountsForPush(payload, accounts, {}, registry)).toEqual(['alice@mail.example.com']);
});
it('checks every account when nothing matches', () => {
const payload = parseRelayPushData({ accountId: 'unknown', accountLabel: 'carol' });
expect(matchAccountsForPush(payload, accounts, {}, registry)).toEqual(accounts);
});
});
describe('selectNotifiableEmails', () => {
const email = (id: string, keywords: Record<string, boolean> = {}): Email =>
({ id, threadId: 't', keywords, mailboxIds: {}, size: 0, receivedAt: '', hasAttachment: false } as Email);
it('drops read, junk and already-notified messages', () => {
const out = selectNotifiableEmails(
[email('a'), email('b', { $seen: true }), email('c', { $junk: true }), email('d')],
['d'],
);
expect(out.map((e) => e.id)).toEqual(['a']);
});
});
describe('senderFaviconsAllowed', () => {
const KEY = 'webmail:settings:v1';
it('follows the "Sender Favicons" setting the settings store persisted', async () => {
await AsyncStorage.setItem(KEY, JSON.stringify({ senderFavicons: false }));
expect(await senderFaviconsAllowed()).toBe(false);
await AsyncStorage.setItem(KEY, JSON.stringify({ senderFavicons: true }));
expect(await senderFaviconsAllowed()).toBe(true);
});
it('defaults to on like the store, but skips icons when the settings are unreadable', async () => {
await AsyncStorage.removeItem(KEY);
expect(await senderFaviconsAllowed()).toBe(true);
await AsyncStorage.setItem(KEY, '{not json');
expect(await senderFaviconsAllowed()).toBe(false);
await AsyncStorage.removeItem(KEY);
});
});