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
152 changes: 152 additions & 0 deletions src/__tests__/clan/ClanController/phraseDailyTask.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { ClanController } from '../../../clan/clan.controller';
import ClanNotifier from '../../../clan/clan.notifier';
import { SEReason } from '../../../common/service/basicService/SEReason';
import { ServerTaskName } from '../../../dailyTasks/enum/serverTaskName.enum';

describe('ClanController phrase daily task', () => {
const clanId = '67fe4e2d8a54d4cc39266a43';

const createController = ({
currentPhrase = 'Together we rise',
updateResult = [true, null],
taskErrors = null,
}: {
currentPhrase?: string;
updateResult?: [boolean | null, any[] | null];
taskErrors?: any[] | null;
} = {}) => {
const clanService = {
readOneById: jest
.fn()
.mockResolvedValue([{ phrase: currentPhrase }, null]),
updateOneById: jest.fn().mockResolvedValue(updateResult),
};
const progressResult = {
status: 'completed',
task: {
type: ServerTaskName.INNER_VOICE,
points: 10,
coins: 5,
clan_id: clanId,
},
completedByPlayerId: 'player-1',
clanId,
completedAmount: 1,
previousAmountLeft: 1,
currentAmountLeft: 0,
};
const dailyTasksService = {
updateClanTask: jest
.fn()
.mockResolvedValue([taskErrors ? null : progressResult, taskErrors]),
};
const dailyTaskProgressService = {
handleClanTaskCompletion: jest
.fn()
.mockResolvedValue([progressResult, null]),
notifyClanTaskCompletion: jest.fn(),
};
const session = {
startTransaction: jest.fn(),
commitTransaction: jest.fn(),
endSession: jest.fn(),
inTransaction: jest.fn().mockReturnValue(true),
abortTransaction: jest.fn(),
};
const connection = {
startSession: jest.fn().mockResolvedValue(session),
};
const notifier = jest
.spyOn(ClanNotifier.prototype, 'phraseUpdated')
.mockImplementation();
const controller = new ClanController(
clanService as any,
undefined as any,
undefined as any,
undefined as any,
undefined as any,
undefined as any,
undefined as any,
dailyTasksService as any,
dailyTaskProgressService as any,
connection as any,
);

return {
clanService,
controller,
dailyTasksService,
dailyTaskProgressService,
notifier,
session,
};
};

afterEach(() => {
jest.restoreAllMocks();
});

it('completes the clan task and publishes a clan update after saving a changed phrase', async () => {
const {
clanService,
controller,
dailyTasksService,
dailyTaskProgressService,
notifier,
session,
} = createController();
const body = { _id: clanId, phrase: 'Victory or nothing!' } as any;
const user = { clan_id: clanId, player_id: 'player-1' } as any;

await controller.update(body, user);

expect(clanService.updateOneById).toHaveBeenCalledWith(clanId, body, {
session,
});
expect(dailyTasksService.updateClanTask).toHaveBeenCalledWith(
clanId,
'player-1',
ServerTaskName.INNER_VOICE,
session,
);
expect(
dailyTaskProgressService.handleClanTaskCompletion,
).toHaveBeenCalledWith(
expect.any(Object),
session,
false,
);
expect(session.commitTransaction).toHaveBeenCalled();
expect(dailyTaskProgressService.notifyClanTaskCompletion).toHaveBeenCalled();
expect(notifier).toHaveBeenCalledWith(clanId, 'Victory or nothing!');
});

it('does not progress the task or publish MQTT when the phrase is unchanged', async () => {
const { controller, dailyTasksService, notifier } = createController();
const body = { _id: clanId, phrase: 'Together we rise' } as any;
const user = { clan_id: clanId, player_id: 'player-1' } as any;

await controller.update(body, user);

expect(dailyTasksService.updateClanTask).not.toHaveBeenCalled();
expect(notifier).not.toHaveBeenCalled();
});

it('rolls back the phrase update and publishes no notifications when clan-task progression fails', async () => {
const { controller, dailyTaskProgressService, notifier, session } =
createController({
taskErrors: [{ reason: SEReason.UNEXPECTED }],
});
const body = { _id: clanId, phrase: 'Victory or nothing!' } as any;
const user = { clan_id: clanId, player_id: 'player-1' } as any;

await controller.update(body, user);

expect(session.abortTransaction).toHaveBeenCalled();
expect(session.commitTransaction).not.toHaveBeenCalled();
expect(
dailyTaskProgressService.notifyClanTaskCompletion,
).not.toHaveBeenCalled();
expect(notifier).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,16 @@ describe('MQTT notification contract', () => {
new ClanNotifier().memberLeave('clan-1', 'player-1');
expectLastPayloadToMatchEnvelope('clan', MqttNotificationType.MEMBER_LEFT);

new ClanNotifier().phraseUpdated('clan-1', 'Together we rise');
expect(publishMock).toHaveBeenLastCalledWith(
`/clan/clan-1/${NotificationResource.CLAN}/phrase/${NotificationStatus.UPDATE}`,
JSON.stringify({
topic: 'clan',
type: MqttNotificationType.CLAN_UPDATED,
payload: { clan_id: 'clan-1', phrase: 'Together we rise' },
}),
);

const friendshipNotifier = new FriendshipNotifier({
findOne: jest.fn().mockReturnValue({
select: jest.fn().mockReturnValue({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { ServerTaskName } from '../../../dailyTasks/enum/serverTaskName.enum';
import { OldTaskName } from '../../../dailyTasks/enum/oldTaskNames.enum';
import { uiDailyTasks } from '../../../dailyTasks/uiDailyTasks/uiDailyTasks';
import { DailyTasksStartupRefreshService } from '../../../dailyTasks/dailyTasksStartupRefresh.service';
import {
ACTIVE_SERVER_TASK_DEFINITIONS,
MIN_OCCURRENCES_PER_TASK_TYPE,
TaskGeneratorService,
} from '../../../dailyTasks/taskGenerator.service';

describe('DailyTasksStartupRefreshService', () => {
const createService = ({
Expand Down Expand Up @@ -53,7 +58,10 @@ describe('DailyTasksStartupRefreshService', () => {
},
startSession: jest.fn(async () => session),
};
const service = new DailyTasksStartupRefreshService(connection as any);
const service = new DailyTasksStartupRefreshService(
connection as any,
new TaskGeneratorService(),
);
const ownerId = (service as any).ownerId;

if (!lockOwnerId) {
Expand Down Expand Up @@ -168,6 +176,7 @@ describe('DailyTasksStartupRefreshService', () => {
ServerTaskName.BANISH_THE_EARWORM,
ServerTaskName.GO_TO_BATTLE,
ServerTaskName.FORM_AN_INNER_CONNECTION,
ServerTaskName.INNER_VOICE,
].includes(task.type),
),
).toBe(true);
Expand All @@ -186,4 +195,21 @@ describe('DailyTasksStartupRefreshService', () => {
ownerId,
});
});

it('creates a balanced server-task pool including INNER_VOICE', () => {
const { service } = createService({ oldTaskFindResults: [] });
const tasks = (service as any).createServerTasksForClan({
_id: 'clan-1',
});

expect(tasks).toHaveLength(11);
expect(
tasks.filter((task) => task.type === ServerTaskName.INNER_VOICE).length,
).toBeGreaterThanOrEqual(MIN_OCCURRENCES_PER_TASK_TYPE);
for (const { type } of ACTIVE_SERVER_TASK_DEFINITIONS) {
expect(
tasks.filter((task) => task.type === type).length,
).toBeGreaterThanOrEqual(MIN_OCCURRENCES_PER_TASK_TYPE);
}
});
});
71 changes: 71 additions & 0 deletions src/__tests__/dailyTasks/clanTaskProgress.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { DailyTasksService } from '../../dailyTasks/dailyTasks.service';
import { ServerTaskName } from '../../dailyTasks/enum/serverTaskName.enum';

describe('DailyTasksService.updateClanTask', () => {
it('progresses INNER_VOICE by clan without a player reservation', async () => {
const service = new DailyTasksService(
{} as any,
{} as any,
{} as any,
{} as any,
{
createTaskRandomValues: jest.fn().mockReturnValue({
type: ServerTaskName.GO_TO_BATTLE,
amount: 2,
points: 10,
coins: 5,
title: { fi: 'Pelaa 2 taistelua' },
}),
} as any,
{} as any,
);
const basicService = {
readOne: jest.fn().mockResolvedValue([
{
_id: 'task-1',
clan_id: 'clan-1',
player_id: null,
type: ServerTaskName.INNER_VOICE,
amount: 1,
amountLeft: 1,
points: 10,
coins: 5,
},
null,
]),
updateOne: jest.fn().mockResolvedValue([true, null]),
};
(service as any).basicService = basicService;

const [result, errors] = await service.updateClanTask(
'clan-1',
'player-1',
ServerTaskName.INNER_VOICE,
{} as any,
);

expect(errors).toBeNull();
expect(result).toMatchObject({
status: 'completed',
completedByPlayerId: 'player-1',
clanId: 'clan-1',
currentAmountLeft: 0,
});
expect(basicService.readOne).toHaveBeenCalledWith({
filter: {
clan_id: 'clan-1',
type: ServerTaskName.INNER_VOICE,
amountLeft: { $gt: 0 },
},
session: expect.anything(),
});
expect(basicService.updateOne).toHaveBeenCalledWith(
expect.objectContaining({
$unset: { player_id: '', startedAt: '' },
}),
expect.objectContaining({
filter: { _id: 'task-1', clan_id: 'clan-1' },
}),
);
});
});
30 changes: 30 additions & 0 deletions src/__tests__/dailyTasks/dailyTaskProgress.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,36 @@ describe('DailyTaskProgressService', () => {
);
});

it('should complete a clan task without rewarding or notifying a player', async () => {
const result = makeResult('completed', 'inner_voice');
clanProgression.handleClanProgression.mockResolvedValue([
{ reachedMilestones: [100] },
null,
]);

const [handled, error] = await service.handleClanTaskCompletion(
result,
session,
);

expect(error).toBeNull();
expect(handled.reachedMilestones).toEqual([100]);
expect(playerRewarder.rewardForPlayerTask).not.toHaveBeenCalled();
expect(notifier.taskUpdated).not.toHaveBeenCalled();
expect(notifier.taskCompleted).not.toHaveBeenCalled();
expect(clanRewarder.rewardClanForPlayerTask).toHaveBeenCalledWith(
'clan-1',
result.task.points,
result.task.coins,
session,
);
expect(notifier.taskCompletedForClan).toHaveBeenCalledWith(
'clan-1',
result.task,
'player-1',
);
});

it('should complete player-only task without rewarding or notifying clan', async () => {
const result = {
...makeResult('completed', 'banish_the_earworm'),
Expand Down
42 changes: 41 additions & 1 deletion src/__tests__/dailyTasks/dailyTaskScoringValues.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { defaultPredefinedDailyTasks } from '../../box/dailyTask/defaultPredefinedDailyTasks';
import { Score } from '../../common/values/scoring.values';
import { TaskGeneratorService } from '../../dailyTasks/taskGenerator.service';
import {
ACTIVE_SERVER_TASK_DEFINITIONS,
MIN_OCCURRENCES_PER_TASK_TYPE,
TaskGeneratorService,
} from '../../dailyTasks/taskGenerator.service';
import { ServerTaskName } from '../../dailyTasks/enum/serverTaskName.enum';
import { uiDailyTasks } from '../../dailyTasks/uiDailyTasks/uiDailyTasks';

describe('daily task scoring values', () => {
Expand All @@ -12,6 +17,41 @@ describe('daily task scoring values', () => {
expect(task.points).toBe(Score.DAILY_TASK.COMPLETED);
});

it('configures INNER_VOICE as a one-step clan motto task', () => {
const generator = new TaskGeneratorService();
jest
.spyOn(generator, 'getRandomTaskType')
.mockReturnValue(ServerTaskName.INNER_VOICE);

expect(generator.createTaskRandomValues()).toMatchObject({
type: ServerTaskName.INNER_VOICE,
amount: 1,
title: {
fi: 'Avaa klaanin asetukset. Muokkaa klaanin mottoa ja tallenna muutos. Mieti, mitä haluatte viestiä toisillenne ja muille.',
},
});
});

it('creates a balanced, shuffled server-task bag', () => {
const generator = new TaskGeneratorService();
const taskTypes = generator.createBalancedTaskTypes();

expect(taskTypes).toHaveLength(11);
for (const { type } of ACTIVE_SERVER_TASK_DEFINITIONS) {
expect(
taskTypes.filter((taskType) => taskType === type).length,
).toBeGreaterThanOrEqual(MIN_OCCURRENCES_PER_TASK_TYPE);
}
});

it('fails clearly when the requested task count cannot satisfy the minimum occurrence guarantee', () => {
const generator = new TaskGeneratorService();

expect(() => generator.createBalancedTaskTypes(1)).toThrow(
'Cannot generate 1 server tasks',
);
});

it('uses the shared completed daily task score for default predefined tasks', () => {
expect(defaultPredefinedDailyTasks).toEqual(
expect.arrayContaining([
Expand Down
1 change: 0 additions & 1 deletion src/box/sessionStarter/sessionStarter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ export default class SessionStarterService {
},
undefined,
{
filter: {},
session,
},
);
Expand Down
Loading
Loading