Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate GWH golem response with Zod #600

Merged
merged 1 commit into from
Dec 9, 2024
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
31 changes: 20 additions & 11 deletions src/scripts/modules/ajax-handlers/golem.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {LoggerService} from "@scripts/util/logger";
import {AjaxSuccessHandler} from "./ajaxSuccessHandler";
import {GolemPayload, GolemResponse} from "./golem.types";
import {GolemPayload, GolemResponse, golemResponseSchema} from "./golem.types";
import {HgResponse, JournalMarkup} from "@scripts/types/hg";
import {EventDates} from "@scripts/util/constants";
import {hasEventEnded} from "@scripts/util/time";

const rarities = ["area", "hat", "scarf"] as const;

Expand All @@ -17,25 +15,20 @@ export class GWHGolemAjaxHandler extends AjaxSuccessHandler {
}

public match(url: string): boolean {
// Triggers on Golem claim, dispatch, upgrade, and on "Decorate" click (+others, perhaps).
if (!url.includes("mousehuntgame.com/managers/ajax/events/winter_hunt_region.php")) {
return false;
}

if (hasEventEnded(EventDates.GreatWinterHuntEndDate)) {
return false;
}

return true;
}

public async execute(responseJSON: HgResponse): Promise<void> {
// Triggers on Golem claim, dispatch, upgrade, and on "Decorate" click (+others, perhaps).
if (!(responseJSON && typeof responseJSON === 'object' && 'golem_rewards' in responseJSON)) {
this.logger.debug("Skipped GWH golem submission since there are no golem rewards.", responseJSON);
if (!(this.isGolemRewardResponse(responseJSON))) {
return;
}

const golemData: GolemResponse = responseJSON.golem_rewards as GolemResponse;
const golemData = responseJSON.golem_rewards;
const uid = responseJSON.user.sn_user_id.toString();
if (!uid) {
this.logger.warn("Skipped GWH golem submission due to missing user attribution.", responseJSON);
Expand Down Expand Up @@ -131,4 +124,20 @@ export class GWHGolemAjaxHandler extends AjaxSuccessHandler {

return null;
}

/**
* Validates that the given object is a JSON response from retrieving golem rewards
* @param responseJSON
* @returns
*/
private isGolemRewardResponse(responseJSON: unknown): responseJSON is GolemResponse {
const response = golemResponseSchema.safeParse(responseJSON);

if (!response.success) {
const errorMessage = response.error.message;
this.logger.debug("Skipped GWH golem submission since there are no golem rewards.", errorMessage);
}

return response.success;
}
}
43 changes: 32 additions & 11 deletions src/scripts/modules/ajax-handlers/golem.types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,39 @@
import z from 'zod';
import {hgResponseSchema} from '@scripts/types/hg';
import {zodRecordWithEnum} from '@scripts/util/zod';

/**
* An item brought back back a golem
*/
const golemItemSchema = z.object({
name: z.string(),
quantity: z.coerce.number(),
});

type GolemItem = z.infer<typeof golemItemSchema>;

const raritySchema = z.enum(["area", "hat", "scarf"]);
export type Rarity = z.infer<typeof raritySchema>;

/**
* Golem Response from HG, previewed by CBS
*/
const golemRewardsSchema = z.object({
items: zodRecordWithEnum(raritySchema, z.array(golemItemSchema)),
});

export type GolemRewards = z.infer<typeof golemRewardsSchema>;

export const golemResponseSchema = hgResponseSchema.extend({
golem_rewards: golemRewardsSchema,
});

export type GolemResponse = z.infer<typeof golemResponseSchema>;

/*
export interface GolemResponse {
items: Record<Rarity, GolemItem[]>
//bonus_items: [];
// bonus_items: [];
// num_upgrade_items: number;
// num_gilded_charms: number;
// num_hailstones: number;
Expand All @@ -21,16 +51,7 @@ export interface GolemResponse {
// level: number;
// };
}

/**
* An item brought back back a golem
*/
interface GolemItem {
name: string;
quantity: number;
}

export type Rarity = "area" | "hat" | "scarf";
*/

/**
* The data that will be recorded externally
Expand Down
3 changes: 0 additions & 3 deletions src/scripts/util/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,4 @@ export class CustomConvertibleIds {
export class EventDates {
// KGA
public static readonly KingsGiveawayEndDate: Date = new Date("2024-07-09T15:00:00Z");

// GWH
public static readonly GreatWinterHuntEndDate: Date = new Date("2024-01-16T16:00:00Z");
}
41 changes: 22 additions & 19 deletions tests/scripts/modules/ajax-handlers/golem.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {GWHGolemAjaxHandler} from '@scripts/modules/ajax-handlers/golem';
import type {GolemPayload} from '@scripts/modules/ajax-handlers/golem.types';
import type {GolemPayload, GolemResponse} from '@scripts/modules/ajax-handlers/golem.types';
import {HgResponse} from '@scripts/types/hg';
import {ConsoleLogger} from '@scripts/util/logger';
import {HgResponseBuilder} from '@tests/utility/builders';

jest.mock('@scripts/util/logger');
import {ConsoleLogger} from '@scripts/util/logger';

const logger = new ConsoleLogger();
const showFlashMessage = jest.fn();
Expand All @@ -21,15 +22,7 @@ describe('GWHGolemAjaxHandler', () => {
expect(handler.match('mousehuntgame.com/managers/ajax/events/kings_giveaway.php')).toBe(false);
});

it('is false when GWH is done', () => {
// return the day after our filter
Date.now = jest.fn(() => new Date('2024-01-22T05:00:00Z').getTime());

expect(handler.match(gwhURL)).toBe(false);
});

it('is true on match during event', () => {
Date.now = jest.fn(() => new Date('2023-12-07T05:00:00Z').getTime());
it('is true when url is gwh', () => {
expect(handler.match(gwhURL)).toBe(true);
});
});
Expand All @@ -40,19 +33,27 @@ describe('GWHGolemAjaxHandler', () => {

handler.execute({} as unknown as HgResponse);

expect(logger.debug).toHaveBeenCalledWith('Skipped GWH golem submission since there are no golem rewards.', {});
expect(logger.debug).toHaveBeenCalledWith('Skipped GWH golem submission since there are no golem rewards.', expect.anything());
expect(handler.submitGolems).not.toHaveBeenCalled();
});

it('calls submitGolems with expected data', () => {

const builder = new HgResponseBuilder()
.withJournalMarkup(testResponses.prologuePondResponse.journal_markup);

const response: GolemResponse = {
...builder.build(),
golem_rewards: testResponses.prologuePondResponse.golem_rewards,
};
Date.now = jest.fn(() => 12345);
handler.submitGolems = jest.fn();

handler.execute(testResponses.prologuePondResponse);
handler.execute(response);

const expectedPayload: GolemPayload[] = [
{
uid: '987654321',
uid: '2',
location: 'Prologue Pond',
timestamp: 12345,
loot: [
Expand Down Expand Up @@ -87,10 +88,6 @@ describe('GWHGolemAjaxHandler', () => {
const testResponses = {
// responses are the minimum that are required for the test to pass
prologuePondResponse: {
user: {
user_id: 'not this',
sn_user_id: 987654321,
},
golem_rewards: {
items: {
area: [
Expand Down Expand Up @@ -119,9 +116,15 @@ const testResponses = {
journal_markup: [
{
render_data: {
entry_id: 1,
mouse_type: false,
css_class: '',
entry_date: '1:23 pm',
environment: 'Town of Gnawnia',
entry_timestamp: 1234567890,
text: 'My golem returned from the Prologue Pond with 1',
},
},
],
} as unknown as HgResponse,
},
};
Loading