Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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