Files
Desktop/ts/test-node/state/ducks/donations_test.preload.ts

333 lines
12 KiB
TypeScript

// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import * as sinon from 'sinon';
import type { StateType } from '../../../state/reducer.preload.js';
import { reducer as rootReducer } from '../../../state/reducer.preload.js';
import { noopAction } from '../../../state/ducks/noop.std.js';
import { applyDonationBadge } from '../../../state/ducks/donations.preload.js';
import * as conversations from '../../../state/ducks/conversations.preload.js';
import type { BadgeType } from '../../../badges/types.std.js';
import { BadgeCategory } from '../../../badges/BadgeCategory.std.js';
import type { ConversationType } from '../../../state/ducks/conversations.preload.js';
import { generateAci } from '../../../types/ServiceId.std.js';
describe('donations duck', () => {
const getEmptyRootState = (): StateType =>
rootReducer(undefined, noopAction());
const storageMap = new Map<string, unknown>();
const storage = {
get: (key: string): unknown => storageMap.get(key),
put: async (key: string, value: unknown): Promise<void> => {
storageMap.set(key, value);
},
remove: async (key: string): Promise<void> => {
storageMap.delete(key);
},
};
beforeEach(() => {
storageMap.clear();
});
describe('applyDonationBadge thunk', () => {
let sandbox: sinon.SinonSandbox;
let myProfileChangedStub: sinon.SinonStub;
let originalMyProfileChanged: typeof conversations.actions.myProfileChanged;
const TEST_BADGE: BadgeType = {
id: 'boost-badge',
category: BadgeCategory.Donor,
name: 'Boost',
descriptionTemplate: 'Boost badge',
images: [],
};
const ourConversationId = 'our-conversation-id';
beforeEach(async () => {
sandbox = sinon.createSandbox();
// Mock myProfileChanged by replacing the function directly
myProfileChangedStub = sandbox.stub().returns(() => Promise.resolve());
originalMyProfileChanged = conversations.actions.myProfileChanged;
conversations.actions.myProfileChanged = myProfileChangedStub;
});
afterEach(async () => {
// Restore original myProfileChanged
conversations.actions.myProfileChanged = originalMyProfileChanged;
sandbox.restore();
});
const createMeWithBadges = (
badges: Array<{
id: string;
isVisible?: boolean;
}>
): ConversationType => ({
id: ourConversationId,
serviceId: generateAci(),
badges,
type: 'direct',
title: 'Me',
acceptedMessageRequest: true,
isMe: true,
});
const createRootState = (me: ConversationType): StateType => {
const state = getEmptyRootState();
return {
...state,
user: {
...state.user,
ourConversationId,
},
conversations: {
...state.conversations,
conversationLookup: {
[ourConversationId]: me,
},
},
};
};
// Helper to create test setup for a scenario
const setupTest = async (
badges: Array<{ id: string; isVisible?: boolean }>
) => {
const me = createMeWithBadges(badges);
const rootState = createRootState(me);
const getState = () => rootState;
const dispatch = sandbox.stub().callsFake(async (action: unknown) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return Promise.resolve();
});
const onComplete = sandbox.stub();
// Helper to execute applyDonationBadge with common params
const executeApplyDonationBadge = async (
badge: BadgeType | undefined,
applyBadge: boolean
) => {
await applyDonationBadge({
badge,
applyBadge,
onComplete,
storage,
})(dispatch, getState, null);
};
return { me, onComplete, executeApplyDonationBadge };
};
describe('Modal States', () => {
describe('All badges invisible (previousDisplayBadgesOnProfile = false)', () => {
it('Submit ON: Makes ALL badges visible, boost primary', async () => {
// Setup: Boost invisible (along with other badges)
const { onComplete, executeApplyDonationBadge } = await setupTest([
{ id: 'other-badge', isVisible: false },
{ id: 'boost-badge', isVisible: false },
]);
// Action: Submit with toggle ON (checkbox checked)
await executeApplyDonationBadge(TEST_BADGE, true);
// Result: Boost becomes visible and primary,
// ALL other badges become visible
sinon.assert.calledOnce(myProfileChangedStub);
const profileData = myProfileChangedStub.getCall(0).args[0];
assert.deepEqual(profileData.badges, [
{ id: 'boost-badge', isVisible: true }, // Primary
{ id: 'other-badge', isVisible: true }, // Now visible too
]);
// Verify storage was updated from false to true
assert.equal(storage.get('displayBadgesOnProfile'), true);
// Note: storageServiceUploadJob would be called here with
// { reason: 'donation-badge-toggle' } but we can't spy on const exports
sinon.assert.calledOnceWithExactly(onComplete);
});
it('Submit OFF: No change', async () => {
// Setup: Boost invisible
const { onComplete, executeApplyDonationBadge } = await setupTest([
{ id: 'boost-badge', isVisible: false },
{ id: 'other-badge', isVisible: false },
]);
// Action: Submit with toggle OFF (checkbox unchecked)
await executeApplyDonationBadge(TEST_BADGE, false);
// Result: No change (badges remain invisible)
// Since boost is not primary, nothing happens
sinon.assert.notCalled(myProfileChangedStub);
// Verify storage was written with false (even though unchanged)
assert.equal(storage.get('displayBadgesOnProfile'), false);
// Note: storageServiceUploadJob would not be called here
sinon.assert.calledOnceWithExactly(onComplete);
});
});
describe('All badges visible, boost primary (previousDisplayBadgesOnProfile = true)', () => {
it('Submit ON: No change', async () => {
// Setup: Boost primary
const { onComplete, executeApplyDonationBadge } = await setupTest([
{ id: 'boost-badge', isVisible: true }, // Primary (index 0)
{ id: 'other-badge', isVisible: true },
]);
// Action: Submit with toggle ON (checkbox checked)
await executeApplyDonationBadge(TEST_BADGE, true);
// Result: No change (boost remains primary)
// myProfileChanged still called but with same order
sinon.assert.calledOnce(myProfileChangedStub);
const profileData = myProfileChangedStub.getCall(0).args[0];
assert.deepEqual(profileData.badges, [
{ id: 'boost-badge', isVisible: true }, // Still primary
{ id: 'other-badge', isVisible: true },
]);
// Verify storage remains at true (no update needed)
assert.equal(storage.get('displayBadgesOnProfile'), true);
// Note: storageServiceUploadJob would not be called here (no change)
sinon.assert.calledOnceWithExactly(onComplete);
});
it('Submit OFF: Hides all badges', async () => {
// Setup: Boost primary
const { onComplete, executeApplyDonationBadge } = await setupTest([
{ id: 'boost-badge', isVisible: true }, // Primary (index 0)
{ id: 'other-badge', isVisible: true },
]);
// Action: Submit with toggle OFF (checkbox unchecked)
await executeApplyDonationBadge(TEST_BADGE, false);
// Result: All badges become invisible
sinon.assert.calledOnce(myProfileChangedStub);
const profileData = myProfileChangedStub.getCall(0).args[0];
assert.deepEqual(profileData.badges, []);
// Verify storage was updated from true to false
assert.equal(storage.get('displayBadgesOnProfile'), false);
sinon.assert.calledOnceWithExactly(onComplete);
});
});
describe('All badges visible, boost not primary (previousDisplayBadgesOnProfile = true)', () => {
it('Submit ON: Makes boost primary', async () => {
// Setup: Boost not primary (other badge is index 0)
const { onComplete, executeApplyDonationBadge } = await setupTest([
{ id: 'other-badge', isVisible: true }, // Primary (index 0)
{ id: 'boost-badge', isVisible: true }, // Not primary
]);
// Action: Submit with toggle ON (checkbox checked)
await executeApplyDonationBadge(TEST_BADGE, true);
// Result: Boost moves to primary position
sinon.assert.calledOnce(myProfileChangedStub);
const profileData = myProfileChangedStub.getCall(0).args[0];
assert.deepEqual(profileData.badges, [
{ id: 'boost-badge', isVisible: true }, // Moved to primary
{ id: 'other-badge', isVisible: true }, // Previous primary shifts down
]);
// Verify storage remains at true (no update needed)
assert.equal(storage.get('displayBadgesOnProfile'), true);
sinon.assert.calledOnceWithExactly(onComplete);
});
it('Submit OFF: No change', async () => {
// Setup: Boost not primary (other badge is index 0)
const { onComplete, executeApplyDonationBadge } = await setupTest([
{ id: 'other-badge', isVisible: true }, // Primary (index 0)
{ id: 'boost-badge', isVisible: true }, // Not primary
]);
// Action: Submit with toggle OFF (checkbox unchecked)
await executeApplyDonationBadge(TEST_BADGE, false);
// Result: No change (other badge remains primary)
sinon.assert.notCalled(myProfileChangedStub);
// Verify storage remains at true (no update needed)
assert.equal(storage.get('displayBadgesOnProfile'), true);
sinon.assert.calledOnceWithExactly(onComplete);
});
});
});
describe('Error Scenarios', () => {
it('No Badge Data: should show error', async () => {
const { onComplete, executeApplyDonationBadge } = await setupTest([]);
// Modal receives undefined badge
await executeApplyDonationBadge(undefined, true);
sinon.assert.calledOnce(onComplete);
const error = onComplete.getCall(0).args[0];
assert.instanceOf(error, Error);
assert.equal(error.message, 'No badge was given to redeem');
sinon.assert.notCalled(myProfileChangedStub);
});
it('Badge Visibility Data Corrupted: should show error', async () => {
const { onComplete, executeApplyDonationBadge } = await setupTest([
{ id: 'boost-badge' }, // Missing isVisible property
]);
await executeApplyDonationBadge(TEST_BADGE, true);
// Should show error toast
sinon.assert.calledOnce(onComplete);
const error = onComplete.getCall(0).args[0];
assert.instanceOf(error, Error);
assert.equal(
error.message,
"Unable to determine user's existing visible badges"
);
sinon.assert.notCalled(myProfileChangedStub);
});
it("User Doesn't Have Badge: should show error", async () => {
const { onComplete, executeApplyDonationBadge } = await setupTest([
{ id: 'other-badge', isVisible: true },
// boost-badge not in list
]);
await executeApplyDonationBadge(TEST_BADGE, true);
// Should show error toast
sinon.assert.calledOnce(onComplete);
const error = onComplete.getCall(0).args[0];
assert.instanceOf(error, Error);
assert.equal(
error.message,
'User does not have the desired badge to apply'
);
sinon.assert.notCalled(myProfileChangedStub);
});
});
});
});