Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .infra/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,10 @@ export const workers: Worker[] = [
topic: 'api.v1.user-streak-updated',
subscription: 'api.user-streak-reset-notification',
},
{
topic: 'api.v1.user-streak-updated',
subscription: 'api.streak-freeze-used-notification',
},
{
topic: 'api.v1.squad-featured-updated',
subscription: 'api.squad-featured-updated-notification',
Expand Down
4 changes: 4 additions & 0 deletions __tests__/__snapshots__/settings.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Object {
"optOutLevelSystem": false,
"optOutQuestSystem": false,
"optOutReadingStreak": false,
"optOutStreakFreeze": false,
"optOutWeeklyGoal": false,
"showOnlyUnreadPosts": false,
"showTopSites": true,
Expand Down Expand Up @@ -63,6 +64,7 @@ Object {
"optOutLevelSystem": false,
"optOutQuestSystem": false,
"optOutReadingStreak": false,
"optOutStreakFreeze": false,
"optOutWeeklyGoal": false,
"showOnlyUnreadPosts": false,
"showTopSites": true,
Expand Down Expand Up @@ -103,6 +105,7 @@ Object {
"optOutLevelSystem": false,
"optOutQuestSystem": false,
"optOutReadingStreak": false,
"optOutStreakFreeze": false,
"optOutWeeklyGoal": false,
"showOnlyUnreadPosts": false,
"showTopSites": true,
Expand Down Expand Up @@ -143,6 +146,7 @@ Object {
"optOutLevelSystem": false,
"optOutQuestSystem": false,
"optOutReadingStreak": false,
"optOutStreakFreeze": false,
"optOutWeeklyGoal": false,
"showOnlyUnreadPosts": false,
"showTopSites": true,
Expand Down
139 changes: 138 additions & 1 deletion __tests__/cron/updateCurrentStreak.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { crons } from '../../src/cron/index';
import cron from '../../src/cron/updateCurrentStreak';
import { usersFixture } from '../fixture';
import { User, UserStreak } from '../../src/entity';
import { Settings, User, UserStreak } from '../../src/entity';
import { doNotFake, expectSuccessfulCron, saveFixtures } from '../helpers';
import { DataSource } from 'typeorm';
import createOrGetConnection from '../../src/db';
import nock from 'nock';
import { UserStreakAction, UserStreakActionType } from '../../src/entity';
import { CoresRole } from '../../src/types';

let con: DataSource;

Expand Down Expand Up @@ -244,4 +245,140 @@ describe('updateCurrentStreak cron', () => {
expect(streak.currentStreak).toBe(0);
});
});

describe('incorporating streak freeze', () => {
beforeEach(async () => {
nock('http://localhost:5000').post('/e').reply(204);
await con
.getRepository(User)
.update({ id: '1' }, { coresRole: CoresRole.User });
});

it('should consume a single streak freeze instead of resetting when one day was missed', async () => {
await con
.getRepository(UserStreak)
.update({ userId: '1' }, { freezesAvailable: 3 });

await expectSuccessfulCron(cron);

const streak = await con
.getRepository(UserStreak)
.findOneBy({ userId: '1' });
expect(streak.currentStreak).toBe(1);
expect(streak.freezesAvailable).toBe(2);

const freezeActions = await con
.getRepository(UserStreakAction)
.findBy({ userId: '1', type: UserStreakActionType.Freeze });
expect(freezeActions).toHaveLength(1);
expect(freezeActions[0].createdAt).toEqual(new Date('2024-06-25'));
});

it('should consume one freeze per missed day when the gap spans multiple days', async () => {
await con.getRepository(UserStreak).update(
{ userId: '1' },
{
lastViewAt: new Date('2024-06-23'), // Sunday
freezesAvailable: 2,
},
);

await expectSuccessfulCron(cron);

const streak = await con
.getRepository(UserStreak)
.findOneBy({ userId: '1' });
expect(streak.currentStreak).toBe(1);
expect(streak.freezesAvailable).toBe(0);

const freezeActions = await con
.getRepository(UserStreakAction)
.findBy({ userId: '1', type: UserStreakActionType.Freeze });
expect(freezeActions).toHaveLength(2);
const sortedTimes = freezeActions
.map(({ createdAt }) => createdAt.getTime())
.sort((a, b) => a - b);
expect(sortedTimes).toEqual([
new Date('2024-06-24').getTime(),
new Date('2024-06-25').getTime(),
]);
});

it('should reset the streak when the freeze balance cannot cover the whole gap', async () => {
await con.getRepository(UserStreak).update(
{ userId: '1' },
{
lastViewAt: new Date('2024-06-23'), // Sunday
freezesAvailable: 1,
},
);

await expectSuccessfulCron(cron);

const streak = await con
.getRepository(UserStreak)
.findOneBy({ userId: '1' });
expect(streak.currentStreak).toBe(0);
expect(streak.freezesAvailable).toBe(1);

const freezeActions = await con
.getRepository(UserStreakAction)
.findBy({ userId: '1', type: UserStreakActionType.Freeze });
expect(freezeActions).toHaveLength(0);
});

it('should reset the streak when the user opted out of automated streak freeze', async () => {
await con
.getRepository(UserStreak)
.update({ userId: '1' }, { freezesAvailable: 3 });
await con
.getRepository(Settings)
.save({ userId: '1', optOutStreakFreeze: true });

await expectSuccessfulCron(cron);

const streak = await con
.getRepository(UserStreak)
.findOneBy({ userId: '1' });
expect(streak.currentStreak).toBe(0);
expect(streak.freezesAvailable).toBe(3);
});

it('should reset the streak when the user has no Cores access, even with freezes available', async () => {
await con
.getRepository(User)
.update({ id: '1' }, { coresRole: CoresRole.None });
await con
.getRepository(UserStreak)
.update({ userId: '1' }, { freezesAvailable: 3 });

await expectSuccessfulCron(cron);

const streak = await con
.getRepository(UserStreak)
.findOneBy({ userId: '1' });
expect(streak.currentStreak).toBe(0);
expect(streak.freezesAvailable).toBe(3);
});

it('should not consume a freeze twice for the same missed day across consecutive cron runs', async () => {
await con
.getRepository(UserStreak)
.update({ userId: '1' }, { freezesAvailable: 3 });

await expectSuccessfulCron(cron);
await expectSuccessfulCron(cron);

const streak = await con
.getRepository(UserStreak)
.findOneBy({ userId: '1' });
expect(streak.currentStreak).toBe(1);
expect(streak.freezesAvailable).toBe(2);

const freezeActions = await con
.getRepository(UserStreakAction)
.findBy({ userId: '1', type: UserStreakActionType.Freeze });
expect(freezeActions).toHaveLength(1);
});
});
});
22 changes: 22 additions & 0 deletions __tests__/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ describe('mutation updateUserSettings', () => {
customLinks
optOutWeeklyGoal
optOutReadingStreak
optOutStreakFreeze
optOutLevelSystem
optOutQuestSystem
optOutCompanion
Expand Down Expand Up @@ -433,6 +434,27 @@ describe('mutation updateUserSettings', () => {
expect(updated.optOutQuestSystem).toBe(true);
});

it('should update optOutStreakFreeze', async () => {
loggedUser = '1';

const repo = con.getRepository(Settings);
await repo.save(
repo.create({
userId: '1',
optOutStreakFreeze: false,
}),
);

const res = await client.mutate(MUTATION, {
variables: { data: { optOutStreakFreeze: true } },
});

expect(res.data.updateUserSettings.optOutStreakFreeze).toBe(true);

const updated = await repo.findOneByOrFail({ userId: '1' });
expect(updated.optOutStreakFreeze).toBe(true);
});

it('should update campaignCtaPlacement', async () => {
loggedUser = '1';

Expand Down
Loading
Loading